Repository: 5e-bits/5e-srd-api Branch: main Commit: 03745614bfd9 Files: 567 Total size: 1.2 MB Directory structure: gitextract_5nuvmdzy/ ├── .dockerignore ├── .github/ │ ├── CODEOWNERS │ ├── FUNDING.yml │ ├── PULL_REQUEST_TEMPLATE.md │ ├── actions/ │ │ └── validate-pr-title/ │ │ └── action.yml │ ├── dependabot.yml │ └── workflows/ │ ├── ci.yml │ ├── codeql-analysis.yml │ ├── lint-pr.yml │ ├── release-please.yml │ └── release.yml ├── .gitignore ├── .nvmrc ├── .prettierrc ├── .redocly.yaml ├── CHANGELOG.md ├── Dockerfile ├── LICENSE.md ├── README.md ├── app.json ├── docker-compose.yml ├── eslint.config.js ├── heroku.yml ├── nodemon.json ├── openapi-to-postman.json ├── package.json ├── release-please-config.json ├── src/ │ ├── controllers/ │ │ ├── api/ │ │ │ ├── 2014/ │ │ │ │ ├── abilityScoreController.ts │ │ │ │ ├── alignmentController.ts │ │ │ │ ├── backgroundController.ts │ │ │ │ ├── classController.ts │ │ │ │ ├── conditionController.ts │ │ │ │ ├── damageTypeController.ts │ │ │ │ ├── equipmentCategoryController.ts │ │ │ │ ├── equipmentController.ts │ │ │ │ ├── featController.ts │ │ │ │ ├── featureController.ts │ │ │ │ ├── languageController.ts │ │ │ │ ├── magicItemController.ts │ │ │ │ ├── magicSchoolController.ts │ │ │ │ ├── monsterController.ts │ │ │ │ ├── proficiencyController.ts │ │ │ │ ├── raceController.ts │ │ │ │ ├── ruleController.ts │ │ │ │ ├── ruleSectionController.ts │ │ │ │ ├── skillController.ts │ │ │ │ ├── spellController.ts │ │ │ │ ├── subclassController.ts │ │ │ │ ├── subraceController.ts │ │ │ │ ├── traitController.ts │ │ │ │ └── weaponPropertyController.ts │ │ │ ├── 2024/ │ │ │ │ ├── abilityScoreController.ts │ │ │ │ ├── alignmentController.ts │ │ │ │ ├── backgroundController.ts │ │ │ │ ├── conditionController.ts │ │ │ │ ├── damageTypeController.ts │ │ │ │ ├── equipmentCategoryController.ts │ │ │ │ ├── equipmentController.ts │ │ │ │ ├── featController.ts │ │ │ │ ├── languageController.ts │ │ │ │ ├── magicItemController.ts │ │ │ │ ├── magicSchoolController.ts │ │ │ │ ├── proficiencyController.ts │ │ │ │ ├── skillController.ts │ │ │ │ ├── speciesController.ts │ │ │ │ ├── subclassController.ts │ │ │ │ ├── subspeciesController.ts │ │ │ │ ├── traitController.ts │ │ │ │ ├── weaponMasteryPropertyController.ts │ │ │ │ └── weaponPropertyController.ts │ │ │ ├── imageController.ts │ │ │ ├── v2014Controller.ts │ │ │ └── v2024Controller.ts │ │ ├── apiController.ts │ │ ├── docsController.ts │ │ └── simpleController.ts │ ├── css/ │ │ └── custom.css │ ├── graphql/ │ │ ├── 2014/ │ │ │ ├── common/ │ │ │ │ ├── choiceTypes.ts │ │ │ │ ├── equipmentTypes.ts │ │ │ │ ├── interfaces.ts │ │ │ │ └── unions.ts │ │ │ ├── resolvers/ │ │ │ │ ├── abilityScore/ │ │ │ │ │ ├── args.ts │ │ │ │ │ └── resolver.ts │ │ │ │ ├── alignment/ │ │ │ │ │ ├── args.ts │ │ │ │ │ └── resolver.ts │ │ │ │ ├── background/ │ │ │ │ │ ├── args.ts │ │ │ │ │ └── resolver.ts │ │ │ │ ├── class/ │ │ │ │ │ ├── args.ts │ │ │ │ │ └── resolver.ts │ │ │ │ ├── condition/ │ │ │ │ │ ├── args.ts │ │ │ │ │ └── resolver.ts │ │ │ │ ├── damageType/ │ │ │ │ │ ├── args.ts │ │ │ │ │ └── resolver.ts │ │ │ │ ├── equipment/ │ │ │ │ │ ├── args.ts │ │ │ │ │ └── resolver.ts │ │ │ │ ├── equipmentCategory/ │ │ │ │ │ ├── args.ts │ │ │ │ │ └── resolver.ts │ │ │ │ ├── feat/ │ │ │ │ │ ├── args.ts │ │ │ │ │ └── resolver.ts │ │ │ │ ├── feature/ │ │ │ │ │ ├── args.ts │ │ │ │ │ └── resolver.ts │ │ │ │ ├── index.ts │ │ │ │ ├── language/ │ │ │ │ │ ├── args.ts │ │ │ │ │ └── resolver.ts │ │ │ │ ├── level/ │ │ │ │ │ ├── args.ts │ │ │ │ │ └── resolver.ts │ │ │ │ ├── magicItem/ │ │ │ │ │ ├── args.ts │ │ │ │ │ └── resolver.ts │ │ │ │ ├── magicSchool/ │ │ │ │ │ ├── args.ts │ │ │ │ │ └── resolver.ts │ │ │ │ ├── monster/ │ │ │ │ │ ├── args.ts │ │ │ │ │ └── resolver.ts │ │ │ │ ├── proficiency/ │ │ │ │ │ ├── args.ts │ │ │ │ │ └── resolver.ts │ │ │ │ ├── race/ │ │ │ │ │ ├── args.ts │ │ │ │ │ └── resolver.ts │ │ │ │ ├── rule/ │ │ │ │ │ ├── args.ts │ │ │ │ │ └── resolver.ts │ │ │ │ ├── ruleSection/ │ │ │ │ │ ├── args.ts │ │ │ │ │ └── resolver.ts │ │ │ │ ├── skill/ │ │ │ │ │ ├── args.ts │ │ │ │ │ └── resolver.ts │ │ │ │ ├── spell/ │ │ │ │ │ ├── args.ts │ │ │ │ │ └── resolver.ts │ │ │ │ ├── subclass/ │ │ │ │ │ ├── args.ts │ │ │ │ │ └── resolver.ts │ │ │ │ ├── subrace/ │ │ │ │ │ ├── args.ts │ │ │ │ │ └── resolver.ts │ │ │ │ ├── trait/ │ │ │ │ │ ├── args.ts │ │ │ │ │ └── resolver.ts │ │ │ │ └── weaponProperty/ │ │ │ │ ├── args.ts │ │ │ │ └── resolver.ts │ │ │ ├── types/ │ │ │ │ ├── backgroundTypes.ts │ │ │ │ ├── featureTypes.ts │ │ │ │ ├── monsterTypes.ts │ │ │ │ ├── proficiencyTypes.ts │ │ │ │ ├── startingEquipment/ │ │ │ │ │ ├── choice.ts │ │ │ │ │ ├── common.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── optionSet.ts │ │ │ │ ├── subclassTypes.ts │ │ │ │ └── traitTypes.ts │ │ │ └── utils/ │ │ │ ├── helpers.ts │ │ │ ├── resolvers.ts │ │ │ └── startingEquipmentResolver.ts │ │ ├── 2024/ │ │ │ ├── common/ │ │ │ │ ├── choiceTypes.ts │ │ │ │ ├── equipmentTypes.ts │ │ │ │ ├── interfaces.ts │ │ │ │ ├── resolver.ts │ │ │ │ └── unions.ts │ │ │ ├── resolvers/ │ │ │ │ ├── abilityScore/ │ │ │ │ │ ├── args.ts │ │ │ │ │ └── resolver.ts │ │ │ │ ├── alignment/ │ │ │ │ │ ├── args.ts │ │ │ │ │ └── resolver.ts │ │ │ │ ├── background/ │ │ │ │ │ ├── args.ts │ │ │ │ │ └── resolver.ts │ │ │ │ ├── condition/ │ │ │ │ │ ├── args.ts │ │ │ │ │ └── resolver.ts │ │ │ │ ├── damageType/ │ │ │ │ │ ├── args.ts │ │ │ │ │ └── resolver.ts │ │ │ │ ├── equipment/ │ │ │ │ │ ├── args.ts │ │ │ │ │ └── resolver.ts │ │ │ │ ├── equipmentCategory/ │ │ │ │ │ ├── args.ts │ │ │ │ │ └── resolver.ts │ │ │ │ ├── feat/ │ │ │ │ │ ├── args.ts │ │ │ │ │ └── resolver.ts │ │ │ │ ├── index.ts │ │ │ │ ├── language/ │ │ │ │ │ ├── args.ts │ │ │ │ │ └── resolver.ts │ │ │ │ ├── magicItem/ │ │ │ │ │ ├── args.ts │ │ │ │ │ └── resolver.ts │ │ │ │ ├── magicSchool/ │ │ │ │ │ ├── args.ts │ │ │ │ │ └── resolver.ts │ │ │ │ ├── proficiency/ │ │ │ │ │ ├── args.ts │ │ │ │ │ └── resolver.ts │ │ │ │ ├── skill/ │ │ │ │ │ ├── args.ts │ │ │ │ │ └── resolver.ts │ │ │ │ ├── species/ │ │ │ │ │ ├── args.ts │ │ │ │ │ └── resolver.ts │ │ │ │ ├── subclass/ │ │ │ │ │ ├── args.ts │ │ │ │ │ └── resolver.ts │ │ │ │ ├── subspecies/ │ │ │ │ │ ├── args.ts │ │ │ │ │ └── resolver.ts │ │ │ │ ├── trait/ │ │ │ │ │ ├── args.ts │ │ │ │ │ └── resolver.ts │ │ │ │ ├── weaponMasteryProperty/ │ │ │ │ │ ├── args.ts │ │ │ │ │ └── resolver.ts │ │ │ │ └── weaponProperty/ │ │ │ │ ├── args.ts │ │ │ │ └── resolver.ts │ │ │ ├── types/ │ │ │ │ └── backgroundEquipment/ │ │ │ │ ├── choice.ts │ │ │ │ ├── common.ts │ │ │ │ ├── index.ts │ │ │ │ └── optionSet.ts │ │ │ └── utils/ │ │ │ ├── backgroundEquipmentResolver.ts │ │ │ ├── choiceResolvers.ts │ │ │ └── resolvers.ts │ │ ├── common/ │ │ │ ├── args.ts │ │ │ ├── choiceTypes.ts │ │ │ ├── enums.ts │ │ │ ├── inputs.ts │ │ │ └── types.ts │ │ └── utils/ │ │ └── resolvers.ts │ ├── middleware/ │ │ ├── apolloServer.ts │ │ ├── bugsnag.ts │ │ └── errorHandler.ts │ ├── models/ │ │ ├── 2014/ │ │ │ ├── abilityScore.ts │ │ │ ├── alignment.ts │ │ │ ├── background.ts │ │ │ ├── class.ts │ │ │ ├── collection.ts │ │ │ ├── condition.ts │ │ │ ├── damageType.ts │ │ │ ├── equipment.ts │ │ │ ├── equipmentCategory.ts │ │ │ ├── feat.ts │ │ │ ├── feature.ts │ │ │ ├── language.ts │ │ │ ├── level.ts │ │ │ ├── magicItem.ts │ │ │ ├── magicSchool.ts │ │ │ ├── monster.ts │ │ │ ├── proficiency.ts │ │ │ ├── race.ts │ │ │ ├── rule.ts │ │ │ ├── ruleSection.ts │ │ │ ├── skill.ts │ │ │ ├── spell.ts │ │ │ ├── subclass.ts │ │ │ ├── subrace.ts │ │ │ ├── trait.ts │ │ │ └── weaponProperty.ts │ │ ├── 2024/ │ │ │ ├── abilityScore.ts │ │ │ ├── alignment.ts │ │ │ ├── background.ts │ │ │ ├── collection.ts │ │ │ ├── condition.ts │ │ │ ├── damageType.ts │ │ │ ├── equipment.ts │ │ │ ├── equipmentCategory.ts │ │ │ ├── feat.ts │ │ │ ├── language.ts │ │ │ ├── magicItem.ts │ │ │ ├── magicSchool.ts │ │ │ ├── proficiency.ts │ │ │ ├── skill.ts │ │ │ ├── species.ts │ │ │ ├── subclass.ts │ │ │ ├── subspecies.ts │ │ │ ├── trait.ts │ │ │ ├── weaponMasteryProperty.ts │ │ │ └── weaponProperty.ts │ │ └── common/ │ │ ├── apiReference.ts │ │ ├── areaOfEffect.ts │ │ ├── choice.ts │ │ ├── damage.ts │ │ └── difficultyClass.ts │ ├── public/ │ │ └── index.html │ ├── routes/ │ │ ├── api/ │ │ │ ├── 2014/ │ │ │ │ ├── abilityScores.ts │ │ │ │ ├── alignments.ts │ │ │ │ ├── backgrounds.ts │ │ │ │ ├── classes.ts │ │ │ │ ├── conditions.ts │ │ │ │ ├── damageTypes.ts │ │ │ │ ├── equipment.ts │ │ │ │ ├── equipmentCategories.ts │ │ │ │ ├── feats.ts │ │ │ │ ├── features.ts │ │ │ │ ├── images.ts │ │ │ │ ├── languages.ts │ │ │ │ ├── magicItems.ts │ │ │ │ ├── magicSchools.ts │ │ │ │ ├── monsters.ts │ │ │ │ ├── proficiencies.ts │ │ │ │ ├── races.ts │ │ │ │ ├── ruleSections.ts │ │ │ │ ├── rules.ts │ │ │ │ ├── skills.ts │ │ │ │ ├── spells.ts │ │ │ │ ├── subclasses.ts │ │ │ │ ├── subraces.ts │ │ │ │ ├── traits.ts │ │ │ │ └── weaponProperties.ts │ │ │ ├── 2014.ts │ │ │ ├── 2024/ │ │ │ │ ├── abilityScores.ts │ │ │ │ ├── alignments.ts │ │ │ │ ├── backgrounds.ts │ │ │ │ ├── conditions.ts │ │ │ │ ├── damageTypes.ts │ │ │ │ ├── equipment.ts │ │ │ │ ├── equipmentCategories.ts │ │ │ │ ├── feats.ts │ │ │ │ ├── languages.ts │ │ │ │ ├── magicItems.ts │ │ │ │ ├── magicSchools.ts │ │ │ │ ├── proficiencies.ts │ │ │ │ ├── skills.ts │ │ │ │ ├── species.ts │ │ │ │ ├── subclasses.ts │ │ │ │ ├── subspecies.ts │ │ │ │ ├── traits.ts │ │ │ │ ├── weaponMasteryProperties.ts │ │ │ │ └── weaponProperty.ts │ │ │ ├── 2024.ts │ │ │ └── images.ts │ │ └── api.ts │ ├── schemas/ │ │ └── schemas.ts │ ├── server.ts │ ├── start.ts │ ├── swagger/ │ │ ├── README.md │ │ ├── parameters/ │ │ │ ├── 2014/ │ │ │ │ ├── combined.yml │ │ │ │ ├── path/ │ │ │ │ │ ├── ability-scores.yml │ │ │ │ │ ├── alignments.yml │ │ │ │ │ ├── backgrounds.yml │ │ │ │ │ ├── classes.yml │ │ │ │ │ ├── common.yml │ │ │ │ │ ├── conditions.yml │ │ │ │ │ ├── damage-types.yml │ │ │ │ │ ├── equipment.yml │ │ │ │ │ ├── features.yml │ │ │ │ │ ├── languages.yml │ │ │ │ │ ├── magic-schools.yml │ │ │ │ │ ├── monsters.yml │ │ │ │ │ ├── proficiencies.yml │ │ │ │ │ ├── races.yml │ │ │ │ │ ├── rule-sections.yml │ │ │ │ │ ├── rules.yml │ │ │ │ │ ├── skills.yml │ │ │ │ │ ├── spells.yml │ │ │ │ │ ├── subclasses.yml │ │ │ │ │ ├── subraces.yml │ │ │ │ │ ├── traits.yml │ │ │ │ │ └── weapon-properties.yml │ │ │ │ └── query/ │ │ │ │ ├── classes.yml │ │ │ │ ├── monsters.yml │ │ │ │ └── spells.yml │ │ │ └── 2024/ │ │ │ └── .keepme │ │ ├── paths/ │ │ │ ├── 2014/ │ │ │ │ ├── ability-scores.yml │ │ │ │ ├── alignments.yml │ │ │ │ ├── backgrounds.yml │ │ │ │ ├── classes.yml │ │ │ │ ├── combined.yml │ │ │ │ ├── common.yml │ │ │ │ ├── conditions.yml │ │ │ │ ├── damage-types.yml │ │ │ │ ├── equipment-categories.yml │ │ │ │ ├── equipment.yml │ │ │ │ ├── feats.yml │ │ │ │ ├── features.yml │ │ │ │ ├── languages.yml │ │ │ │ ├── magic-items.yml │ │ │ │ ├── magic-schools.yml │ │ │ │ ├── monsters.yml │ │ │ │ ├── proficiencies.yml │ │ │ │ ├── races.yml │ │ │ │ ├── rule-sections.yml │ │ │ │ ├── rules.yml │ │ │ │ ├── skills.yml │ │ │ │ ├── spells.yml │ │ │ │ ├── subclasses.yml │ │ │ │ ├── subraces.yml │ │ │ │ ├── traits.yml │ │ │ │ └── weapon-properties.yml │ │ │ └── 2024/ │ │ │ └── .keepme │ │ ├── schemas/ │ │ │ ├── 2014/ │ │ │ │ ├── ability-scores.yml │ │ │ │ ├── alignments.yml │ │ │ │ ├── armor.yml │ │ │ │ ├── backgrounds.yml │ │ │ │ ├── classes.yml │ │ │ │ ├── combined.yml │ │ │ │ ├── common.yml │ │ │ │ ├── equipment.yml │ │ │ │ ├── feats.yml │ │ │ │ ├── features.yml │ │ │ │ ├── game-mechanics.yml │ │ │ │ ├── language.yml │ │ │ │ ├── monsters-common.yml │ │ │ │ ├── monsters.yml │ │ │ │ ├── multiclassing.yml │ │ │ │ ├── proficiencies.yml │ │ │ │ ├── races.yml │ │ │ │ ├── rules.yml │ │ │ │ ├── skills.yml │ │ │ │ ├── spell.yml │ │ │ │ ├── spellcasting.yml │ │ │ │ ├── subclass.yml │ │ │ │ ├── subrace.yml │ │ │ │ ├── traits.yml │ │ │ │ └── weapon.yml │ │ │ └── 2024/ │ │ │ └── .keepme │ │ └── swagger.yml │ ├── tests/ │ │ ├── controllers/ │ │ │ ├── api/ │ │ │ │ ├── 2014/ │ │ │ │ │ ├── abilityScoreController.test.ts │ │ │ │ │ ├── alignmentController.test.ts │ │ │ │ │ ├── backgroundController.test.ts │ │ │ │ │ ├── classController.test.ts │ │ │ │ │ ├── conditionController.test.ts │ │ │ │ │ ├── damageTypeController.test.ts │ │ │ │ │ ├── equipmentCategoryController.test.ts │ │ │ │ │ ├── equipmentController.test.ts │ │ │ │ │ ├── featController.test.ts │ │ │ │ │ ├── featureController.test.ts │ │ │ │ │ ├── languageController.test.ts │ │ │ │ │ ├── magicItemController.test.ts │ │ │ │ │ ├── magicSchoolController.test.ts │ │ │ │ │ ├── monsterController.test.ts │ │ │ │ │ ├── proficiencyController.test.ts │ │ │ │ │ ├── raceController.test.ts │ │ │ │ │ ├── ruleSectionController.test.ts │ │ │ │ │ ├── rulesController.test.ts │ │ │ │ │ ├── skillController.test.ts │ │ │ │ │ ├── spellController.test.ts │ │ │ │ │ ├── subclassController.test.ts │ │ │ │ │ ├── subraceController.test.ts │ │ │ │ │ ├── traitController.test.ts │ │ │ │ │ └── weaponPropertyController.test.ts │ │ │ │ ├── 2024/ │ │ │ │ │ ├── BackgroundController.test.ts │ │ │ │ │ ├── FeatController.test.ts │ │ │ │ │ ├── MagicItemController.test.ts │ │ │ │ │ ├── ProficiencyController.test.ts │ │ │ │ │ ├── SubclassController.test.ts │ │ │ │ │ ├── abilityScoreController.test.ts │ │ │ │ │ ├── alignmentController.test.ts │ │ │ │ │ ├── conditionController.test.ts │ │ │ │ │ ├── damageTypeController.test.ts │ │ │ │ │ ├── equipmentCategoryController.test.ts │ │ │ │ │ ├── equipmentController.test.ts │ │ │ │ │ ├── languageController.test.ts │ │ │ │ │ ├── magicSchoolController.test.ts │ │ │ │ │ ├── skillController.test.ts │ │ │ │ │ ├── speciesController.test.ts │ │ │ │ │ ├── subspeciesController.test.ts │ │ │ │ │ ├── traitController.test.ts │ │ │ │ │ ├── weaponMasteryPropertyController.test.ts │ │ │ │ │ └── weaponPropertyController.test.ts │ │ │ │ ├── v2014Controller.test.ts │ │ │ │ └── v2024Controller.test.ts │ │ │ ├── apiController.test.ts │ │ │ ├── globalSetup.ts │ │ │ └── simpleController.test.ts │ │ ├── factories/ │ │ │ ├── 2014/ │ │ │ │ ├── abilityScore.factory.ts │ │ │ │ ├── alignment.factory.ts │ │ │ │ ├── background.factory.ts │ │ │ │ ├── class.factory.ts │ │ │ │ ├── collection.factory.ts │ │ │ │ ├── common.factory.ts │ │ │ │ ├── condition.factory.ts │ │ │ │ ├── damageType.factory.ts │ │ │ │ ├── equipment.factory.ts │ │ │ │ ├── equipmentCategory.factory.ts │ │ │ │ ├── feat.factory.ts │ │ │ │ ├── feature.factory.ts │ │ │ │ ├── language.factory.ts │ │ │ │ ├── level.factory.ts │ │ │ │ ├── magicItem.factory.ts │ │ │ │ ├── magicSchool.factory.ts │ │ │ │ ├── monster.factory.ts │ │ │ │ ├── proficiency.factory.ts │ │ │ │ ├── race.factory.ts │ │ │ │ ├── rule.factory.ts │ │ │ │ ├── ruleSection.factory.ts │ │ │ │ ├── skill.factory.ts │ │ │ │ ├── spell.factory.ts │ │ │ │ ├── subclass.factory.ts │ │ │ │ ├── subrace.factory.ts │ │ │ │ ├── trait.factory.ts │ │ │ │ └── weaponProperty.factory.ts │ │ │ └── 2024/ │ │ │ ├── abilityScore.factory.ts │ │ │ ├── alignment.factory.ts │ │ │ ├── background.factory.ts │ │ │ ├── collection.factory.ts │ │ │ ├── common.factory.ts │ │ │ ├── condition.factory.ts │ │ │ ├── damageType.factory.ts │ │ │ ├── equipment.factory.ts │ │ │ ├── equipmentCategory.factory.ts │ │ │ ├── feat.factory.ts │ │ │ ├── language.factory.ts │ │ │ ├── magicItem.factory.ts │ │ │ ├── magicSchool.factory.ts │ │ │ ├── proficiency.factory.ts │ │ │ ├── skill.factory.ts │ │ │ ├── species.factory.ts │ │ │ ├── subclass.factory.ts │ │ │ ├── subspecies.factory.ts │ │ │ ├── trait.factory.ts │ │ │ ├── weaponMasteryProperty.factory.ts │ │ │ └── weaponProperty.factory.ts │ │ ├── integration/ │ │ │ ├── api/ │ │ │ │ ├── 2014/ │ │ │ │ │ ├── abilityScores.itest.ts │ │ │ │ │ ├── classes.itest.ts │ │ │ │ │ ├── conditions.itest.ts │ │ │ │ │ ├── damageTypes.itest.ts │ │ │ │ │ ├── equipment.itest.ts │ │ │ │ │ ├── equipmentCategories.itest.ts │ │ │ │ │ ├── feats.itest.ts │ │ │ │ │ ├── features.itest.ts │ │ │ │ │ ├── languages.itest.ts │ │ │ │ │ ├── magicItems.itest.ts │ │ │ │ │ ├── magicSchools.itest.ts │ │ │ │ │ ├── monsters.itest.ts │ │ │ │ │ ├── proficiencies.itest.ts │ │ │ │ │ ├── races.itest.ts │ │ │ │ │ ├── ruleSections.itest.ts │ │ │ │ │ ├── rules.itest.ts │ │ │ │ │ ├── skills.itest.ts │ │ │ │ │ ├── spells.itest.ts │ │ │ │ │ ├── subclasses.itest.ts │ │ │ │ │ ├── subraces.itest.ts │ │ │ │ │ ├── traits.itest.ts │ │ │ │ │ └── weaponProperties.itest.ts │ │ │ │ ├── 2024/ │ │ │ │ │ ├── abilityScores.itest.ts │ │ │ │ │ ├── alignment.itest.ts │ │ │ │ │ ├── background.itest.ts │ │ │ │ │ ├── condition.itest.ts │ │ │ │ │ ├── damageType.itest.ts │ │ │ │ │ ├── equipment.itest.ts │ │ │ │ │ ├── equipmentCategories.itest.ts │ │ │ │ │ ├── feat.itest.ts │ │ │ │ │ ├── language.itest.ts │ │ │ │ │ ├── magicItem.itest.ts │ │ │ │ │ ├── magicSchool.itest.ts │ │ │ │ │ ├── proficiency.itest.ts │ │ │ │ │ ├── skills.itest.ts │ │ │ │ │ ├── species.itest.ts │ │ │ │ │ ├── subclass.itest.ts │ │ │ │ │ ├── subspecies.itest.ts │ │ │ │ │ ├── traits.itest.ts │ │ │ │ │ ├── weaponMasteryProperty.itest.ts │ │ │ │ │ └── weaponProperty.itest.ts │ │ │ │ ├── abilityScores.itest.ts │ │ │ │ ├── classes.itest.ts │ │ │ │ ├── conditions.itest.ts │ │ │ │ ├── damageTypes.itest.ts │ │ │ │ ├── equipment.itest.ts │ │ │ │ ├── equipmentCategories.itest.ts │ │ │ │ ├── feats.itest.ts │ │ │ │ ├── features.itest.ts │ │ │ │ ├── languages.itest.ts │ │ │ │ ├── magicItems.itest.ts │ │ │ │ ├── magicSchools.itest.ts │ │ │ │ ├── monsters.itest.ts │ │ │ │ ├── proficiencies.itest.ts │ │ │ │ ├── races.itest.ts │ │ │ │ ├── ruleSections.itest.ts │ │ │ │ ├── rules.itest.ts │ │ │ │ ├── skills.itest.ts │ │ │ │ ├── spells.itest.ts │ │ │ │ ├── subclasses.itest.ts │ │ │ │ ├── subraces.itest.ts │ │ │ │ ├── traits.itest.ts │ │ │ │ └── weaponProperties.itest.ts │ │ │ └── server.itest.ts │ │ ├── support/ │ │ │ ├── db.ts │ │ │ ├── index.ts │ │ │ ├── requestHelpers.ts │ │ │ └── types.d.ts │ │ ├── util/ │ │ │ └── data.test.ts │ │ └── vitest.setup.ts │ └── util/ │ ├── RedisClient.ts │ ├── awsS3Client.ts │ ├── data.ts │ ├── environmentVariables.ts │ ├── index.ts │ ├── modelOptions.ts │ ├── prewarmCache.ts │ └── regex.ts ├── tsconfig.json ├── vitest.config.integration.ts └── vitest.config.ts ================================================ FILE CONTENTS ================================================ ================================================ FILE: .dockerignore ================================================ /node_modules/ dist # Git / VCS .git/ # Editor / IDE .vscode/ # Environment / Secrets .env # Logs *.log npm-debug.log* # OS generated files .DS_Store ================================================ FILE: .github/CODEOWNERS ================================================ # Default owner for all files * @bagelbits ================================================ FILE: .github/FUNDING.yml ================================================ # These are supported funding model platforms github: [5e-bits] patreon: # Replace with a single Patreon username open_collective: # Replace with a single Open Collective username ko_fi: bagelbits tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry liberapay: # Replace with a single Liberapay username issuehunt: # Replace with a single IssueHunt username otechie: # Replace with a single Otechie username custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] ================================================ FILE: .github/PULL_REQUEST_TEMPLATE.md ================================================ ## What does this do? \ ## How was it tested? \ ## Is there a Github issue this is resolving? \ ## Was any impacted documentation updated to reflect this change? \ ## Here's a fun image for your troubles \ ================================================ FILE: .github/actions/validate-pr-title/action.yml ================================================ name: 'Validate PR Title' description: 'Validates that PR titles follow conventional commit format with custom types' inputs: title: description: 'PR title to validate' required: true outputs: valid: description: 'Whether the title is valid' value: ${{ steps.validate.outputs.valid }} runs: using: 'composite' steps: - name: Validate PR title id: validate shell: bash run: | title="${{ inputs.title }}" # Define allowed types allowed_types="feat|fix|docs|style|refactor|perf|test|build|ci|chore|deps" # Check if title matches conventional commit format if echo "$title" | grep -qE "^($allowed_types)(\(.+\))?!?: .+"; then echo "valid=true" >> $GITHUB_OUTPUT echo "✅ PR title is valid: $title" else echo "valid=false" >> $GITHUB_OUTPUT echo "❌ PR title is invalid: $title" echo "" echo "Expected format: [optional scope]: " echo "" echo "Allowed types: $allowed_types" echo "" echo "Examples:" echo " feat: add new feature" echo " fix: resolve bug" echo " deps: update dependencies" echo " chore: update CI configuration" exit 1 fi ================================================ FILE: .github/dependabot.yml ================================================ # To get started with Dependabot version updates, you'll need to specify which # package ecosystems to update and where the package manifests are located. # Please see the documentation for all configuration options: # https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file version: 2 updates: - package-ecosystem: 'npm' # See documentation for possible values directory: '/' # Location of package manifests schedule: interval: 'weekly' commit-message: prefix: 'chore' include: 'scope' - package-ecosystem: 'github-actions' # Workflow files stored in the default location of `.github/workflows`. (You don't need to specify `/.github/workflows` for `directory`. You can use `directory: "/"`.) directory: '/' schedule: interval: 'weekly' commit-message: prefix: 'chore' include: 'scope' ================================================ FILE: .github/workflows/ci.yml ================================================ # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions name: CI on: push: branches: [main] pull_request: branches: [main] env: REGISTRY: ghcr.io jobs: lint: name: Run linter runs-on: ubuntu-latest steps: - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # @v6.0.0 - name: Use Node.js 22.x uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # @v6.0.0 with: node-version: 22.x - run: npm install - name: Lint Code run: npm run lint - name: Validate OpenAPI Spec run: npm run validate-swagger unit: name: Run tests runs-on: ubuntu-latest steps: - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # @v6.0.0 - name: Use Node.js 22.x uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # @v6.0.0 with: node-version: 22.x - run: npm install - run: npm run test:unit integration: name: Run Integration tests runs-on: ubuntu-latest steps: - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # @v6.0.0 - run: npm run test:integration:local ================================================ FILE: .github/workflows/codeql-analysis.yml ================================================ # For most projects, this workflow file will not need changing; you simply need # to commit it to your repository. # # You may wish to alter this file to override the set of languages analyzed, # or to provide custom queries or build logic. # # ******** NOTE ******** # We have attempted to detect the languages in your repository. Please check # the `language` matrix defined below to confirm you have the correct set of # supported CodeQL languages. # name: 'CodeQL' on: push: branches: [main] pull_request: # The branches below must be a subset of the branches above branches: [main] schedule: - cron: '15 6 * * 5' jobs: analyze: name: Analyze runs-on: ubuntu-latest strategy: fail-fast: false matrix: language: ['javascript'] # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] # Learn more: # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed steps: - name: Checkout repository uses: actions/checkout@v6 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL uses: github/codeql-action/init@v4 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. # By default, queries listed here will override any specified in a config file. # Prefix the list here with "+" to use these queries and those in the config file. # queries: ./path/to/local/query, your-org/your-repo/queries@main # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild uses: github/codeql-action/autobuild@v4 # ℹ️ Command-line programs to run using the OS shell. # 📚 https://git.io/JvXDl # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines # and modify them (or add more) to build your code if your project # uses a compiled language #- run: | # make bootstrap # make release - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@v4 ================================================ FILE: .github/workflows/lint-pr.yml ================================================ name: "Lint PR" on: pull_request_target: types: - opened - edited - synchronize jobs: main: name: Validate PR title runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v6 - name: Validate PR title uses: ./.github/actions/validate-pr-title with: title: ${{ github.event.pull_request.title }} ================================================ FILE: .github/workflows/release-please.yml ================================================ name: Release Please on: push: branches: [main] workflow_dispatch: permissions: contents: write pull-requests: write jobs: release-please: runs-on: ubuntu-latest outputs: release_created: ${{ steps.release.outputs.release_created }} version: ${{ steps.release.outputs.version }} steps: - name: Generate Deploy Bot token id: generate-token uses: actions/create-github-app-token@v3 with: app-id: ${{ secrets.DEPLOYMENT_APP_ID }} private-key: ${{ secrets.DEPLOYMENT_APP_PRIVATE_KEY }} - name: Checkout uses: actions/checkout@v6 with: fetch-depth: 0 - name: Release Please id: release uses: googleapis/release-please-action@v4 with: token: ${{ steps.generate-token.outputs.token }} release-type: node config-file: ./release-please-config.json ================================================ FILE: .github/workflows/release.yml ================================================ name: Release on: release: types: [published] workflow_dispatch: repository_dispatch: env: REGISTRY: ghcr.io jobs: build-and-publish: name: Build and Publish Release Assets runs-on: ubuntu-latest if: github.repository == '5e-bits/5e-srd-api' permissions: contents: write packages: write steps: - name: Determine Release Tag id: tag run: | if [ "${{ github.event_name }}" = "release" ]; then echo "tag=${{ github.event.release.tag_name }}" >> $GITHUB_OUTPUT else # For workflow_dispatch, get the latest release tag LATEST_TAG=$(gh api repos/${{ github.repository }}/releases/latest --jq .tag_name) echo "tag=$LATEST_TAG" >> $GITHUB_OUTPUT fi env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Checkout uses: actions/checkout@v6 with: ref: ${{ steps.tag.outputs.tag }} - name: Use Node.js 22.x uses: actions/setup-node@v6 with: node-version: 22.x - name: Install Dependencies run: npm ci - name: Build Artifacts run: | npm run bundle-swagger npm run gen-postman - name: Upload Release Assets if: github.event_name == 'release' run: | gh release upload ${{ github.event.release.tag_name }} \ ./dist/openapi.yml \ ./dist/openapi.json \ ./dist/collection.postman.json env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} container-release: name: Container Release runs-on: ubuntu-latest if: github.repository == '5e-bits/5e-srd-api' env: IMAGE_NAME: ${{ github.repository }} permissions: contents: read packages: write steps: - name: Determine Release Tag id: tag run: | if [ "${{ github.event_name }}" = "release" ]; then echo "tag=${{ github.event.release.tag_name }}" >> $GITHUB_OUTPUT else # For workflow_dispatch, get the latest release tag LATEST_TAG=$(gh api repos/${{ github.repository }}/releases/latest --jq .tag_name) echo "tag=$LATEST_TAG" >> $GITHUB_OUTPUT fi env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Checkout repository uses: actions/checkout@v6 with: ref: ${{ steps.tag.outputs.tag }} - name: Log in to the Container registry uses: docker/login-action@v4 with: registry: ${{ env.REGISTRY }} username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - name: Build and push Docker image uses: docker/build-push-action@v7 with: context: . push: true tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.tag.outputs.tag }},${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest labels: version=${{ steps.tag.outputs.tag }} heroku-deploy: name: Deploy to Heroku runs-on: ubuntu-latest if: github.repository == '5e-bits/5e-srd-api' needs: [build-and-publish, container-release] steps: - name: Determine Release Tag id: tag run: | if [ "${{ github.event_name }}" = "release" ]; then echo "tag=${{ github.event.release.tag_name }}" >> $GITHUB_OUTPUT else # For workflow_dispatch, get the latest release tag LATEST_TAG=$(gh api repos/${{ github.repository }}/releases/latest --jq .tag_name) echo "tag=$LATEST_TAG" >> $GITHUB_OUTPUT fi env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Checkout uses: actions/checkout@v6 with: ref: ${{ steps.tag.outputs.tag }} - name: Install Heroku CLI run: curl https://cli-assets.heroku.com/install.sh | sh - name: Deploy to Heroku uses: akhileshns/heroku-deploy@v3.14.15 with: heroku_api_key: ${{ secrets.HEROKU_API_KEY }} heroku_app_name: "dnd-5e-srd-api" heroku_email: "cdurianward@gmail.com" ================================================ FILE: .gitignore ================================================ # Logs logs *.log npm-debug.log* # Runtime data pids *.pid *.seed # Directory for instrumented libs generated by jscoverage/JSCover lib-cov # Coverage directory used by tools like istanbul coverage # nyc test coverage .nyc_output # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) .grunt # node-waf configuration .lock-wscript # Compiled binary addons (http://nodejs.org/api/addons.html) build/Release # Dependency directories node_modules jspm_packages # Optional npm cache directory .npm # Optional REPL history .node_repl_history .env # JetBrains config files .idea dist dist/** # Portman creates some temporary files here during conversion. tmp/** collection.postman.json # Docker docker-compose.override.yml # Claude .claude ================================================ FILE: .nvmrc ================================================ lts/jod ================================================ FILE: .prettierrc ================================================ { "printWidth": 100, "singleQuote": true, "trailingComma": "none", "tabWidth": 2, "semi": false, "quoteProps": "as-needed" } ================================================ FILE: .redocly.yaml ================================================ extends: - recommended rules: operation-operationId: off operation-4xx-response: off # no-invalid-media-type-examples: off ================================================ FILE: CHANGELOG.md ================================================ # [5.1.0](https://github.com/5e-bits/5e-srd-api/compare/v5.0.0...v5.1.0) (2025-09-15) ### Features * **2024:** Add equipment and categories ([#815](https://github.com/5e-bits/5e-srd-api/issues/815)) ([81dae46](https://github.com/5e-bits/5e-srd-api/commit/81dae461faa031b93d9c0edf86a458f7c1f3f2c6)) # [5.0.0](https://github.com/5e-bits/5e-srd-api/compare/v4.2.1...v5.0.0) (2025-09-04) * refactor(race/subrace)!: remove redundant data ([#825](https://github.com/5e-bits/5e-srd-api/issues/825)) ([043fc16](https://github.com/5e-bits/5e-srd-api/commit/043fc160c5f6d41d88a18d8cac11f66f4e2a55d9)) ### BREAKING CHANGES * dropped the `race.starting_proficiencies`, `race.starting_proficiency_options`, `subrace.starting_proficiencies`, `subrace.language_options`, and `subrace.languages` properties of all races and subraces in the database. Clients can instead find this data on the corresponding traits linked to each race or subrace. ## How was it tested? I ran the database + API project locally with Docker and called the endpoints of the various classes and subclasses. I also ran the unit and integration tests in the API project. ## Is there a Github issue this is resolving? https://github.com/5e-bits/5e-database/issues/874 ## Was any impacted documentation updated to reflect this change? I touched every reference of the properties in the API project. I took a look at the docs project, but couldn't fully find my way around the project to give a clear indication on if anything needed to change. ## Here's a fun image for your troubles My players once sold an iron pot to well known business woman and secret member of the Zhentarim and convinced her that it was a magic pot that can restore spoiled food. They even sneaked into her house to cast purify food and drink on it to make sure she believed them. ![Iron Pot](https://github.com/user-attachments/assets/506e3b32-4093-42fd-8fa0-f8fd95bb85cb) ## [5.4.0](https://github.com/5e-bits/5e-srd-api/compare/v5.3.0...v5.4.0) (2026-04-01) ### Features * **2024:** Add magic items ([#1044](https://github.com/5e-bits/5e-srd-api/issues/1044)) ([8686fd0](https://github.com/5e-bits/5e-srd-api/commit/8686fd0ca89358cd3a256353985ec73fd9ab5d4f)) * **2024:** Add Species, Subspecies, and Traits ([#1038](https://github.com/5e-bits/5e-srd-api/issues/1038)) ([0bcde32](https://github.com/5e-bits/5e-srd-api/commit/0bcde324e7228b85fd40d324aec0d865287a4872)) * **2024:** Add subclasses ([#1043](https://github.com/5e-bits/5e-srd-api/issues/1043)) ([29ae4fa](https://github.com/5e-bits/5e-srd-api/commit/29ae4fa8f4efae0eb29bc9d6523e5c48839fa0c7)) ## [5.3.0](https://github.com/5e-bits/5e-srd-api/compare/v5.2.4...v5.3.0) (2026-03-10) ### Features * **2024:** Add Backgrounds, Feats, and Proficiencies for 2024 ([#1018](https://github.com/5e-bits/5e-srd-api/issues/1018)) ([b584a84](https://github.com/5e-bits/5e-srd-api/commit/b584a8428ab079b7e889b54f28eeb33a0347cea5)) ## [5.2.4](https://github.com/5e-bits/5e-srd-api/compare/v5.2.3...v5.2.4) (2025-12-11) ### Bug Fixes * **ci:** Use app token ([#956](https://github.com/5e-bits/5e-srd-api/issues/956)) ([5abffd8](https://github.com/5e-bits/5e-srd-api/commit/5abffd830dd00dbd5bb55dd96c1627d002fed95d)) ## [5.2.3](https://github.com/5e-bits/5e-srd-api/compare/v5.2.2...v5.2.3) (2025-12-11) ### Bug Fixes * **ci:** Release is now automatic on release PR merge ([#954](https://github.com/5e-bits/5e-srd-api/issues/954)) ([d8a798d](https://github.com/5e-bits/5e-srd-api/commit/d8a798d3bb3552c6ba5a3248cda80923013ac6b9)) ## [5.2.2](https://github.com/5e-bits/5e-srd-api/compare/v5.2.1...v5.2.2) (2025-12-04) ### Bug Fixes * Add Spell field resolver to Subclass Resolver ([#952](https://github.com/5e-bits/5e-srd-api/issues/952)) ([c15ef5d](https://github.com/5e-bits/5e-srd-api/commit/c15ef5da7fa3271cd25d67908d7f16d064930113)) ## [5.2.1](https://github.com/5e-bits/5e-srd-api/compare/v5.2.0...v5.2.1) (2025-12-01) ### Bug Fixes * missing subclass features ([#946](https://github.com/5e-bits/5e-srd-api/issues/946)) ([8170142](https://github.com/5e-bits/5e-srd-api/commit/8170142dfd9cea306773d1942e21aab52d5901d6)) ## [5.2.0](https://github.com/5e-bits/5e-srd-api/compare/v5.1.0...v5.2.0) (2025-10-24) ### Features * **release:** Use release please ([#911](https://github.com/5e-bits/5e-srd-api/issues/911)) ([a8b50dd](https://github.com/5e-bits/5e-srd-api/commit/a8b50dd9256ecf0e5be8517625be839be6e6976e)) ### Bug Fixes * **dependabot:** use build instead of deps ([#920](https://github.com/5e-bits/5e-srd-api/issues/920)) ([52a439d](https://github.com/5e-bits/5e-srd-api/commit/52a439da3855b62291007ff37bfd99650e37c7cb)) ## [4.2.1](https://github.com/5e-bits/5e-srd-api/compare/v4.2.0...v4.2.1) (2025-06-23) ### Bug Fixes * **races:** Language options is optional for graphql ([#807](https://github.com/5e-bits/5e-srd-api/issues/807)) ([2715e0f](https://github.com/5e-bits/5e-srd-api/commit/2715e0f457cc1404b870e4284b53e2ca227a8355)) # [4.2.0](https://github.com/5e-bits/5e-srd-api/compare/v4.1.1...v4.2.0) (2025-06-13) ### Features * **2024:** Add a bunch of easy endpoints to 2024 ([#800](https://github.com/5e-bits/5e-srd-api/issues/800)) ([2b4871d](https://github.com/5e-bits/5e-srd-api/commit/2b4871da07bea5f8e9d9e907e37e8c4124254cea)) ## [4.1.1](https://github.com/5e-bits/5e-srd-api/compare/v4.1.0...v4.1.1) (2025-06-11) ### Bug Fixes * **graphql:** Spell DC now resolves to Ability Score ([#798](https://github.com/5e-bits/5e-srd-api/issues/798)) ([ddb5c26](https://github.com/5e-bits/5e-srd-api/commit/ddb5c26e7dc1794534bd364bcfa20386793dd35d)) # [4.1.0](https://github.com/5e-bits/5e-srd-api/compare/v4.0.3...v4.1.0) (2025-06-11) ### Features * **graphql:** Sets up /graphql/2024 endpoint and abstracts shared code ([#790](https://github.com/5e-bits/5e-srd-api/issues/790)) ([acf7780](https://github.com/5e-bits/5e-srd-api/commit/acf7780e301cf1fb2a166eeefd177a8f4225af3b)) ## [4.0.3](https://github.com/5e-bits/5e-srd-api/compare/v4.0.2...v4.0.3) (2025-06-05) ### Bug Fixes * **graphql:** Fix the endpoint for deprecated graphql endpoint ([#791](https://github.com/5e-bits/5e-srd-api/issues/791)) ([2139389](https://github.com/5e-bits/5e-srd-api/commit/2139389d91bfb264acb1929e36a3ebd9ea5b19c2)) ## [4.0.2](https://github.com/5e-bits/5e-srd-api/compare/v4.0.1...v4.0.2) (2025-06-02) ### Bug Fixes * **prettier:** Run prettier against everything ([#780](https://github.com/5e-bits/5e-srd-api/issues/780)) ([01905b2](https://github.com/5e-bits/5e-srd-api/commit/01905b2be462990966e7790b2897aebb1cbe578a)) ## [4.0.1](https://github.com/5e-bits/5e-srd-api/compare/v4.0.0...v4.0.1) (2025-06-02) ### Bug Fixes * **lint:** Fix simple linting rules ([#778](https://github.com/5e-bits/5e-srd-api/issues/778)) ([3c6cc95](https://github.com/5e-bits/5e-srd-api/commit/3c6cc95753f3f485a58f2e2dfe93cebd1de614d2)) # [4.0.0](https://github.com/5e-bits/5e-srd-api/compare/v3.24.0...v4.0.0) (2025-06-01) * feat(graphql)!: Migrate to typegraphql ([#752](https://github.com/5e-bits/5e-srd-api/issues/752)) ([6bb9e75](https://github.com/5e-bits/5e-srd-api/commit/6bb9e755ea0e35aebe8f2f3bdae0362fa77694e6)) ### BREAKING CHANGES * Some fields are now different but more consistent across the graphql endpoints. ## What does this do? * Completely rewrites our entire GraphQL endpoint using `type-graphql` and `zod` * Fixes migration errors from the typegoose migration ## How was it tested? Locally, there are numerous side-by-side comparisons with production. But it is possible I missed something. ## Is there a Github issue this is resolving? Nope. I'm just insane. ## Was any impacted documentation updated to reflect this change? Luckily, GraphQL introspection is self-documenting. ## Here's a fun image for your troubles ![image](https://github.com/user-attachments/assets/22bfa110-aeac-4625-99c6-d57bbc00c4d1) # [3.24.0](https://github.com/5e-bits/5e-srd-api/compare/v3.23.7...v3.24.0) (2025-05-30) ### Features * **class:** Add `level` query for class spells for filtering spell level ([#776](https://github.com/5e-bits/5e-srd-api/issues/776)) ([0c41457](https://github.com/5e-bits/5e-srd-api/commit/0c414571ea409c37c15d1b82d1da86844cf18ba9)) ## [3.23.7](https://github.com/5e-bits/5e-srd-api/compare/v3.23.6...v3.23.7) (2025-05-08) ### Bug Fixes * **deploy:** Undo everything from before ([c3c84d1](https://github.com/5e-bits/5e-srd-api/commit/c3c84d1b20ce930ed1afb558c26955a049ca9560)) ## [3.23.6](https://github.com/5e-bits/5e-srd-api/compare/v3.23.5...v3.23.6) (2025-05-08) ### Bug Fixes * **deploy:** Let's try this one more time ([5c5c1cf](https://github.com/5e-bits/5e-srd-api/commit/5c5c1cfa774826837114014e7dbdf9e150e865d3)) ## [3.23.5](https://github.com/5e-bits/5e-srd-api/compare/v3.23.4...v3.23.5) (2025-05-08) ### Bug Fixes * **deploy:** Let's try that again. Now using deploy bot as author ([3f5dfa0](https://github.com/5e-bits/5e-srd-api/commit/3f5dfa04c60144c82086829039edb73a8bc9055e)) ## [3.23.4](https://github.com/5e-bits/5e-srd-api/compare/v3.23.3...v3.23.4) (2025-05-08) ### Bug Fixes * **deploy:** Use deploy bot for authoring commits ([1f62894](https://github.com/5e-bits/5e-srd-api/commit/1f628949cd218d5939dd8b6f68c702f2355761ba)) ## [3.23.3](https://github.com/5e-bits/5e-srd-api/compare/v3.23.2...v3.23.3) (2025-05-06) ### Bug Fixes * **class:** showSpellsForClassAndLevel now gives the spells available at that class level ([#758](https://github.com/5e-bits/5e-srd-api/issues/758)) ([22b1b35](https://github.com/5e-bits/5e-srd-api/commit/22b1b351e7355bcdde04c5fd2fee0c967fb4f2f5)) ## [3.23.2](https://github.com/5e-bits/5e-srd-api/compare/v3.23.1...v3.23.2) (2025-05-04) ### Bug Fixes * **desc:** Fix models to match data reality for desc ([#751](https://github.com/5e-bits/5e-srd-api/issues/751)) ([6bcd610](https://github.com/5e-bits/5e-srd-api/commit/6bcd610df4cccc57d79e8ff3b075fee356c34f04)) ## [3.23.1](https://github.com/5e-bits/5e-srd-api/compare/v3.23.0...v3.23.1) (2025-05-04) ### Bug Fixes * **images:** Fix key for regex ([6f8a99d](https://github.com/5e-bits/5e-srd-api/commit/6f8a99da050ff4ce0c57bfc5377fcdf57b42f60c)) # [3.23.0](https://github.com/5e-bits/5e-srd-api/compare/v3.22.0...v3.23.0) (2025-05-04) ### Features * **images:** Add /api/images endpoint and use fetch for image fetching ([#750](https://github.com/5e-bits/5e-srd-api/issues/750)) ([fec63b7](https://github.com/5e-bits/5e-srd-api/commit/fec63b78ac075ca8c093896f6bd6c8519ac9870b)) # [3.22.0](https://github.com/5e-bits/5e-srd-api/compare/v3.21.0...v3.22.0) (2025-04-28) ### Features * **node:** Bump to Node 22 ([#742](https://github.com/5e-bits/5e-srd-api/issues/742)) ([8c27177](https://github.com/5e-bits/5e-srd-api/commit/8c271775661473764295d71bc681a31bda6dd01c)) # [3.21.0](https://github.com/5e-bits/5e-srd-api/compare/v3.20.1...v3.21.0) (2025-04-27) ### Features * **release:** Convert semver release to use App instead of PAT ([#736](https://github.com/5e-bits/5e-srd-api/issues/736)) ([aad1a29](https://github.com/5e-bits/5e-srd-api/commit/aad1a29c459bed41daa73af09c4d64db9dcab770)) ## [3.20.1](https://github.com/5e-bits/5e-srd-api/compare/v3.20.0...v3.20.1) (2025-04-27) ### Bug Fixes * :bug: Resolve full `Option` objects in `subrace.language_options` resolver ([#735](https://github.com/5e-bits/5e-srd-api/issues/735)) ([ef37127](https://github.com/5e-bits/5e-srd-api/commit/ef37127a68c0303c51d62d21835baa595b742435)) # [3.20.0](https://github.com/5e-bits/5e-srd-api/compare/v3.19.0...v3.20.0) (2025-04-25) ### Bug Fixes * **release:** Give workflow write permissions ([6c50c47](https://github.com/5e-bits/5e-srd-api/commit/6c50c47f0a599967c8e0ac6cea271452cbf696f9)) * **release:** Set token on checkout ([158dc35](https://github.com/5e-bits/5e-srd-api/commit/158dc35bde5efb687e7c937c038e7ba54e9bc352)) * **release:** Use PAT instead of normal GITHUB_TOKEN ([d518a8d](https://github.com/5e-bits/5e-srd-api/commit/d518a8d8eb1cca5ea399fb3d3ce97c7bd0fba618)) ### Features * **semver:** Updates changelog and npm version on release ([#733](https://github.com/5e-bits/5e-srd-api/issues/733)) ([40f60f3](https://github.com/5e-bits/5e-srd-api/commit/40f60f39ea1ed10d1b2b27f7e53c3d0fe6a7a9be)) # Changelog ## 2020-01-12 - Make GET queries case insensitive for `name` where supported. - Fix Home link to work when you're on the Docs page ## 2020-01-11 - 100% Test coverage between unit and integration tests - Overloaded routes will be removed and moved onto routes that make sense. - General cleanup of the code base and breakup to make testing easier. ## 2020-01-09 - Add in Docker Compose ## 2020-01-08 - Add Prettier for auto formatting - Add in 404 support to stop timeouts - Add Heroku badge - Add in Jest testing framework - Add Bugsnag for error reporting ## 2020-01-06 - Update current docs to match new database changes ## 2020-01-05 - Fix race and subrace routes (#33) ## 2020-01-04 - Converted number indices to string indices based off of name - Added a Contribute link for the API - Move changes to Changelog - Fixed the navbar for the Docs to use the same partial ## 2020-01-02 - All of the database changes made in the last few months have finally landed - Replaced Slack chat with Discord - Added some HTTPS support ## 2018-01-07 - The Database and API are now OPEN SOURCE! Find it on my github - Updated changes from DevinOchman's pull request: New Races, Subraces, Traits ## 2017-04-17 - Created Slack chat group, which you can access here. A place to ask questions and make suggestions etc. - Updated "url" members of every object to have 'www.' to avoid CORS errors ================================================ FILE: Dockerfile ================================================ # ---- Builder Stage ---- FROM node:22-alpine AS builder WORKDIR /app COPY package.json ./ # Copy the package-lock.json that was freshly generated on your host COPY package-lock.json ./ # Clean existing node_modules just in case of Docker layer caching weirdness. # Then run `npm ci` which is generally recommended for CI/Docker if you have a package-lock.json. RUN rm -rf node_modules RUN npm ci # Copy the rest of the application source code # .dockerignore will handle exclusions like node_modules, dist, etc. COPY . . # Build the application # This uses tsconfig.json to output to ./dist RUN npm run build # ---- Final Stage ---- FROM node:22-alpine WORKDIR /app # Copy package.json and lock file (good practice) COPY package.json ./ COPY package-lock.json* ./ # Copy node_modules from builder stage - this includes all dependencies with scripts run COPY --from=builder /app/node_modules ./node_modules/ # Set environment to production AFTER dependencies are in place ENV NODE_ENV=production # Copy built application from builder stage COPY --from=builder /app/dist ./dist/ # Copy entire source tree (needed for tests and potentially other runtime file access) COPY --from=builder /app/src ./src/ # Copy config files needed for tests/runtime COPY --from=builder /app/vitest.config*.ts ./ COPY --from=builder /app/tsconfig.json ./ # # Add non-root user for security # RUN addgroup -S appgroup && adduser -S appuser -G appgroup # # Change ownership of app directory to the non-root user AFTER user creation # RUN chown -R appuser:appgroup /app # USER appuser # Expose port (replace 3000 if different) EXPOSE 3000 # Start the main process. CMD ["node", "--experimental-specifier-resolution=node", "dist/src/start.js"] ================================================ FILE: LICENSE.md ================================================ MIT License Copyright (c) [2018-2020] [Adrian Padua, Christopher Ward] Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # 5e-srd-api [![GitHub Actions Workflow Status](https://img.shields.io/github/actions/workflow/status/5e-bits/5e-srd-api/ci.yml?style=flat&logo=github&logoColor=white)](https://github.com/5e-bits/5e-srd-api/actions/workflows/ci.yml) [![Discord](https://img.shields.io/discord/656547667601653787?style=flat&logo=discord&logoColor=white)](https://discord.gg/TQuYTv7) ![Uptime](https://img.shields.io/endpoint?url=https%3A%2F%2Fraw.githubusercontent.com%2F5e-bits%2Fdnd-uptime%2Fmain%2Fapi%2Fwebsite%2Fresponse-time.json) ![Uptime](https://img.shields.io/endpoint?url=https%3A%2F%2Fraw.githubusercontent.com%2F5e-bits%2Fdnd-uptime%2Fmain%2Fapi%2Fwebsite%2Fuptime.json) REST API to access [D&D 5th Edition SRD API](https://www.dnd5eapi.co/) Talk to us [on Discord!](https://discord.gg/TQuYTv7) ## How to Run Make sure you have the latest version of the database: ```shell docker compose pull ``` Then run it with docker-compose: ```shell docker compose up --build ``` ### M1/M2/M3 Macs The command above pulls the latest image of the database from ghcr.io, which only targets the amd64 platform. If you are running on a different platform (like a Mac with Apple Silicon), you will need to build the image yourself. See the [5e-database](https://github.com/5e-bits/5e-database#how-to-run) repo for additional details. ```shell cd ../ git clone https://github.com/5e-bits/5e-database.git ``` Then back over here in the 5e-srd-api repo, in the file `docker-compose.yml`, you can replace the line `image: bagelbits/5e-database` with `build: ../5e-database` (or whatever you named the custom db image). ## Making API Requests Make API requests by using the root address: `http://localhost:3000/api/2014` You should get a response with the available endpoints for the root: ```json { "ability-scores": "/api/2014/ability-scores", "classes": "/api/2014/classes", "conditions": "/api/2014/conditions", "damage-types": "/api/2014/damage-types", "equipment-categories": "/api/2014/equipment-categories", "equipment": "/api/2014/equipment", "features": "/api/2014/features", "languages": "/api/2014/languages", "magic-schools": "/api/2014/magic-schools", "monsters": "/api/2014/monsters", "proficiencies": "/api/2014/proficiencies", "races": "/api/2014/races", "skills": "/api/2014/skills", "spells": "/api/2014/spells", "subclasses": "/api/2014/subclasses", "subraces": "/api/2014/subraces", "traits": "/api/2014/traits", "weapon-properties": "/api/2014/weapon-properties" } ``` ### Versioning The API is versioned by release years of the SRD. Currently only `/api/2014` is available. The next version will be `/api/2024`. ## Working with a local image of 5e Database If you are working on a feature which requires changes to both this repo, _and_ the 5e-database repo, it is useful to know how to connect the former to the latter for testing purposes. A simple process for doing so is as follows: 1. In the file `docker-compose.yml`, you can replace the line `image: bagelbits/5e-database` with `build: [relativePathToDatabaseRepo]`. Make sure not to commit this change, as it is intended for local testing only. 2. Run your branch of 5e-srd-api using the method outlined in the above section of this readme file. So long as there are no transient errors, the API should build successfully, and your changes to both repos should be noticeable. ## Working with image resources from s3 The API uses s3 to store image files for monsters. The image files live in a bucket called `dnd-5e-api-images` under the `/monsters` folder. To test locally, you can [use `localstack` to mock s3](https://docs.localstack.cloud/user-guide/aws/s3/). To do so, you will first need to install `localstack`, `awscli`, and `awslocal`. You can then run the following commands to configure and start the localstack container: ```shell export AWS_CONFIG_ENV=localstack_dev localstack start awslocal s3api create-bucket --bucket dnd-5e-api-images awslocal s3 cp aboleth.png s3://dnd-5e-api-images/monsters/ npm run dev ``` Request the image by navigating to an image URL in a browser, or via HTTP request: ```shell curl http://localhost:3000/api/2014/monsters/aboleth.png --output downloaded-aboleth.png ``` When interacting with the image you should see logs in the terminal where you started localstack. You can also use [localstack's webui](https://app.localstack.cloud/dashboard) to view the bucket and contents. ## Data Issues If you see anything wrong with the data itself, please open an issue or PR over [here.](https://github.com/5e-bits/5e-database/) ## Running Tests ### Unit Tests You can run unit tests locally by using the command: `npm run test:unit` ### Integration Tests Integration tests need to be ran in the API docker container for them to function properly. In order to run integration tests locally you can use the command: `npm run test:integration:local` ## Documentation Public facing API documentation lives [here.](https://www.dnd5eapi.co/docs) The [docs repository](https://github.com/5e-bits/docs) contains the source for the public facing API documentation. It uses [Docusaurus](https://docusaurus.io/) to generate the site from a bundled OpenAPI spec. More details on working with the OpenAPI spec can be found in the [`src/swagger`](src/swagger/) directory's [README](src/swagger/README.md). The most up-to-date bundled OpenAPI specs themselves are included in [the latest release](https://github.com/5e-bits/5e-srd-api/releases/latest) in both [JSON](https://github.com/5e-bits/5e-srd-api/releases/latest/download/openapi.json) and [YAML](https://github.com/5e-bits/5e-srd-api/releases/latest/download/openapi.yml) formats, which can be used to generate your own documentation, clients, etc. A [Postman collection](https://github.com/5e-bits/5e-srd-api/releases/latest/download/collection.postman.json) can also be found in the latest release. This can be imported into [the Postman HTTP client](https://www.postman.com/) to execute test requests against production & local deployments of the API. ## Contributing - Fork this repository - Create a new branch for your work - Push up any changes to your branch, and open a pull request. Don't feel it needs to be perfect — incomplete work is totally fine. We'd love to help get it ready for merging. ## Code of Conduct The Code of Conduct for this repo can be found [here.](https://github.com/5e-bits/5e-srd-api/wiki#code-of-conduct) ## Contributors ================================================ FILE: app.json ================================================ { "name": "5e-srd-api", "scripts": {}, "env": { "AWS_ACCESS_KEY_ID": { "required": true }, "AWS_SECRET_ACCESS_KEY": { "required": true }, "BUGSNAG_API_KEY": { "required": true }, "GRAPHQL_API_KEY": { "required": true }, "MONGODB_URI": { "required": true }, "NODE_ENV": { "required": true }, "REALM_API_KEY": { "required": true }, "REALM_APP_ID": { "required": true } }, "formation": { "web": { "quantity": 1 } }, "addons": ["bugsnag:tauron", "heroku-redis:mini"], "buildpacks": [ { "url": "heroku/nodejs" } ], "stack": "container" } ================================================ FILE: docker-compose.yml ================================================ services: db: image: ghcr.io/5e-bits/5e-database:latest # build: ../5e-database ports: - '27017:27017' cache: image: redis:6.2.5 ports: - '6379:6379' api: environment: MONGODB_URI: mongodb://db/5e-database REDIS_URL: redis://cache:6379 build: . ports: - '3000:3000' depends_on: - db - cache ================================================ FILE: eslint.config.js ================================================ import eslint from '@eslint/js' import stylistic from '@stylistic/eslint-plugin' import eslintConfigPrettier from 'eslint-config-prettier' import { createTypeScriptImportResolver } from 'eslint-import-resolver-typescript' import { importX } from 'eslint-plugin-import-x' import globals from 'globals' import tseslint from 'typescript-eslint' export default tseslint.config( { name: 'base-ignore', ignores: ['**/coverage/**', '**/dist/**', '**/node_modules/**'] }, eslint.configs.recommended, // Main TypeScript and JS linting configuration { name: 'typescript-and-imports-x', files: ['**/*.ts', '**/*.js'], extends: [ ...tseslint.configs.recommended, importX.flatConfigs.recommended, importX.flatConfigs.typescript ], plugins: { '@stylistic': stylistic, 'import-x': importX }, languageOptions: { parserOptions: { project: true, tsconfigRootDir: import.meta.dirname }, globals: { ...globals.node, ...globals.browser, ...globals.jquery } }, rules: { '@typescript-eslint/no-explicit-any': 'off', '@typescript-eslint/strict-boolean-expressions': 'error', 'import-x/order': [ 'error', { groups: [ 'builtin', 'external', 'internal', ['parent', 'sibling', 'index'], 'object', 'type' ], 'newlines-between': 'always', alphabetize: { order: 'asc', caseInsensitive: true }, pathGroups: [{ pattern: '@/**', group: 'internal', position: 'after' }], pathGroupsExcludedImportTypes: ['builtin', 'external', 'object', 'type'] } ], 'import-x/no-duplicates': 'error', 'import-x/no-named-as-default': 'off', 'import-x/no-named-as-default-member': 'off' }, settings: { 'import-x/resolver-next': [ createTypeScriptImportResolver({ alwaysTryTypes: true, project: './tsconfig.json' }) ] } }, eslintConfigPrettier // MUST BE LAST ) ================================================ FILE: heroku.yml ================================================ build: docker: web: Dockerfile run: web: node dist/src/start.js ================================================ FILE: nodemon.json ================================================ { "verbose": false, "ignore": [ "*.test.*", "dist/*", "node_modules/*", "collection.postman.json", "tmp/*" ], "ext": "js,ts,ejs,json,yml" } ================================================ FILE: openapi-to-postman.json ================================================ { "folderStrategy": "Tags", "includeAuthInfoInExample": true, "optimizeConversion": true, "stackLimit": 50, "requestParametersResolution": "Example", "exampleParametersResolution": "Example" } ================================================ FILE: package.json ================================================ { "name": "dnd-5e-srd-api", "version": "5.4.0", "engines": { "node": "22.x", "npm": ">=10.8.0" }, "type": "module", "private": true, "scripts": { "build:ts": "tsc && tsc-alias", "build": "npm-run-all --sequential clean bundle-swagger build:ts copy-assets", "clean": "rimraf dist/*", "copy-assets": "mkdir -p dist && cp -R src/css dist/src && cp -R src/public dist/src", "start": "npm run build && node dist/src/start.js", "dev": "nodemon npm run start", "lint": "eslint . --config eslint.config.js", "lint:fix": "eslint . --config eslint.config.js --fix", "validate-swagger": "redocly lint src/swagger/swagger.yml", "bundle-swagger": "npm run validate-swagger && redocly bundle --output dist/openapi.yml src/swagger/swagger.yml && redocly bundle --output dist/openapi.json src/swagger/swagger.yml", "gen-postman": "npm run bundle-swagger && openapi2postmanv2 -s dist/openapi.yml -o dist/collection.postman.json -c openapi-to-postman.json", "test": "npm run test:unit && npm run test:integration:local", "test:unit": "vitest run", "test:integration": "vitest run --config ./vitest.config.integration.ts", "test:integration:local": "docker compose pull && docker compose build && docker compose run --use-aliases api npm run test:integration" }, "dependencies": { "@apollo/server": "^5.5.0", "@as-integrations/express5": "^1.1.2", "@aws-sdk/client-s3": "^3.994.0", "@bugsnag/js": "^8.8.1", "@bugsnag/plugin-express": "^8.8.0", "@graphql-tools/schema": "^10.0.31", "@redocly/cli": "^2.25.4", "@typegoose/typegoose": "^13.2.1", "body-parser": "^2.2.2", "class-validator": "^0.15.1", "cookie-parser": "^1.4.7", "cors": "^2.8.6", "debug": "^4.4.3", "ejs": "^5.0.2", "escape-string-regexp": "^5.0.0", "express": "^5.1.0", "express-rate-limit": "^8.2.2", "graphql": "^16.11.0", "graphql-depth-limit": "^1.1.0", "mongoose": "~9.2.3", "morgan": "^1.10.1", "node-fetch": "^3.1.1", "redis": "^5.10.0", "reflect-metadata": "^0.2.2", "serve-favicon": "^2.5.1", "type-graphql": "^2.0.0-rc.2", "zod": "^4.3.6" }, "devDependencies": { "@eslint/js": "^10.0.1", "@faker-js/faker": "^10.3.0", "@stylistic/eslint-plugin": "^5.7.1", "@types/cors": "^2.8.19", "@types/express": "^5.0.5", "@types/fs-extra": "^11.0.4", "@types/graphql-depth-limit": "^1.1.3", "@types/morgan": "^1.9.10", "@types/node": "^25.5.2", "@types/shelljs": "^0.10.0", "@types/supertest": "^7.2.0", "@typescript-eslint/eslint-plugin": "^8.57.2", "@typescript-eslint/parser": "^8.58.2", "eslint": "^10.2.0", "eslint-config-prettier": "^10.1.8", "eslint-import-resolver-typescript": "^4.4.4", "eslint-plugin-ejs": "^0.0.2", "eslint-plugin-import-x": "^4.16.1", "eslint-plugin-prettier": "^5.5.5", "fishery": "^2.3.1", "fs-extra": "^11.3.1", "globals": "^17.0.0", "mongodb-memory-server": "^11.0.1", "node-mocks-http": "^1.17.2", "nodemon": "^3.1.10", "npm-run-all": "^4.1.5", "openapi-to-postmanv2": "^5.3.5", "prettier": "^3.8.2", "rimraf": "^6.1.3", "shelljs": "^0.10.0", "supertest": "^7.2.2", "ts-node": "^10.9.2", "tsc-alias": "^1.8.16", "typedoc": "^0.28.16", "typescript": "^5.9.3", "typescript-eslint": "^8.58.0", "vitest": "^4.1.2", "yaml": "^2.8.2" } } ================================================ FILE: release-please-config.json ================================================ { "release-type": "node", "packages": { ".": { "release-type": "node", "package-name": "5e-srd-api", "changelog-sections": [ { "type": "feat", "section": "Features" }, { "type": "fix", "section": "Bug Fixes" }, { "type": "deps", "section": "Dependencies", "hidden": false } ] } } } ================================================ FILE: src/controllers/api/2014/abilityScoreController.ts ================================================ import SimpleController from '@/controllers/simpleController' import AbilityScoreModel from '@/models/2014/abilityScore' export default new SimpleController(AbilityScoreModel) ================================================ FILE: src/controllers/api/2014/alignmentController.ts ================================================ import SimpleController from '@/controllers/simpleController' import AlignmentModel from '@/models/2014/alignment' export default new SimpleController(AlignmentModel) ================================================ FILE: src/controllers/api/2014/backgroundController.ts ================================================ import SimpleController from '@/controllers/simpleController' import Background from '@/models/2014/background' export default new SimpleController(Background) ================================================ FILE: src/controllers/api/2014/classController.ts ================================================ import { NextFunction, Request, Response } from 'express' import SimpleController from '@/controllers/simpleController' import Class from '@/models/2014/class' import Feature from '@/models/2014/feature' import Level from '@/models/2014/level' import Proficiency from '@/models/2014/proficiency' import Spell from '@/models/2014/spell' import Subclass from '@/models/2014/subclass' import { ClassLevelsQuerySchema, LevelParamsSchema, ShowParamsSchema, SpellIndexQuerySchema } from '@/schemas/schemas' import { escapeRegExp, ResourceList } from '@/util' const simpleController = new SimpleController(Class) interface ShowLevelsForClassQuery { 'class.url': string $or: Record>[] } export const index = async (req: Request, res: Response, next: NextFunction) => simpleController.index(req, res, next) export const show = async (req: Request, res: Response, next: NextFunction) => simpleController.show(req, res, next) export const showLevelsForClass = async (req: Request, res: Response, next: NextFunction) => { try { // Validate path and query parameters const validatedParams = ShowParamsSchema.safeParse(req.params) const validatedQuery = ClassLevelsQuerySchema.safeParse(req.query) if (!validatedParams.success) { return res .status(400) .json({ error: 'Invalid path parameters', details: validatedParams.error.issues }) } if (!validatedQuery.success) { return res .status(400) .json({ error: 'Invalid query parameters', details: validatedQuery.error.issues }) } const { index } = validatedParams.data const { subclass } = validatedQuery.data const searchQueries: ShowLevelsForClassQuery = { 'class.url': '/api/2014/classes/' + index, $or: [{ subclass: null }] } if (subclass !== undefined) { searchQueries.$or.push({ 'subclass.url': { $regex: new RegExp(escapeRegExp(subclass), 'i') } }) } const data = await Level.find(searchQueries).sort({ level: 'asc' }) if (data !== null && data !== undefined && data.length > 0) { return res.status(200).json(data) } else { return res.status(404).json({ error: 'Not found' }) } } catch (err) { next(err) } } export const showLevelForClass = async (req: Request, res: Response, next: NextFunction) => { try { // Validate path parameters const validatedParams = LevelParamsSchema.safeParse(req.params) if (!validatedParams.success) { return res .status(400) .json({ error: 'Invalid path parameters', details: validatedParams.error.issues }) } const { index, level } = validatedParams.data const urlString = '/api/2014/classes/' + index + '/levels/' + level const data = await Level.findOne({ url: urlString }) if (!data) return next() return res.status(200).json(data) } catch (err) { next(err) } } export const showMulticlassingForClass = async ( req: Request, res: Response, next: NextFunction ) => { try { const urlString = '/api/2014/classes/' + req.params.index const data = await Class.findOne({ url: urlString }) return res.status(200).json(data?.multi_classing) } catch (err) { next(err) } } export const showSubclassesForClass = async (req: Request, res: Response, next: NextFunction) => { try { // Validate path parameters const validatedParams = ShowParamsSchema.safeParse(req.params) if (!validatedParams.success) { return res .status(400) .json({ error: 'Invalid path parameters', details: validatedParams.error.issues }) } const { index } = validatedParams.data const urlString = '/api/2014/classes/' + index const data = await Subclass.find({ 'class.url': urlString }) .select({ index: 1, name: 1, url: 1, _id: 0 }) .sort({ url: 'asc', level: 'asc' }) if (data !== null && data !== undefined && data.length > 0) { return res.status(200).json(ResourceList(data)) } else { return res.status(404).json({ error: 'Not found' }) } } catch (err) { next(err) } } export const showStartingEquipmentForClass = async ( req: Request, res: Response, next: NextFunction ) => { try { const data = await Class.findOne({ index: req.params.index }) return res.status(200).json({ starting_equipment: data?.starting_equipment, starting_equipment_options: data?.starting_equipment_options }) } catch (err) { next(err) } } export const showSpellcastingForClass = async (req: Request, res: Response, next: NextFunction) => { try { // Validate path parameters const validatedParams = ShowParamsSchema.safeParse(req.params) if (!validatedParams.success) { return res .status(400) .json({ error: 'Invalid path parameters', details: validatedParams.error.issues }) } const { index } = validatedParams.data const data = await Class.findOne({ index: index }) if ( data !== null && data !== undefined && data.spellcasting !== null && data.spellcasting !== undefined ) { return res.status(200).json(data.spellcasting) } else { return res.status(404).json({ error: 'Not found' }) } } catch (err) { next(err) } } export const showSpellsForClass = async (req: Request, res: Response, next: NextFunction) => { try { const validatedParams = ShowParamsSchema.safeParse(req.params) const validatedQuery = SpellIndexQuerySchema.safeParse(req.query) if (!validatedParams.success) { return res .status(400) .json({ error: 'Invalid path parameters', details: validatedParams.error.issues }) } if (!validatedQuery.success) { return res .status(400) .json({ error: 'Invalid query parameters', details: validatedQuery.error.issues }) } const { index } = validatedParams.data const { level } = validatedQuery.data // Check if class exists first const classExists = await Class.findOne({ index }).lean() if (!classExists) { return res.status(404).json({ error: 'Not found' }) } const urlString = '/api/2014/classes/' + index const findQuery: { 'classes.url': string; level?: { $in: number[] } } = { 'classes.url': urlString } if (level !== undefined) { findQuery.level = { $in: level.map(Number) } } const data = await Spell.find(findQuery) .select({ index: 1, level: 1, name: 1, url: 1, _id: 0 }) .sort({ level: 'asc', url: 'asc' }) return res.status(200).json(ResourceList(data)) } catch (err) { next(err) } } export const showSpellsForClassAndLevel = async ( req: Request, res: Response, next: NextFunction ) => { try { // Validate path parameters const validatedParams = LevelParamsSchema.safeParse(req.params) if (!validatedParams.success) { return res .status(400) .json({ error: 'Invalid path parameters', details: validatedParams.error.issues }) } const { index, level: classLevel } = validatedParams.data // Find the level data for the class const levelData = await Level.findOne({ 'class.index': index, level: classLevel }).lean() let maxSpellLevel = -1 // Default to -1 indicating no spellcasting ability found if (levelData?.spellcasting) { maxSpellLevel = 0 // Has spellcasting, so at least cantrips (level 0) might be available const spellcasting = levelData.spellcasting for (let i = 9; i >= 1; i--) { // Check if the spell slot exists and is greater than 0 const spellSlotKey = `spell_slots_level_${i}` as keyof typeof spellcasting if (spellcasting[spellSlotKey] != null && spellcasting[spellSlotKey]! > 0) { maxSpellLevel = i break // Found the highest level } } } if (maxSpellLevel < 0) { return res.status(200).json(ResourceList([])) } const urlString = '/api/2014/classes/' + index // Find spells for the class with level <= maxSpellLevel const spellData = await Spell.find({ 'classes.url': urlString, level: { $lte: maxSpellLevel, $gte: 0 } // Query uses maxSpellLevel }) .select({ index: 1, name: 1, url: 1, _id: 0 }) .sort({ index: 'asc' }) .lean() // Use lean for performance as we only read data return res.status(200).json(ResourceList(spellData)) } catch (err) { next(err) } } export const showFeaturesForClass = async (req: Request, res: Response, next: NextFunction) => { try { // Validate path parameters const validatedParams = ShowParamsSchema.safeParse(req.params) if (!validatedParams.success) { return res .status(400) .json({ error: 'Invalid path parameters', details: validatedParams.error.issues }) } const { index } = validatedParams.data // Check if class exists first const classExists = await Class.findOne({ index }).lean() if (!classExists) { return res.status(404).json({ error: 'Not found' }) } const urlString = '/api/2014/classes/' + index const data = await Feature.find({ 'class.url': urlString }) .select({ index: 1, name: 1, url: 1, _id: 0 }) .sort({ level: 'asc', url: 'asc' }) return res.status(200).json(ResourceList(data)) } catch (err) { next(err) } } export const showFeaturesForClassAndLevel = async ( req: Request, res: Response, next: NextFunction ) => { try { // Validate path parameters const validatedParams = LevelParamsSchema.safeParse(req.params) if (!validatedParams.success) { return res .status(400) .json({ error: 'Invalid path parameters', details: validatedParams.error.issues }) } const { index, level } = validatedParams.data const urlString = '/api/2014/classes/' + index const data = await Feature.find({ 'class.url': urlString, level }) .select({ index: 1, name: 1, url: 1, _id: 0 }) .sort({ level: 'asc', url: 'asc' }) return res.status(200).json(ResourceList(data)) } catch (err) { next(err) } } export const showProficienciesForClass = async ( req: Request, res: Response, next: NextFunction ) => { try { // Validate path parameters const validatedParams = ShowParamsSchema.safeParse(req.params) if (!validatedParams.success) { return res .status(400) .json({ error: 'Invalid path parameters', details: validatedParams.error.issues }) } const { index } = validatedParams.data // Check if class exists first const classExists = await Class.findOne({ index }).lean() if (!classExists) { return res.status(404).json({ error: 'Not found' }) } const urlString = '/api/2014/classes/' + index const data = await Proficiency.find({ 'classes.url': urlString }) .select({ index: 1, name: 1, url: 1, _id: 0 }) .sort({ index: 'asc' }) return res.status(200).json(ResourceList(data)) } catch (err) { next(err) } } ================================================ FILE: src/controllers/api/2014/conditionController.ts ================================================ import SimpleController from '@/controllers/simpleController' import ConditionModel from '@/models/2014/condition' export default new SimpleController(ConditionModel) ================================================ FILE: src/controllers/api/2014/damageTypeController.ts ================================================ import SimpleController from '@/controllers/simpleController' import DamageType from '@/models/2014/damageType' export default new SimpleController(DamageType) ================================================ FILE: src/controllers/api/2014/equipmentCategoryController.ts ================================================ import SimpleController from '@/controllers/simpleController' import EquipmentCategory from '@/models/2014/equipmentCategory' export default new SimpleController(EquipmentCategory) ================================================ FILE: src/controllers/api/2014/equipmentController.ts ================================================ import SimpleController from '@/controllers/simpleController' import Equipment from '@/models/2014/equipment' export default new SimpleController(Equipment) ================================================ FILE: src/controllers/api/2014/featController.ts ================================================ import SimpleController from '@/controllers/simpleController' import Feat from '@/models/2014/feat' export default new SimpleController(Feat) ================================================ FILE: src/controllers/api/2014/featureController.ts ================================================ import SimpleController from '@/controllers/simpleController' import Feature from '@/models/2014/feature' export default new SimpleController(Feature) ================================================ FILE: src/controllers/api/2014/languageController.ts ================================================ import SimpleController from '@/controllers/simpleController' import Language from '@/models/2014/language' export default new SimpleController(Language) ================================================ FILE: src/controllers/api/2014/magicItemController.ts ================================================ import { NextFunction, Request, Response } from 'express' import MagicItem from '@/models/2014/magicItem' import { NameQuerySchema, ShowParamsSchema } from '@/schemas/schemas' import { escapeRegExp, redisClient, ResourceList } from '@/util' interface IndexQuery { name?: { $regex: RegExp } } export const index = async (req: Request, res: Response, next: NextFunction) => { try { // Validate query parameters const validatedQuery = NameQuerySchema.safeParse(req.query) if (!validatedQuery.success) { return res .status(400) .json({ error: 'Invalid query parameters', details: validatedQuery.error.issues }) } const { name } = validatedQuery.data const searchQueries: IndexQuery = {} if (name !== undefined) { searchQueries.name = { $regex: new RegExp(escapeRegExp(name), 'i') } } const redisKey = req.originalUrl const data = await redisClient.get(redisKey) if (data !== null && data !== undefined && data !== '') { res.status(200).json(JSON.parse(data)) } else { const data = await MagicItem.find(searchQueries) .select({ index: 1, name: 1, url: 1, _id: 0 }) .sort({ index: 'asc' }) const jsonData = ResourceList(data) redisClient.set(redisKey, JSON.stringify(jsonData)) return res.status(200).json(jsonData) } } catch (err) { next(err) } } export const show = async (req: Request, res: Response, next: NextFunction) => { try { // Validate path parameters const validatedParams = ShowParamsSchema.safeParse(req.params) if (!validatedParams.success) { return res .status(400) .json({ error: 'Invalid path parameters', details: validatedParams.error.issues }) } const { index } = validatedParams.data const data = await MagicItem.findOne({ index: index }) if (data === null || data === undefined) return next() return res.status(200).json(data) } catch (err) { next(err) } } ================================================ FILE: src/controllers/api/2014/magicSchoolController.ts ================================================ import SimpleController from '@/controllers/simpleController' import MagicSchool from '@/models/2014/magicSchool' export default new SimpleController(MagicSchool) ================================================ FILE: src/controllers/api/2014/monsterController.ts ================================================ import { NextFunction, Request, Response } from 'express' import Monster from '@/models/2014/monster' import { MonsterIndexQuerySchema, ShowParamsSchema } from '@/schemas/schemas' import { escapeRegExp, redisClient, ResourceList } from '@/util' interface IndexQuery { name?: { $regex: RegExp } challenge_rating?: { $in: number[] } } export const index = async (req: Request, res: Response, next: NextFunction) => { try { // Validate query parameters const validatedQuery = MonsterIndexQuerySchema.safeParse(req.query) if (!validatedQuery.success) { return res .status(400) .json({ error: 'Invalid query parameters', details: validatedQuery.error.issues }) } const { name, challenge_rating } = validatedQuery.data const searchQueries: IndexQuery = {} if (name !== undefined) { searchQueries.name = { $regex: new RegExp(escapeRegExp(name), 'i') } } if (challenge_rating !== undefined && challenge_rating.length > 0) { searchQueries.challenge_rating = { $in: challenge_rating } } const redisKey = req.originalUrl const data = await redisClient.get(redisKey) if (data !== null) { res.status(200).json(JSON.parse(data)) } else { const data = await Monster.find(searchQueries) .select({ index: 1, name: 1, url: 1, _id: 0 }) .sort({ index: 'asc' }) const jsonData = ResourceList(data) redisClient.set(redisKey, JSON.stringify(jsonData)) return res.status(200).json(jsonData) } } catch (err) { next(err) } } export const show = async (req: Request, res: Response, next: NextFunction) => { try { // Validate path parameters const validatedParams = ShowParamsSchema.safeParse(req.params) if (!validatedParams.success) { return res .status(400) .json({ error: 'Invalid path parameters', details: validatedParams.error.issues }) } const { index } = validatedParams.data const data = await Monster.findOne({ index: index }) if (!data) return next() return res.status(200).json(data) } catch (err) { next(err) } } ================================================ FILE: src/controllers/api/2014/proficiencyController.ts ================================================ import SimpleController from '@/controllers/simpleController' import Proficiency from '@/models/2014/proficiency' export default new SimpleController(Proficiency) ================================================ FILE: src/controllers/api/2014/raceController.ts ================================================ import { NextFunction, Request, Response } from 'express' import SimpleController from '@/controllers/simpleController' import Proficiency from '@/models/2014/proficiency' import Race from '@/models/2014/race' import Subrace from '@/models/2014/subrace' import Trait from '@/models/2014/trait' import { ShowParamsSchema } from '@/schemas/schemas' import { ResourceList } from '@/util/data' const simpleController = new SimpleController(Race) export const index = async (req: Request, res: Response, next: NextFunction) => simpleController.index(req, res, next) export const show = async (req: Request, res: Response, next: NextFunction) => simpleController.show(req, res, next) export const showSubracesForRace = async (req: Request, res: Response, next: NextFunction) => { try { // Validate path parameters const validatedParams = ShowParamsSchema.safeParse(req.params) if (!validatedParams.success) { return res .status(400) .json({ error: 'Invalid path parameters', details: validatedParams.error.issues }) } const { index } = validatedParams.data const urlString = '/api/2014/races/' + index const data = await Subrace.find({ 'race.url': urlString }).select({ index: 1, name: 1, url: 1, _id: 0 }) return res.status(200).json(ResourceList(data)) } catch (err) { next(err) } } export const showTraitsForRace = async (req: Request, res: Response, next: NextFunction) => { try { // Validate path parameters const validatedParams = ShowParamsSchema.safeParse(req.params) if (!validatedParams.success) { return res .status(400) .json({ error: 'Invalid path parameters', details: validatedParams.error.issues }) } const { index } = validatedParams.data const urlString = '/api/2014/races/' + index const data = await Trait.find({ 'races.url': urlString }).select({ index: 1, name: 1, url: 1, _id: 0 }) return res.status(200).json(ResourceList(data)) } catch (err) { next(err) } } export const showProficienciesForRace = async (req: Request, res: Response, next: NextFunction) => { try { // Validate path parameters const validatedParams = ShowParamsSchema.safeParse(req.params) if (!validatedParams.success) { return res .status(400) .json({ error: 'Invalid path parameters', details: validatedParams.error.issues }) } const { index } = validatedParams.data const urlString = '/api/2014/races/' + index const data = await Proficiency.find({ 'races.url': urlString }) .select({ index: 1, name: 1, url: 1, _id: 0 }) .sort({ index: 'asc' }) return res.status(200).json(ResourceList(data)) } catch (err) { next(err) } } ================================================ FILE: src/controllers/api/2014/ruleController.ts ================================================ import { NextFunction, Request, Response } from 'express' import Rule from '@/models/2014/rule' import { NameDescQuerySchema, ShowParamsSchema } from '@/schemas/schemas' import { escapeRegExp, redisClient, ResourceList } from '@/util' interface IndexQuery { name?: { $regex: RegExp } desc?: { $regex: RegExp } } export const index = async (req: Request, res: Response, next: NextFunction) => { try { // Validate query parameters const validatedQuery = NameDescQuerySchema.safeParse(req.query) if (!validatedQuery.success) { return res .status(400) .json({ error: 'Invalid query parameters', details: validatedQuery.error.issues }) } const { name, desc } = validatedQuery.data const searchQueries: IndexQuery = {} if (name !== undefined) { searchQueries.name = { $regex: new RegExp(escapeRegExp(name), 'i') } } if (desc !== undefined) { searchQueries.desc = { $regex: new RegExp(escapeRegExp(desc), 'i') } } const redisKey = req.originalUrl const data = await redisClient.get(redisKey) if (data !== null) { res.status(200).json(JSON.parse(data)) } else { const data = await Rule.find(searchQueries) .select({ index: 1, name: 1, url: 1, _id: 0 }) .sort({ index: 'asc' }) const jsonData = ResourceList(data) redisClient.set(redisKey, JSON.stringify(jsonData)) return res.status(200).json(jsonData) } } catch (err) { next(err) } } export const show = async (req: Request, res: Response, next: NextFunction) => { try { // Validate path parameters const validatedParams = ShowParamsSchema.safeParse(req.params) if (!validatedParams.success) { return res .status(400) .json({ error: 'Invalid path parameters', details: validatedParams.error.issues }) } const { index } = validatedParams.data const data = await Rule.findOne({ index: index }) if (!data) return next() return res.status(200).json(data) } catch (err) { next(err) } } ================================================ FILE: src/controllers/api/2014/ruleSectionController.ts ================================================ import { NextFunction, Request, Response } from 'express' import RuleSection from '@/models/2014/ruleSection' import { NameDescQuerySchema, ShowParamsSchema } from '@/schemas/schemas' import { escapeRegExp, redisClient, ResourceList } from '@/util' interface IndexQuery { name?: { $regex: RegExp } desc?: { $regex: RegExp } } export const index = async (req: Request, res: Response, next: NextFunction) => { try { // Validate query parameters const validatedQuery = NameDescQuerySchema.safeParse(req.query) if (!validatedQuery.success) { return res .status(400) .json({ error: 'Invalid query parameters', details: validatedQuery.error.issues }) } const { name, desc } = validatedQuery.data const searchQueries: IndexQuery = {} if (name !== undefined) { searchQueries.name = { $regex: new RegExp(escapeRegExp(name), 'i') } } if (desc !== undefined) { searchQueries.desc = { $regex: new RegExp(escapeRegExp(desc), 'i') } } const redisKey = req.originalUrl const data = await redisClient.get(redisKey) if (data !== null) { res.status(200).json(JSON.parse(data)) } else { const data = await RuleSection.find(searchQueries) .select({ index: 1, name: 1, url: 1, _id: 0 }) .sort({ index: 'asc' }) const jsonData = ResourceList(data) redisClient.set(redisKey, JSON.stringify(jsonData)) return res.status(200).json(jsonData) } } catch (err) { next(err) } } export const show = async (req: Request, res: Response, next: NextFunction) => { try { // Validate path parameters const validatedParams = ShowParamsSchema.safeParse(req.params) if (!validatedParams.success) { return res .status(400) .json({ error: 'Invalid path parameters', details: validatedParams.error.issues }) } const { index } = validatedParams.data const data = await RuleSection.findOne({ index: index }) if (!data) return next() return res.status(200).json(data) } catch (err) { next(err) } } ================================================ FILE: src/controllers/api/2014/skillController.ts ================================================ import SimpleController from '@/controllers/simpleController' import Skill from '@/models/2014/skill' export default new SimpleController(Skill) ================================================ FILE: src/controllers/api/2014/spellController.ts ================================================ import { NextFunction, Request, Response } from 'express' import Spell from '@/models/2014/spell' import { ShowParamsSchema, SpellIndexQuerySchema } from '@/schemas/schemas' import { escapeRegExp, redisClient, ResourceList } from '@/util' interface IndexQuery { name?: { $regex: RegExp } level?: { $in: number[] } 'school.name'?: { $in: RegExp[] } } export const index = async (req: Request, res: Response, next: NextFunction) => { try { const validatedQuery = SpellIndexQuerySchema.safeParse(req.query) if (!validatedQuery.success) { return res .status(400) .json({ error: 'Invalid query parameters', details: validatedQuery.error.issues }) } const { name, level, school } = validatedQuery.data const searchQueries: IndexQuery = {} if (name !== undefined) { searchQueries.name = { $regex: new RegExp(escapeRegExp(name), 'i') } } if (level !== undefined) { searchQueries.level = { $in: level.map(Number) } } if (school !== undefined) { const schoolRegex = school.map((s) => new RegExp(escapeRegExp(s), 'i')) searchQueries['school.name'] = { $in: schoolRegex } } const redisKey = req.originalUrl const data = await redisClient.get(redisKey) if (data !== null) { res.status(200).json(JSON.parse(data)) } else { const data = await Spell.find(searchQueries) .select({ index: 1, level: 1, name: 1, url: 1, _id: 0 }) .sort({ index: 'asc' }) const jsonData = ResourceList(data) redisClient.set(redisKey, JSON.stringify(jsonData)) return res.status(200).json(jsonData) } } catch (err) { next(err) } } export const show = async (req: Request, res: Response, next: NextFunction) => { try { const validatedParams = ShowParamsSchema.safeParse(req.params) if (!validatedParams.success) { return res .status(400) .json({ error: 'Invalid path parameters', details: validatedParams.error.issues }) } const { index } = validatedParams.data const data = await Spell.findOne({ index: index }) if (!data) return next() return res.status(200).json(data) } catch (err) { next(err) } } ================================================ FILE: src/controllers/api/2014/subclassController.ts ================================================ import { NextFunction, Request, Response } from 'express' import SimpleController from '@/controllers/simpleController' import Feature from '@/models/2014/feature' import Level from '@/models/2014/level' import Subclass from '@/models/2014/subclass' import { LevelParamsSchema, ShowParamsSchema } from '@/schemas/schemas' import { ResourceList } from '@/util/data' const simpleController = new SimpleController(Subclass) export const index = async (req: Request, res: Response, next: NextFunction) => simpleController.index(req, res, next) export const show = async (req: Request, res: Response, next: NextFunction) => simpleController.show(req, res, next) export const showLevelsForSubclass = async (req: Request, res: Response, next: NextFunction) => { try { const validatedParams = ShowParamsSchema.safeParse(req.params) if (!validatedParams.success) { return res .status(400) .json({ error: 'Invalid path parameters', details: validatedParams.error.issues }) } const { index } = validatedParams.data const urlString = '/api/2014/subclasses/' + index const data = await Level.find({ 'subclass.url': urlString }).sort({ level: 'asc' }) return res.status(200).json(data) } catch (err) { next(err) } } export const showLevelForSubclass = async (req: Request, res: Response, next: NextFunction) => { try { const validatedParams = LevelParamsSchema.safeParse(req.params) if (!validatedParams.success) { return res .status(400) .json({ error: 'Invalid path parameters', details: validatedParams.error.issues }) } const { index, level } = validatedParams.data const urlString = '/api/2014/subclasses/' + index + '/levels/' + level const data = await Level.findOne({ url: urlString }) if (!data) return next() return res.status(200).json(data) } catch (err) { next(err) } } export const showFeaturesForSubclass = async (req: Request, res: Response, next: NextFunction) => { try { const validatedParams = ShowParamsSchema.safeParse(req.params) if (!validatedParams.success) { return res .status(400) .json({ error: 'Invalid path parameters', details: validatedParams.error.issues }) } const { index } = validatedParams.data const urlString = '/api/2014/subclasses/' + index const data = await Feature.find({ 'subclass.url': urlString }) .select({ index: 1, name: 1, url: 1, _id: 0 }) .sort({ level: 'asc', url: 'asc' }) return res.status(200).json(ResourceList(data)) } catch (err) { next(err) } } export const showFeaturesForSubclassAndLevel = async ( req: Request, res: Response, next: NextFunction ) => { try { const validatedParams = LevelParamsSchema.safeParse(req.params) if (!validatedParams.success) { return res .status(400) .json({ error: 'Invalid path parameters', details: validatedParams.error.issues }) } const { index, level } = validatedParams.data const urlString = '/api/2014/subclasses/' + index const data = await Feature.find({ level: level, 'subclass.url': urlString }) .select({ index: 1, name: 1, url: 1, _id: 0 }) .sort({ level: 'asc', url: 'asc' }) return res.status(200).json(ResourceList(data)) } catch (err) { next(err) } } ================================================ FILE: src/controllers/api/2014/subraceController.ts ================================================ import { NextFunction, Request, Response } from 'express' import SimpleController from '@/controllers/simpleController' import Proficiency from '@/models/2014/proficiency' import Subrace from '@/models/2014/subrace' import Trait from '@/models/2014/trait' import { ShowParamsSchema } from '@/schemas/schemas' import { ResourceList } from '@/util/data' const simpleController = new SimpleController(Subrace) export const index = async (req: Request, res: Response, next: NextFunction) => simpleController.index(req, res, next) export const show = async (req: Request, res: Response, next: NextFunction) => simpleController.show(req, res, next) export const showTraitsForSubrace = async (req: Request, res: Response, next: NextFunction) => { try { // Validate path parameters const validatedParams = ShowParamsSchema.safeParse(req.params) if (!validatedParams.success) { return res .status(400) .json({ error: 'Invalid path parameters', details: validatedParams.error.issues }) } const { index } = validatedParams.data const urlString = '/api/2014/subraces/' + index const data = await Trait.find({ 'subraces.url': urlString }).select({ index: 1, name: 1, url: 1, _id: 0 }) return res.status(200).json(ResourceList(data)) } catch (err) { next(err) } } export const showProficienciesForSubrace = async ( req: Request, res: Response, next: NextFunction ) => { try { // Validate path parameters const validatedParams = ShowParamsSchema.safeParse(req.params) if (!validatedParams.success) { return res .status(400) .json({ error: 'Invalid path parameters', details: validatedParams.error.issues }) } const { index } = validatedParams.data const urlString = '/api/2014/subraces/' + index const data = await Proficiency.find({ 'races.url': urlString }) .select({ index: 1, name: 1, url: 1, _id: 0 }) .sort({ index: 'asc' }) return res.status(200).json(ResourceList(data)) } catch (err) { next(err) } } ================================================ FILE: src/controllers/api/2014/traitController.ts ================================================ import SimpleController from '@/controllers/simpleController' import Trait from '@/models/2014/trait' export default new SimpleController(Trait) ================================================ FILE: src/controllers/api/2014/weaponPropertyController.ts ================================================ import SimpleController from '@/controllers/simpleController' import WeaponProperty from '@/models/2014/weaponProperty' export default new SimpleController(WeaponProperty) ================================================ FILE: src/controllers/api/2024/abilityScoreController.ts ================================================ import SimpleController from '@/controllers/simpleController' import AbilityScoreModel from '@/models/2024/abilityScore' export default new SimpleController(AbilityScoreModel) ================================================ FILE: src/controllers/api/2024/alignmentController.ts ================================================ import SimpleController from '@/controllers/simpleController' import AlignmentModel from '@/models/2024/alignment' export default new SimpleController(AlignmentModel) ================================================ FILE: src/controllers/api/2024/backgroundController.ts ================================================ import SimpleController from '@/controllers/simpleController' import BackgroundModel from '@/models/2024/background' export default new SimpleController(BackgroundModel) ================================================ FILE: src/controllers/api/2024/conditionController.ts ================================================ import SimpleController from '@/controllers/simpleController' import ConditionModel from '@/models/2024/condition' export default new SimpleController(ConditionModel) ================================================ FILE: src/controllers/api/2024/damageTypeController.ts ================================================ import SimpleController from '@/controllers/simpleController' import DamageTypeModel from '@/models/2024/damageType' export default new SimpleController(DamageTypeModel) ================================================ FILE: src/controllers/api/2024/equipmentCategoryController.ts ================================================ import SimpleController from '@/controllers/simpleController' import EquipmentCategory from '@/models/2024/equipmentCategory' export default new SimpleController(EquipmentCategory) ================================================ FILE: src/controllers/api/2024/equipmentController.ts ================================================ import SimpleController from '@/controllers/simpleController' import Equipment from '@/models/2024/equipment' export default new SimpleController(Equipment) ================================================ FILE: src/controllers/api/2024/featController.ts ================================================ import SimpleController from '@/controllers/simpleController' import FeatModel from '@/models/2024/feat' export default new SimpleController(FeatModel) ================================================ FILE: src/controllers/api/2024/languageController.ts ================================================ import SimpleController from '@/controllers/simpleController' import LanguageModel from '@/models/2024/language' export default new SimpleController(LanguageModel) ================================================ FILE: src/controllers/api/2024/magicItemController.ts ================================================ import SimpleController from '@/controllers/simpleController' import MagicItemModel from '@/models/2024/magicItem' export default new SimpleController(MagicItemModel) ================================================ FILE: src/controllers/api/2024/magicSchoolController.ts ================================================ import SimpleController from '@/controllers/simpleController' import MagicSchoolModel from '@/models/2024/magicSchool' export default new SimpleController(MagicSchoolModel) ================================================ FILE: src/controllers/api/2024/proficiencyController.ts ================================================ import SimpleController from '@/controllers/simpleController' import ProficiencyModel from '@/models/2024/proficiency' export default new SimpleController(ProficiencyModel) ================================================ FILE: src/controllers/api/2024/skillController.ts ================================================ import SimpleController from '@/controllers/simpleController' import Skill from '@/models/2024/skill' export default new SimpleController(Skill) ================================================ FILE: src/controllers/api/2024/speciesController.ts ================================================ import { NextFunction, Request, Response } from 'express' import SimpleController from '@/controllers/simpleController' import Species2024Model from '@/models/2024/species' import Subspecies2024Model from '@/models/2024/subspecies' import Trait2024Model from '@/models/2024/trait' import { ShowParamsSchema } from '@/schemas/schemas' import { ResourceList } from '@/util/data' const simpleController = new SimpleController(Species2024Model) export const index = async (req: Request, res: Response, next: NextFunction) => simpleController.index(req, res, next) export const show = async (req: Request, res: Response, next: NextFunction) => simpleController.show(req, res, next) export const showSubspeciesForSpecies = async (req: Request, res: Response, next: NextFunction) => { try { const validatedParams = ShowParamsSchema.safeParse(req.params) if (!validatedParams.success) { return res .status(400) .json({ error: 'Invalid path parameters', details: validatedParams.error.issues }) } const { index } = validatedParams.data const urlString = '/api/2024/species/' + index const data = await Subspecies2024Model.find({ 'species.url': urlString }).select({ index: 1, name: 1, url: 1, _id: 0 }) return res.status(200).json(ResourceList(data)) } catch (err) { next(err) } } export const showTraitsForSpecies = async (req: Request, res: Response, next: NextFunction) => { try { const validatedParams = ShowParamsSchema.safeParse(req.params) if (!validatedParams.success) { return res .status(400) .json({ error: 'Invalid path parameters', details: validatedParams.error.issues }) } const { index } = validatedParams.data const urlString = '/api/2024/species/' + index const data = await Trait2024Model.find({ 'species.url': urlString }).select({ index: 1, name: 1, url: 1, _id: 0 }) return res.status(200).json(ResourceList(data)) } catch (err) { next(err) } } ================================================ FILE: src/controllers/api/2024/subclassController.ts ================================================ import SimpleController from '@/controllers/simpleController' import SubclassModel from '@/models/2024/subclass' export default new SimpleController(SubclassModel) ================================================ FILE: src/controllers/api/2024/subspeciesController.ts ================================================ import { NextFunction, Request, Response } from 'express' import SimpleController from '@/controllers/simpleController' import Subspecies2024Model from '@/models/2024/subspecies' import Trait2024Model from '@/models/2024/trait' import { ShowParamsSchema } from '@/schemas/schemas' import { ResourceList } from '@/util/data' const simpleController = new SimpleController(Subspecies2024Model) export const index = async (req: Request, res: Response, next: NextFunction) => simpleController.index(req, res, next) export const show = async (req: Request, res: Response, next: NextFunction) => simpleController.show(req, res, next) export const showTraitsForSubspecies = async ( req: Request, res: Response, next: NextFunction ) => { try { const validatedParams = ShowParamsSchema.safeParse(req.params) if (!validatedParams.success) { return res .status(400) .json({ error: 'Invalid path parameters', details: validatedParams.error.issues }) } const { index } = validatedParams.data const urlString = '/api/2024/subspecies/' + index const data = await Trait2024Model.find({ 'subspecies.url': urlString }).select({ index: 1, name: 1, url: 1, _id: 0 }) return res.status(200).json(ResourceList(data)) } catch (err) { next(err) } } ================================================ FILE: src/controllers/api/2024/traitController.ts ================================================ import SimpleController from '@/controllers/simpleController' import Trait2024Model from '@/models/2024/trait' export default new SimpleController(Trait2024Model) ================================================ FILE: src/controllers/api/2024/weaponMasteryPropertyController.ts ================================================ import SimpleController from '@/controllers/simpleController' import WeaponMasteryPropertyModel from '@/models/2024/weaponMasteryProperty' export default new SimpleController(WeaponMasteryPropertyModel) ================================================ FILE: src/controllers/api/2024/weaponPropertyController.ts ================================================ import SimpleController from '@/controllers/simpleController' import WeaponPropertyModel from '@/models/2024/weaponProperty' export default new SimpleController(WeaponPropertyModel) ================================================ FILE: src/controllers/api/imageController.ts ================================================ import { NextFunction, Request, Response } from 'express' import { awsRegion } from '@/util/environmentVariables' const BUCKET_NAME = 'dnd-5e-api-images' const AWS_REGION = awsRegion || 'us-east-1' const show = async (req: Request, res: Response, next: NextFunction) => { let key: string | undefined try { key = req.url.slice(1) if (!key || !/^[a-zA-Z0-9/._-]+$/.test(key)) { return res.status(400).send('Invalid image path') } const publicUrl = `https://${BUCKET_NAME}.s3.${AWS_REGION}.amazonaws.com/${key}` const s3Response = await fetch(publicUrl) if (!s3Response.ok) { return res.status(s3Response.status).send(await s3Response.text()) } const contentTypeFromHeader = s3Response.headers.get('content-type') const actualContentType = contentTypeFromHeader != null ? contentTypeFromHeader : 'application/octet-stream' res.setHeader('Content-Type', actualContentType) const contentLength = s3Response.headers.get('content-length') if (typeof contentLength === 'string' && contentLength) { res.setHeader('Content-Length', contentLength) } if (s3Response.body) { const reader = s3Response.body.getReader() while (true) { const { done, value } = await reader.read() if (done) break res.write(value) } res.end() } else { throw new Error('Response body from S3 was null') } } catch (err: any) { if (err !== null) { res.status(404).end('File Not Found') } else { console.error('Error fetching image from S3:', err) next(err) } } } export default { show } ================================================ FILE: src/controllers/api/v2014Controller.ts ================================================ import { NextFunction, Request, Response } from 'express' import Collection from '@/models/2014/collection' export const index = async (req: Request, res: Response, next: NextFunction) => { try { const data = await Collection.find({}) .select({ index: 1, _id: 0 }) .sort({ index: 'asc' }) .exec() const apiIndex: Record = {} data.forEach((item) => { if (item.index === 'levels') return apiIndex[item.index] = `/api/2014/${item.index}` }) return res.status(200).json(apiIndex) } catch (err) { next(err) } } ================================================ FILE: src/controllers/api/v2024Controller.ts ================================================ import { NextFunction, Request, Response } from 'express' import Collection from '@/models/2024/collection' export const index = async (req: Request, res: Response, next: NextFunction) => { try { const data = await Collection.find({}) .select({ index: 1, _id: 0 }) .sort({ index: 'asc' }) .exec() const apiIndex: Record = {} data.forEach((item) => { if (item.index === 'levels') return apiIndex[item.index] = `/api/2024/${item.index}` }) return res.status(200).json(apiIndex) } catch (err) { next(err) } } ================================================ FILE: src/controllers/apiController.ts ================================================ import { NextFunction, Request, Response } from 'express' import Collection from '@/models/2014/collection' export default async (req: Request, res: Response, next: NextFunction) => { try { const collections = await Collection.find({}) const colName = req.path.split('/')[1] const colRequested = collections.find((col) => col.index === colName) if (colRequested === undefined && colName !== '') { res.sendStatus(404) return } const queryString = req.originalUrl.split('?')?.[1] const redirectUrl = '/api/2014' + req.path const urlWithQuery = queryString === undefined ? redirectUrl : redirectUrl + '?' + queryString res.redirect(301, urlWithQuery) } catch (err) { next(err) } } ================================================ FILE: src/controllers/docsController.ts ================================================ import { Request, Response } from 'express' export default (req: Request, res: Response) => { res.redirect('https://5e-bits.github.io/docs') } ================================================ FILE: src/controllers/simpleController.ts ================================================ import { ReturnModelType } from '@typegoose/typegoose' import { NextFunction, Request, Response } from 'express' import { NameQuerySchema, ShowParamsSchema } from '@/schemas/schemas' import { ResourceList } from '@/util/data' import { escapeRegExp } from '@/util/regex' interface IndexQuery { name?: { $regex: RegExp } } class SimpleController { Schema: ReturnModelType constructor(Schema: ReturnModelType) { this.Schema = Schema } async index(req: Request, res: Response, next: NextFunction) { try { // Validate query parameters const validatedQuery = NameQuerySchema.safeParse(req.query) if (!validatedQuery.success) { // Handle validation errors - customize error response as needed return res .status(400) .json({ error: 'Invalid query parameters', details: validatedQuery.error.issues }) } const { name } = validatedQuery.data const searchQueries: IndexQuery = {} if (name !== undefined) { // Use validated name searchQueries.name = { $regex: new RegExp(escapeRegExp(name), 'i') } } const data = await this.Schema.find(searchQueries) .select({ index: 1, name: 1, url: 1, _id: 0 }) .sort({ index: 'asc' }) .exec() return res.status(200).json(ResourceList(data)) } catch (err) { next(err) } } async show(req: Request, res: Response, next: NextFunction) { try { // Validate path parameters const validatedParams = ShowParamsSchema.safeParse(req.params) if (!validatedParams.success) { // Handle validation errors return res .status(400) .json({ error: 'Invalid path parameters', details: validatedParams.error.issues }) } const { index } = validatedParams.data // Use validated index // Use validated index in the query const data = await this.Schema.findOne({ index }) if (data === null) return next() res.status(200).json(data) } catch (err) { next(err) } } } export default SimpleController ================================================ FILE: src/css/custom.css ================================================ /* Reset and base styles */ * { margin: 0; padding: 0; box-sizing: border-box; } :root { --primary-color: #d81921; --primary-dark: #c62828; --text-color: #263238; --bg-dark: #21201e; --bg-light: #f5f5f5; --link-color: #1b95e0; --spacing: 1rem; } body { font-family: 'Helvetica', sans-serif; color: var(--text-color); line-height: 1.6; padding-top: 3.5rem; } /* Typography */ h1, h2, h3, h4, h5, h6 { margin: 0 0 var(--spacing) 0; font-weight: 900; } a { color: var(--link-color); text-decoration: none; } /* Navigation */ .navbar { position: fixed; top: 0; left: 0; right: 0; background: var(--primary-color); box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); z-index: 1000; height: 3.5rem; overflow: hidden; } .nav-container { max-width: 1200px; margin: 0 auto; padding: 0 var(--spacing); } .nav-links { display: flex; justify-content: flex-end; list-style: none; } .nav-links li a { display: block; padding: 1rem; color: white; transition: background-color 0.3s; } .nav-links li.active a, .nav-links li a:hover { background-color: var(--primary-dark); } /* Header */ .header { background-color: var(--bg-dark); padding: 4rem 1rem; text-align: center; } .header h1 { color: white; margin: 0 auto; max-width: 800px; font-size: 3.5rem; line-height: 1.2; } .header h2 { color: white; margin: 0.5rem auto 0; max-width: 800px; font-weight: 300; font-size: 1.5rem; opacity: 0.9; } /* CTA Section */ .cta { background-color: var(--bg-light); padding: 3rem 1rem; text-align: center; font-size: 1.3rem; font-weight: 300; } /* Interactive Section */ .content { max-width: 1200px; margin: 0 auto; padding: 2rem var(--spacing); } .interactive-section { max-width: 800px; margin: 0 auto; } .api-input { display: flex; margin: 1rem 0; border: 1px solid #ddd; border-radius: 4px; overflow: hidden; } .api-prefix { padding: 0.5rem 1rem; background: var(--bg-light); border-right: 1px solid #ddd; white-space: nowrap; } #interactive { flex: 1; padding: 0.5rem; border: none; outline: none; } .api-input button { padding: 0.5rem 1.5rem; background: var(--link-color); color: white; border: none; cursor: pointer; transition: background-color 0.3s; } .api-input button:hover { background-color: #1576b3; } .hints { font-size: 0.9rem; margin: 1rem 0; } .output { background: var(--bg-light); padding: 1rem; border-radius: 4px; overflow: auto; max-height: 400px; white-space: pre-wrap; word-break: break-word; } /* Responsive Design */ @media (max-width: 768px) { .nav-links { display: none; } .menu-toggle { display: block; } .api-input { flex-direction: column; } .api-prefix { border-right: none; border-bottom: 1px solid #ddd; } } .menu-toggle { display: none; background: none; border: none; padding: 1rem; cursor: pointer; } .menu-toggle span { display: block; width: 25px; height: 3px; background: white; margin: 4px 0; transition: 0.3s; } ================================================ FILE: src/graphql/2014/common/choiceTypes.ts ================================================ import { Field, Int, ObjectType } from 'type-graphql' import { AbilityScore } from '@/models/2014/abilityScore' import { Language } from '@/models/2014/language' import { Proficiency } from '@/models/2014/proficiency' import { ProficiencyChoiceItem } from './unions' // --- Language Choice Types --- @ObjectType({ description: 'Represents a reference to a language within a choice option set.' }) export class LanguageChoiceOption { @Field(() => String, { description: 'The type of this option (e.g., "reference").' }) option_type!: string @Field(() => Language, { description: 'The resolved Language object.' }) item!: Language } @ObjectType({ description: 'Represents a set of language options for a choice.' }) export class LanguageChoiceOptionSet { @Field(() => String, { description: 'The type of the option set (e.g., resource_list, options_array).' }) option_set_type!: string @Field(() => [LanguageChoiceOption], { description: 'The list of language options available.' }) options!: LanguageChoiceOption[] } @ObjectType({ description: 'Represents a choice from a list of languages.' }) export class LanguageChoice { @Field(() => Int, { description: 'The number of languages to choose from this list.' }) choose!: number @Field(() => String, { description: 'The type of choice (e.g., languages).' }) type!: string @Field(() => LanguageChoiceOptionSet, { description: 'The set of language options available.' }) from!: LanguageChoiceOptionSet } // --- Proficiency Choice Types --- @ObjectType({ description: 'Represents a reference to a Proficiency or nested ProficiencyChoice within a choice option set.' }) export class ProficiencyChoiceOption { @Field(() => String, { description: 'The type of this option (e.g., "reference", "choice").' }) option_type!: string @Field(() => ProficiencyChoiceItem, { description: 'The resolved Proficiency object or nested ProficiencyChoice.' }) item!: Proficiency | ProficiencyChoice } @ObjectType({ description: 'Represents a set of Proficiency options for a choice.' }) export class ProficiencyChoiceOptionSet { @Field(() => String, { description: 'The type of the option set (e.g., resource_list, options_array).' }) option_set_type!: string @Field(() => [ProficiencyChoiceOption], { description: 'The list of Proficiency options available.' }) options!: ProficiencyChoiceOption[] } @ObjectType({ description: 'Represents a choice from a list of Proficiencies or nested ProficiencyChoices.' }) export class ProficiencyChoice { @Field(() => Int, { description: 'The number of Proficiencies to choose from this list.' }) choose!: number @Field(() => String, { description: 'The type of choice (e.g., proficiencies).' }) type!: string @Field(() => ProficiencyChoiceOptionSet, { description: 'The set of Proficiency options available.' }) from!: ProficiencyChoiceOptionSet @Field(() => String, { nullable: true, description: 'Description of the choice.' }) desc?: string } // --- Prerequisite Choice Types --- @ObjectType({ description: 'A single prerequisite option' }) export class PrerequisiteChoiceOption { @Field(() => String, { description: 'The type of option.' }) option_type!: string @Field(() => AbilityScore, { description: 'The ability score required.' }) ability_score!: AbilityScore @Field(() => Int, { description: 'The minimum score required.' }) minimum_score!: number } @ObjectType({ description: 'A set of prerequisite options to choose from' }) export class PrerequisiteChoiceOptionSet { @Field(() => String, { description: 'The type of option set.' }) option_set_type!: string @Field(() => [PrerequisiteChoiceOption], { description: 'The available options.' }) options!: PrerequisiteChoiceOption[] } @ObjectType({ description: 'A choice of prerequisites for multi-classing' }) export class PrerequisiteChoice { @Field(() => Int, { description: 'Number of prerequisites to choose.' }) choose!: number @Field(() => String, { description: 'Type of prerequisites to choose from.' }) type!: string @Field(() => PrerequisiteChoiceOptionSet, { description: 'The options to choose from.' }) from!: PrerequisiteChoiceOptionSet @Field(() => String, { nullable: true, description: 'Description of the prerequisite choice.' }) desc?: string } // --- Ability Score Bonus Choice Types --- @ObjectType({ description: 'A single ability score bonus option' }) export class AbilityScoreBonusChoiceOption { @Field(() => String, { description: 'The type of option.' }) option_type!: string @Field(() => AbilityScore, { description: 'The ability score to increase.' }) ability_score!: AbilityScore @Field(() => Int, { description: 'The amount to increase the ability score by.' }) bonus!: number } @ObjectType({ description: 'A set of ability score bonus options to choose from' }) export class AbilityScoreBonusChoiceOptionSet { @Field(() => String, { description: 'The type of option set.' }) option_set_type!: string @Field(() => [AbilityScoreBonusChoiceOption], { description: 'The available options.' }) options!: AbilityScoreBonusChoiceOption[] } @ObjectType({ description: 'A choice of ability score bonuses for a race' }) export class AbilityScoreBonusChoice { @Field(() => Int, { description: 'Number of ability score bonuses to choose.' }) choose!: number @Field(() => String, { description: 'Type of ability score bonuses to choose from.' }) type!: string @Field(() => AbilityScoreBonusChoiceOptionSet, { description: 'The options to choose from.' }) from!: AbilityScoreBonusChoiceOptionSet @Field(() => String, { nullable: true, description: 'Description of the ability score bonus choice.' }) desc?: string } ================================================ FILE: src/graphql/2014/common/equipmentTypes.ts ================================================ import { Field, Int, ObjectType } from 'type-graphql' import { ArmorClass, Content, Equipment, Range, Speed, ThrowRange } from '@/models/2014/equipment' import { WeaponProperty } from '@/models/2014/weaponProperty' import { APIReference } from '@/models/common/apiReference' import { Damage } from '@/models/common/damage' import { IEquipment } from './interfaces' @ObjectType({ description: 'Represents Armor equipment', implements: IEquipment }) export class Armor extends Equipment { @Field(() => String, { description: 'Category of armor (e.g., Light, Medium, Heavy).' }) declare armor_category: string @Field(() => ArmorClass, { description: 'Armor export Class details for this armor.' }) declare armor_class: ArmorClass @Field(() => Int, { nullable: true, description: 'Minimum Strength score required to use this armor effectively.' }) declare str_minimum?: number @Field(() => Boolean, { nullable: true, description: 'Whether wearing the armor imposes disadvantage on Stealth checks.' }) declare stealth_disadvantage?: boolean } @ObjectType({ description: 'Represents Weapon equipment', implements: IEquipment }) export class Weapon extends Equipment { @Field(() => String, { description: 'Category of weapon (e.g., Simple, Martial).' }) declare weapon_category: string @Field(() => String, { description: 'Range classification of weapon (e.g., Melee, Ranged).' }) declare weapon_range: string @Field(() => String, { description: 'Range category for weapons (e.g., Melee, Ranged).' }) declare category_range: string @Field(() => Damage, { nullable: true, description: 'Primary damage dealt by the weapon.' }) declare damage?: Damage @Field(() => Damage, { nullable: true, description: 'Damage dealt when using the weapon with two hands.' }) declare two_handed_damage?: Damage @Field(() => Range, { nullable: true, description: 'Weapon range details.' }) declare range?: Range @Field(() => ThrowRange, { nullable: true, description: 'Range when the weapon is thrown.' }) declare throw_range?: ThrowRange @Field(() => [WeaponProperty], { nullable: true, description: 'Properties of the weapon.' }) declare properties?: APIReference[] // Resolved externally } @ObjectType({ description: 'Represents Tool equipment', implements: IEquipment }) export class Tool extends Equipment { @Field(() => String, { description: "Category of tool (e.g., Artisan's Tools, Gaming Set)." }) declare tool_category: string } @ObjectType({ description: 'Represents Gear equipment (general purpose)', implements: IEquipment }) export class Gear extends Equipment {} @ObjectType({ description: "Represents Gear that contains other items (e.g., Explorer's Pack)", implements: IEquipment }) export class Pack extends Gear { @Field(() => [Content], { nullable: true, description: 'Items contained within the pack.' }) declare contents?: Content[] } @ObjectType({ description: 'Represents Ammunition equipment', implements: IEquipment }) export class Ammunition extends Gear { @Field(() => Int, { description: 'Quantity of ammunition in the bundle.' }) declare quantity: number } @ObjectType({ description: 'Represents Vehicle equipment', implements: IEquipment }) export class Vehicle extends Equipment { @Field(() => String, { description: 'Category of vehicle (e.g., Ship, Land).' }) declare vehicle_category: string @Field(() => Speed, { nullable: true, description: 'Movement speed of the vehicle.' }) declare speed?: Speed @Field(() => String, { nullable: true, description: 'Carrying capacity of the vehicle.' }) declare capacity?: string } ================================================ FILE: src/graphql/2014/common/interfaces.ts ================================================ import { Field, Float, InterfaceType } from 'type-graphql' import { Cost } from '@/models/2014/equipment' @InterfaceType({ description: 'Common fields shared by all types of equipment and magic items.' }) export abstract class IEquipment { @Field(() => String, { description: 'The unique identifier for this equipment.' }) index!: string @Field(() => String, { description: 'The name of the equipment.' }) name!: string @Field(() => Cost, { description: 'Cost of the equipment in coinage.' }) cost!: Cost @Field(() => Float, { nullable: true, description: 'Weight of the equipment in pounds.' }) weight?: number @Field(() => [String], { nullable: true, description: 'Description of the equipment.' }) desc?: string[] } ================================================ FILE: src/graphql/2014/common/unions.ts ================================================ import { createUnionType } from 'type-graphql' import { ProficiencyChoice } from '@/graphql/2014/common/choiceTypes' import { MagicItem } from '@/models/2014/magicItem' import { Proficiency } from '@/models/2014/proficiency' import { Ammunition, Armor, Gear, Pack, Tool, Vehicle, Weapon } from './equipmentTypes' function resolveEquipmentType( value: any ): | typeof Armor | typeof Weapon | typeof Tool | typeof Gear | typeof Pack | typeof Ammunition | typeof Vehicle | null { if ('armor_class' in value) { return Armor } if ('weapon_category' in value || 'weapon_range' in value) { return Weapon } if ('tool_category' in value) { return Tool } if ('vehicle_category' in value) { return Vehicle } if ('contents' in value) { return Pack } if (value.gear_category?.index === 'ammunition') { return Ammunition } if ('gear_category' in value) { return Gear } return null } export const EquipmentOrMagicItem = createUnionType({ name: 'EquipmentOrMagicItem', types: () => { return [Armor, Weapon, Tool, Gear, Pack, Ammunition, Vehicle, MagicItem] as const }, resolveType: (value) => { if ('rarity' in value) { return MagicItem } const equipmentType = resolveEquipmentType(value) if (equipmentType) { return equipmentType } console.warn('Could not resolve type for EquipmentOrMagicItem:', value) throw new Error('Could not resolve type for EquipmentOrMagicItem') } }) export const AnyEquipment = createUnionType({ name: 'AnyEquipment', types: () => { return [Armor, Weapon, Tool, Gear, Pack, Ammunition, Vehicle] as const }, resolveType: (value) => { const equipmentType = resolveEquipmentType(value) if (equipmentType) { return equipmentType } console.warn('Could not resolve type for AnyEquipment:', value) return Gear } }) export const ProficiencyChoiceItem = createUnionType({ name: 'ProficiencyChoiceItem', types: () => [Proficiency, ProficiencyChoice] as const, resolveType: (value) => { if (typeof value === 'object' && 'choose' in value && 'type' in value && 'from' in value) { return ProficiencyChoice } return Proficiency } }) ================================================ FILE: src/graphql/2014/resolvers/abilityScore/args.ts ================================================ import { ArgsType, Field, InputType, registerEnumType } from 'type-graphql' import { z } from 'zod' import { BaseFilterArgs, BaseFilterArgsSchema, BaseIndexArgsSchema, BaseOrderInterface } from '@/graphql/common/args' import { OrderByDirection } from '@/graphql/common/enums' export enum AbilityScoreOrderField { NAME = 'name', FULL_NAME = 'full_name' } export const ABILITY_SCORE_SORT_FIELD_MAP: Record = { [AbilityScoreOrderField.NAME]: 'name', [AbilityScoreOrderField.FULL_NAME]: 'full_name' } registerEnumType(AbilityScoreOrderField, { name: 'AbilityScoreOrderField', description: 'Fields to sort Ability Scores by' }) @InputType() export class AbilityScoreOrder implements BaseOrderInterface { @Field(() => AbilityScoreOrderField) by!: AbilityScoreOrderField @Field(() => OrderByDirection) direction!: OrderByDirection @Field(() => AbilityScoreOrder, { nullable: true }) then_by?: AbilityScoreOrder } export const AbilityScoreOrderSchema: z.ZodType = z.lazy(() => z.object({ by: z.nativeEnum(AbilityScoreOrderField), direction: z.nativeEnum(OrderByDirection), then_by: AbilityScoreOrderSchema.optional() }) ) export const AbilityScoreArgsSchema = z.object({ ...BaseFilterArgsSchema.shape, full_name: z.string().optional(), order: AbilityScoreOrderSchema.optional() }) export const AbilityScoreIndexArgsSchema = BaseIndexArgsSchema @ArgsType() export class AbilityScoreArgs extends BaseFilterArgs { @Field(() => String, { nullable: true, description: 'Filter by ability score full name (case-insensitive, partial match)' }) full_name?: string @Field(() => AbilityScoreOrder, { nullable: true, description: 'Specify sorting order for ability scores.' }) order?: AbilityScoreOrder } ================================================ FILE: src/graphql/2014/resolvers/abilityScore/resolver.ts ================================================ import { Arg, Args, FieldResolver, Query, Resolver, Root } from 'type-graphql' import { buildSortPipeline } from '@/graphql/common/args' import { resolveMultipleReferences } from '@/graphql/utils/resolvers' import AbilityScoreModel, { AbilityScore } from '@/models/2014/abilityScore' import SkillModel, { Skill } from '@/models/2014/skill' import { escapeRegExp } from '@/util' import { ABILITY_SCORE_SORT_FIELD_MAP, AbilityScoreArgs, AbilityScoreArgsSchema, AbilityScoreIndexArgsSchema, AbilityScoreOrderField } from './args' @Resolver(AbilityScore) export class AbilityScoreResolver { @Query(() => [AbilityScore], { description: 'Gets all ability scores, optionally filtered by name and sorted.' }) async abilityScores( @Args(() => AbilityScoreArgs) args: AbilityScoreArgs ): Promise { const validatedArgs = AbilityScoreArgsSchema.parse(args) const query = AbilityScoreModel.find() const filters: Record[] = [] if (validatedArgs.name != null && validatedArgs.name !== '') { filters.push({ name: { $regex: new RegExp(escapeRegExp(validatedArgs.name), 'i') } }) } if (validatedArgs.full_name != null && validatedArgs.full_name !== '') { filters.push({ full_name: { $regex: new RegExp(escapeRegExp(validatedArgs.full_name), 'i') } }) } if (filters.length > 0) { query.where({ $and: filters }) } const sortQuery = buildSortPipeline({ order: validatedArgs.order, sortFieldMap: ABILITY_SCORE_SORT_FIELD_MAP, defaultSortField: AbilityScoreOrderField.NAME }) if (Object.keys(sortQuery).length > 0) { query.sort(sortQuery) } if (validatedArgs.skip) { query.skip(validatedArgs.skip) } if (validatedArgs.limit) { query.limit(validatedArgs.limit) } return query.lean() } @Query(() => AbilityScore, { nullable: true, description: 'Gets a single ability score by index.' }) async abilityScore(@Arg('index', () => String) indexInput: string): Promise { const { index } = AbilityScoreIndexArgsSchema.parse({ index: indexInput }) return AbilityScoreModel.findOne({ index }).lean() } @FieldResolver(() => [Skill]) async skills(@Root() abilityScore: AbilityScore): Promise { return resolveMultipleReferences(abilityScore.skills, SkillModel) } } ================================================ FILE: src/graphql/2014/resolvers/alignment/args.ts ================================================ import { ArgsType, Field, InputType, registerEnumType } from 'type-graphql' import { z } from 'zod' import { BaseFilterArgs, BaseFilterArgsSchema, BaseIndexArgsSchema, BaseOrderInterface } from '@/graphql/common/args' import { OrderByDirection } from '@/graphql/common/enums' export enum AlignmentOrderField { NAME = 'name' } export const ALIGNMENT_SORT_FIELD_MAP: Record = { [AlignmentOrderField.NAME]: 'name' } registerEnumType(AlignmentOrderField, { name: 'AlignmentOrderField', description: 'Fields to sort Alignments by' }) @InputType() export class AlignmentOrder implements BaseOrderInterface { @Field(() => AlignmentOrderField) by!: AlignmentOrderField @Field(() => OrderByDirection) direction!: OrderByDirection @Field(() => AlignmentOrder, { nullable: true }) then_by?: AlignmentOrder } export const AlignmentOrderSchema: z.ZodType = z.lazy(() => z.object({ by: z.nativeEnum(AlignmentOrderField), direction: z.nativeEnum(OrderByDirection), then_by: AlignmentOrderSchema.optional() }) ) export const AlignmentArgsSchema = z.object({ ...BaseFilterArgsSchema.shape, order: AlignmentOrderSchema.optional() }) export const AlignmentIndexArgsSchema = BaseIndexArgsSchema @ArgsType() export class AlignmentArgs extends BaseFilterArgs { @Field(() => AlignmentOrder, { nullable: true, description: 'Specify sorting order for alignments.' }) order?: AlignmentOrder } ================================================ FILE: src/graphql/2014/resolvers/alignment/resolver.ts ================================================ import { Arg, Args, Query, Resolver } from 'type-graphql' import { buildSortPipeline } from '@/graphql/common/args' import AlignmentModel, { Alignment } from '@/models/2014/alignment' import { escapeRegExp } from '@/util' import { ALIGNMENT_SORT_FIELD_MAP, AlignmentArgs, AlignmentArgsSchema, AlignmentIndexArgsSchema, AlignmentOrderField } from './args' @Resolver(Alignment) export class AlignmentResolver { @Query(() => [Alignment], { description: 'Gets all alignments, optionally filtered by name and sorted.' }) async alignments(@Args(() => AlignmentArgs) args: AlignmentArgs): Promise { const validatedArgs = AlignmentArgsSchema.parse(args) const query = AlignmentModel.find() if (validatedArgs.name != null && validatedArgs.name !== '') { query.where({ name: { $regex: new RegExp(escapeRegExp(validatedArgs.name), 'i') } }) } const sortQuery = buildSortPipeline({ order: validatedArgs.order, sortFieldMap: ALIGNMENT_SORT_FIELD_MAP, defaultSortField: AlignmentOrderField.NAME }) if (Object.keys(sortQuery).length > 0) { query.sort(sortQuery) } if (validatedArgs.skip) { query.skip(validatedArgs.skip) } if (validatedArgs.limit) { query.limit(validatedArgs.limit) } return query.lean() } @Query(() => Alignment, { nullable: true, description: 'Gets a single alignment by index.' }) async alignment(@Arg('index', () => String) indexInput: string): Promise { const { index } = AlignmentIndexArgsSchema.parse({ index: indexInput }) return AlignmentModel.findOne({ index }).lean() } } ================================================ FILE: src/graphql/2014/resolvers/background/args.ts ================================================ import { ArgsType, Field, InputType, registerEnumType } from 'type-graphql' import { z } from 'zod' import { BaseFilterArgs, BaseFilterArgsSchema, BaseIndexArgsSchema, BaseOrderInterface } from '@/graphql/common/args' import { OrderByDirection } from '@/graphql/common/enums' export enum BackgroundOrderField { NAME = 'name' } export const BACKGROUND_SORT_FIELD_MAP: Record = { [BackgroundOrderField.NAME]: 'name' } registerEnumType(BackgroundOrderField, { name: 'BackgroundOrderField', description: 'Fields to sort Backgrounds by' }) @InputType() export class BackgroundOrder implements BaseOrderInterface { @Field(() => BackgroundOrderField) by!: BackgroundOrderField @Field(() => OrderByDirection) direction!: OrderByDirection @Field(() => BackgroundOrder, { nullable: true }) then_by?: BackgroundOrder } export const BackgroundOrderSchema: z.ZodType = z.lazy(() => z.object({ by: z.nativeEnum(BackgroundOrderField), direction: z.nativeEnum(OrderByDirection), then_by: BackgroundOrderSchema.optional() }) ) export const BackgroundArgsSchema = z.object({ ...BaseFilterArgsSchema.shape, order: BackgroundOrderSchema.optional() }) export const BackgroundIndexArgsSchema = BaseIndexArgsSchema @ArgsType() export class BackgroundArgs extends BaseFilterArgs { @Field(() => BackgroundOrder, { nullable: true, description: 'Specify sorting order for backgrounds.' }) order?: BackgroundOrder } ================================================ FILE: src/graphql/2014/resolvers/background/resolver.ts ================================================ import { Arg, Args, FieldResolver, Query, Resolver, Root } from 'type-graphql' import { LanguageChoice } from '@/graphql/2014/common/choiceTypes' import { IdealChoice, IdealOption as ResolvedIdealOption } from '@/graphql/2014/types/backgroundTypes' import { StartingEquipmentChoice } from '@/graphql/2014/types/startingEquipment' import { resolveLanguageChoice } from '@/graphql/2014/utils/resolvers' import { resolveStartingEquipmentChoices } from '@/graphql/2014/utils/startingEquipmentResolver' import { buildSortPipeline } from '@/graphql/common/args' import { StringChoice } from '@/graphql/common/choiceTypes' import { resolveMultipleReferences, resolveSingleReference, resolveStringChoice } from '@/graphql/utils/resolvers' import AlignmentModel, { Alignment } from '@/models/2014/alignment' import BackgroundModel, { Background, EquipmentRef } from '@/models/2014/background' import EquipmentModel, { Equipment } from '@/models/2014/equipment' import ProficiencyModel, { Proficiency } from '@/models/2014/proficiency' import { Choice, IdealOption, OptionsArrayOptionSet } from '@/models/common/choice' import { escapeRegExp } from '@/util' import { BACKGROUND_SORT_FIELD_MAP, BackgroundArgs, BackgroundArgsSchema, BackgroundIndexArgsSchema, BackgroundOrderField } from './args' @Resolver(Background) export class BackgroundResolver { @Query(() => [Background], { description: 'Gets all backgrounds, optionally filtered by name and sorted by name.' }) async backgrounds(@Args(() => BackgroundArgs) args: BackgroundArgs): Promise { const validatedArgs = BackgroundArgsSchema.parse(args) const query = BackgroundModel.find() if (validatedArgs.name != null && validatedArgs.name !== '') { query.where({ name: { $regex: new RegExp(escapeRegExp(validatedArgs.name), 'i') } }) } const sortQuery = buildSortPipeline({ order: validatedArgs.order, sortFieldMap: BACKGROUND_SORT_FIELD_MAP, defaultSortField: BackgroundOrderField.NAME }) if (Object.keys(sortQuery).length > 0) { query.sort(sortQuery) } if (validatedArgs.skip) { query.skip(validatedArgs.skip) } if (validatedArgs.limit) { query.limit(validatedArgs.limit) } return query.lean() } @Query(() => Background, { nullable: true, description: 'Gets a single background by index.' }) async background(@Arg('index', () => String) indexInput: string): Promise { const { index } = BackgroundIndexArgsSchema.parse({ index: indexInput }) return BackgroundModel.findOne({ index }).lean() } @FieldResolver(() => [Proficiency], { nullable: true }) async starting_proficiencies(@Root() background: Background): Promise { return resolveMultipleReferences(background.starting_proficiencies, ProficiencyModel) } @FieldResolver(() => StringChoice, { nullable: true, description: 'Resolves the flaws choice for the background.' }) async flaws(@Root() background: Background): Promise { return resolveStringChoice(background.flaws as Choice) } @FieldResolver(() => StringChoice, { nullable: true, description: 'Resolves the bonds choice for the background.' }) async bonds(@Root() background: Background): Promise { return resolveStringChoice(background.bonds as Choice) } @FieldResolver(() => StringChoice, { nullable: true, description: 'Resolves the personality traits choice for the background.' }) async personality_traits(@Root() background: Background): Promise { return resolveStringChoice(background.personality_traits as Choice) } @FieldResolver(() => IdealChoice, { nullable: true, description: 'Resolves the ideals choice for the background.' }) async ideals(@Root() background: Background): Promise { const choiceData = background.ideals as Choice const optionSet = choiceData.from as OptionsArrayOptionSet const resolvedIdealOptions: ResolvedIdealOption[] = [] if (Array.isArray(optionSet.options)) { for (const option of optionSet.options) { const idealOption = option as IdealOption const resolvedAlignments = (await resolveMultipleReferences( idealOption.alignments, AlignmentModel )) as Alignment[] resolvedIdealOptions.push({ option_type: idealOption.option_type, desc: idealOption.desc, alignments: resolvedAlignments }) } } return { choose: choiceData.choose, type: choiceData.type, from: { option_set_type: optionSet.option_set_type, options: resolvedIdealOptions } } } @FieldResolver(() => LanguageChoice, { nullable: true, description: 'Resolves the language choices for the background.' }) async language_options(@Root() background: Background): Promise { return resolveLanguageChoice(background.language_options as Choice) } @FieldResolver(() => [StartingEquipmentChoice], { nullable: true, description: 'Resolves starting equipment choices for the background.' }) async starting_equipment_options( @Root() background: Background ): Promise { return resolveStartingEquipmentChoices(background.starting_equipment_options) } } @Resolver(EquipmentRef) export class EquipmentRefResolver { @FieldResolver(() => Equipment) async equipment(@Root() equipmentRef: EquipmentRef): Promise { return resolveSingleReference(equipmentRef.equipment, EquipmentModel) } } ================================================ FILE: src/graphql/2014/resolvers/class/args.ts ================================================ import { ArgsType, Field, InputType, registerEnumType } from 'type-graphql' import { z } from 'zod' import { BaseFilterArgs, BaseFilterArgsSchema, BaseIndexArgsSchema, BaseOrderInterface } from '@/graphql/common/args' import { OrderByDirection } from '@/graphql/common/enums' import { NumberFilterInput, NumberFilterInputSchema } from '@/graphql/common/inputs' export enum ClassOrderField { NAME = 'name', HIT_DIE = 'hit_die' } export const CLASS_SORT_FIELD_MAP: Record = { [ClassOrderField.NAME]: 'name', [ClassOrderField.HIT_DIE]: 'hit_die' } registerEnumType(ClassOrderField, { name: 'ClassOrderField', description: 'Fields to sort Classes by' }) @InputType() export class ClassOrder implements BaseOrderInterface { @Field(() => ClassOrderField) by!: ClassOrderField @Field(() => OrderByDirection) direction!: OrderByDirection @Field(() => ClassOrder, { nullable: true }) then_by?: ClassOrder } export const ClassOrderSchema: z.ZodType = z.lazy(() => z.object({ by: z.nativeEnum(ClassOrderField), direction: z.nativeEnum(OrderByDirection), then_by: ClassOrderSchema.optional() }) ) export const ClassArgsSchema = z.object({ ...BaseFilterArgsSchema.shape, hit_die: NumberFilterInputSchema.optional(), order: ClassOrderSchema.optional() }) export const ClassIndexArgsSchema = BaseIndexArgsSchema @ArgsType() export class ClassArgs extends BaseFilterArgs { @Field(() => NumberFilterInput, { nullable: true, description: 'Filter by hit die size. Allows exact match, list of values, or a range.' }) hit_die?: NumberFilterInput @Field(() => ClassOrder, { nullable: true, description: 'Specify sorting order for classes. Allows nested sorting.' }) order?: ClassOrder } ================================================ FILE: src/graphql/2014/resolvers/class/resolver.ts ================================================ import { Arg, Args, FieldResolver, Query, Resolver, Root } from 'type-graphql' import { PrerequisiteChoice, PrerequisiteChoiceOption, PrerequisiteChoiceOptionSet, ProficiencyChoice } from '@/graphql/2014/common/choiceTypes' import { AnyEquipment } from '@/graphql/2014/common/unions' import { StartingEquipmentChoice } from '@/graphql/2014/types/startingEquipment' import { resolveProficiencyChoiceArray } from '@/graphql/2014/utils/resolvers' import { resolveStartingEquipmentChoices } from '@/graphql/2014/utils/startingEquipmentResolver' import { buildSortPipeline } from '@/graphql/common/args' import { buildMongoQueryFromNumberFilter } from '@/graphql/common/inputs' import { resolveMultipleReferences, resolveSingleReference } from '@/graphql/utils/resolvers' import AbilityScoreModel, { AbilityScore } from '@/models/2014/abilityScore' import ClassModel, { Class, ClassEquipment, MultiClassing, MultiClassingPrereq } from '@/models/2014/class' import EquipmentModel from '@/models/2014/equipment' import LevelModel, { Level } from '@/models/2014/level' import ProficiencyModel, { Proficiency } from '@/models/2014/proficiency' import SpellModel, { Spell } from '@/models/2014/spell' import SubclassModel, { Subclass } from '@/models/2014/subclass' import { APIReference } from '@/models/common/apiReference' import { Choice, OptionsArrayOptionSet, ScorePrerequisiteOption } from '@/models/common/choice' import { escapeRegExp } from '@/util' import { CLASS_SORT_FIELD_MAP, ClassArgs, ClassArgsSchema, ClassIndexArgsSchema, ClassOrderField } from './args' @Resolver(Class) export class ClassResolver { @Query(() => [Class], { description: 'Gets all classes, optionally filtering by name or hit die and sorted.' }) async classes(@Args(() => ClassArgs) args: ClassArgs): Promise { const validatedArgs = ClassArgsSchema.parse(args) const query = ClassModel.find() const filters: any[] = [] if (validatedArgs.name != null && validatedArgs.name !== '') { filters.push({ name: { $regex: new RegExp(escapeRegExp(validatedArgs.name), 'i') } }) } if (validatedArgs.hit_die) { const hitDieQuery = buildMongoQueryFromNumberFilter(validatedArgs.hit_die) if (hitDieQuery) { filters.push({ hit_die: hitDieQuery }) } } if (filters.length > 0) { query.where({ $and: filters }) } const sortQuery = buildSortPipeline({ order: validatedArgs.order, sortFieldMap: CLASS_SORT_FIELD_MAP, defaultSortField: ClassOrderField.NAME }) if (Object.keys(sortQuery).length > 0) { query.sort(sortQuery) } if (validatedArgs.skip) { query.skip(validatedArgs.skip) } if (validatedArgs.limit) { query.limit(validatedArgs.limit) } return query.lean() } @Query(() => Class, { nullable: true, description: 'Gets a single class by its index.' }) async class(@Arg('index', () => String) indexInput: string): Promise { const { index } = ClassIndexArgsSchema.parse({ index: indexInput }) return ClassModel.findOne({ index }).lean() } @FieldResolver(() => [Level]) async class_levels(@Root() classData: Class): Promise { return LevelModel.find({ 'class.index': classData.index, subclass: { $exists: false } }) .sort({ level: 1 }) .lean() } @FieldResolver(() => [Proficiency]) async proficiencies(@Root() classData: Class): Promise { return resolveMultipleReferences(classData.proficiencies, ProficiencyModel) } @FieldResolver(() => [AbilityScore]) async saving_throws(@Root() classData: Class): Promise { return resolveMultipleReferences(classData.saving_throws, AbilityScoreModel) } @FieldResolver(() => [Subclass]) async subclasses(@Root() classData: Class): Promise { return resolveMultipleReferences(classData.subclasses, SubclassModel) } @FieldResolver(() => [Spell]) async spells(@Root() classData: Class): Promise { return SpellModel.find({ 'classes.index': classData.index }).sort({ level: 1, name: 1 }).lean() } @FieldResolver(() => [ProficiencyChoice]) async proficiency_choices(@Root() classData: Class): Promise { return resolveProficiencyChoiceArray(classData.proficiency_choices) } @FieldResolver(() => [StartingEquipmentChoice], { nullable: true, description: 'Resolves starting equipment choices for the class.' }) async starting_equipment_options( @Root() classData: Class ): Promise { return resolveStartingEquipmentChoices(classData.starting_equipment_options) } } @Resolver(MultiClassing) export class MultiClassingResolver { @FieldResolver(() => [Proficiency]) async proficiencies(@Root() multiClassing: MultiClassing): Promise { return resolveMultipleReferences(multiClassing.proficiencies, ProficiencyModel) } @FieldResolver(() => [ProficiencyChoice]) async proficiency_choices(@Root() multiClassing: MultiClassing): Promise { return resolveProficiencyChoiceArray(multiClassing.proficiency_choices) } @FieldResolver(() => PrerequisiteChoice) async prerequisite_options( @Root() multiClassing: MultiClassing ): Promise { return resolvePrerequisiteChoice(multiClassing.prerequisite_options) } } @Resolver(MultiClassingPrereq) export class MultiClassingPrereqResolver { @FieldResolver(() => AbilityScore) async ability_score(@Root() prerequisite: MultiClassingPrereq): Promise { return resolveSingleReference(prerequisite.ability_score, AbilityScoreModel) } } @Resolver(ClassEquipment) export class ClassEquipmentResolver { @FieldResolver(() => AnyEquipment, { nullable: true }) async equipment(@Root() classEquipment: ClassEquipment): Promise { return resolveSingleReference(classEquipment.equipment, EquipmentModel) } } async function resolvePrerequisiteChoice( choiceData: Choice | undefined | null ): Promise { if (!choiceData || !choiceData.type || typeof choiceData.choose !== 'number') { return null } const gqlEmbeddedOptions: PrerequisiteChoiceOption[] = [] const optionsArraySet = choiceData.from as OptionsArrayOptionSet for (const opt of optionsArraySet.options) { if (opt.option_type === 'score_prerequisite') { const scoreOpt = opt as ScorePrerequisiteOption const abilityScore = await resolveSingleReference(scoreOpt.ability_score, AbilityScoreModel) if (abilityScore != null) { gqlEmbeddedOptions.push({ option_type: scoreOpt.option_type, ability_score: abilityScore as AbilityScore, minimum_score: scoreOpt.minimum_score }) } } } if (gqlEmbeddedOptions.length === 0 && optionsArraySet.options.length > 0) { return null } const gqlOptionSet: PrerequisiteChoiceOptionSet = { option_set_type: choiceData.from.option_set_type, options: gqlEmbeddedOptions } return { choose: choiceData.choose, type: choiceData.type, desc: choiceData.desc, from: gqlOptionSet } } ================================================ FILE: src/graphql/2014/resolvers/condition/args.ts ================================================ import { ArgsType, Field, InputType, registerEnumType } from 'type-graphql' import { z } from 'zod' import { BaseFilterArgs, BaseFilterArgsSchema, BaseIndexArgsSchema, BaseOrderInterface } from '@/graphql/common/args' import { OrderByDirection } from '@/graphql/common/enums' export enum ConditionOrderField { NAME = 'name' } export const CONDITION_SORT_FIELD_MAP: Record = { [ConditionOrderField.NAME]: 'name' } registerEnumType(ConditionOrderField, { name: 'ConditionOrderField', description: 'Fields to sort Conditions by' }) @InputType() export class ConditionOrder implements BaseOrderInterface { @Field(() => ConditionOrderField) by!: ConditionOrderField @Field(() => OrderByDirection) direction!: OrderByDirection @Field(() => ConditionOrder, { nullable: true }) then_by?: ConditionOrder } export const ConditionOrderSchema: z.ZodType = z.lazy(() => z.object({ by: z.nativeEnum(ConditionOrderField), direction: z.nativeEnum(OrderByDirection), then_by: ConditionOrderSchema.optional() }) ) export const ConditionArgsSchema = z.object({ ...BaseFilterArgsSchema.shape, order: ConditionOrderSchema.optional() }) export const ConditionIndexArgsSchema = BaseIndexArgsSchema @ArgsType() export class ConditionArgs extends BaseFilterArgs { @Field(() => ConditionOrder, { nullable: true, description: 'Specify sorting order for conditions.' }) order?: ConditionOrder } ================================================ FILE: src/graphql/2014/resolvers/condition/resolver.ts ================================================ import { Arg, Args, Query, Resolver } from 'type-graphql' import { buildSortPipeline } from '@/graphql/common/args' import ConditionModel, { Condition } from '@/models/2014/condition' import { escapeRegExp } from '@/util' import { CONDITION_SORT_FIELD_MAP, ConditionArgs, ConditionArgsSchema, ConditionIndexArgsSchema, ConditionOrderField } from './args' @Resolver(Condition) export class ConditionResolver { @Query(() => [Condition], { description: 'Gets all conditions, optionally filtered by name and sorted by name.' }) async conditions(@Args(() => ConditionArgs) args: ConditionArgs): Promise { const validatedArgs = ConditionArgsSchema.parse(args) const query = ConditionModel.find() if (validatedArgs.name != null && validatedArgs.name !== '') { query.where({ name: { $regex: new RegExp(escapeRegExp(validatedArgs.name), 'i') } }) } const sortQuery = buildSortPipeline({ order: validatedArgs.order, sortFieldMap: CONDITION_SORT_FIELD_MAP, defaultSortField: ConditionOrderField.NAME }) if (Object.keys(sortQuery).length > 0) { query.sort(sortQuery) } if (validatedArgs.skip) { query.skip(validatedArgs.skip) } if (validatedArgs.limit) { query.limit(validatedArgs.limit) } return query.lean() } @Query(() => Condition, { nullable: true, description: 'Gets a single condition by index.' }) async condition(@Arg('index', () => String) indexInput: string): Promise { const { index } = ConditionIndexArgsSchema.parse({ index: indexInput }) return ConditionModel.findOne({ index }).lean() } } ================================================ FILE: src/graphql/2014/resolvers/damageType/args.ts ================================================ import { ArgsType, Field, InputType, registerEnumType } from 'type-graphql' import { z } from 'zod' import { BaseFilterArgs, BaseFilterArgsSchema, BaseIndexArgsSchema, BaseOrderInterface } from '@/graphql/common/args' import { OrderByDirection } from '@/graphql/common/enums' export enum DamageTypeOrderField { NAME = 'name' } export const DAMAGE_TYPE_SORT_FIELD_MAP: Record = { [DamageTypeOrderField.NAME]: 'name' } registerEnumType(DamageTypeOrderField, { name: 'DamageTypeOrderField', description: 'Fields to sort Damage Types by' }) @InputType() export class DamageTypeOrder implements BaseOrderInterface { @Field(() => DamageTypeOrderField) by!: DamageTypeOrderField @Field(() => OrderByDirection) direction!: OrderByDirection @Field(() => DamageTypeOrder, { nullable: true }) then_by?: DamageTypeOrder } export const DamageTypeOrderSchema: z.ZodType = z.lazy(() => z.object({ by: z.nativeEnum(DamageTypeOrderField), direction: z.nativeEnum(OrderByDirection), then_by: DamageTypeOrderSchema.optional() }) ) export const DamageTypeArgsSchema = z.object({ ...BaseFilterArgsSchema.shape, order: DamageTypeOrderSchema.optional() }) export const DamageTypeIndexArgsSchema = BaseIndexArgsSchema @ArgsType() export class DamageTypeArgs extends BaseFilterArgs { @Field(() => DamageTypeOrder, { nullable: true, description: 'Specify sorting order for damage types.' }) order?: DamageTypeOrder } ================================================ FILE: src/graphql/2014/resolvers/damageType/resolver.ts ================================================ import { Arg, Args, Query, Resolver } from 'type-graphql' import { buildSortPipeline } from '@/graphql/common/args' import DamageTypeModel, { DamageType } from '@/models/2014/damageType' import { escapeRegExp } from '@/util' import { DAMAGE_TYPE_SORT_FIELD_MAP, DamageTypeArgs, DamageTypeArgsSchema, DamageTypeIndexArgsSchema, DamageTypeOrderField } from './args' @Resolver(DamageType) export class DamageTypeResolver { @Query(() => [DamageType], { description: 'Gets all damage types, optionally filtered by name and sorted by name.' }) async damageTypes(@Args(() => DamageTypeArgs) args: DamageTypeArgs): Promise { const validatedArgs = DamageTypeArgsSchema.parse(args) const query = DamageTypeModel.find() if (validatedArgs.name != null && validatedArgs.name !== '') { query.where({ name: { $regex: new RegExp(escapeRegExp(validatedArgs.name), 'i') } }) } const sortQuery = buildSortPipeline({ order: validatedArgs.order, sortFieldMap: DAMAGE_TYPE_SORT_FIELD_MAP, defaultSortField: DamageTypeOrderField.NAME }) if (Object.keys(sortQuery).length > 0) { query.sort(sortQuery) } if (validatedArgs.skip) { query.skip(validatedArgs.skip) } if (validatedArgs.limit) { query.limit(validatedArgs.limit) } return query.lean() } @Query(() => DamageType, { nullable: true, description: 'Gets a single damage type by index.' }) async damageType(@Arg('index', () => String) indexInput: string): Promise { const { index } = DamageTypeIndexArgsSchema.parse({ index: indexInput }) return DamageTypeModel.findOne({ index }).lean() } } ================================================ FILE: src/graphql/2014/resolvers/equipment/args.ts ================================================ import { ArgsType, Field, InputType, registerEnumType } from 'type-graphql' import { z } from 'zod' import { BaseFilterArgs, BaseFilterArgsSchema, BaseIndexArgsSchema, BaseOrderInterface } from '@/graphql/common/args' import { OrderByDirection } from '@/graphql/common/enums' export enum EquipmentOrderField { NAME = 'name', WEIGHT = 'weight', COST_QUANTITY = 'cost_quantity' } export const EQUIPMENT_SORT_FIELD_MAP: Record = { [EquipmentOrderField.NAME]: 'name', [EquipmentOrderField.WEIGHT]: 'weight', [EquipmentOrderField.COST_QUANTITY]: 'cost.quantity' } registerEnumType(EquipmentOrderField, { name: 'EquipmentOrderField', description: 'Fields to sort Equipment by' }) @InputType() export class EquipmentOrder implements BaseOrderInterface { @Field(() => EquipmentOrderField) by!: EquipmentOrderField @Field(() => OrderByDirection) direction!: OrderByDirection @Field(() => EquipmentOrder, { nullable: true }) then_by?: EquipmentOrder } export const EquipmentOrderSchema: z.ZodType = z.lazy(() => z.object({ by: z.nativeEnum(EquipmentOrderField), direction: z.nativeEnum(OrderByDirection), then_by: EquipmentOrderSchema.optional() // Simplified }) ) export const EquipmentArgsSchema = z.object({ ...BaseFilterArgsSchema.shape, equipment_category: z.array(z.string()).optional(), order: EquipmentOrderSchema.optional() }) export const EquipmentIndexArgsSchema = BaseIndexArgsSchema @ArgsType() export class EquipmentArgs extends BaseFilterArgs { @Field(() => [String], { nullable: true, description: 'Filter by one or more equipment category indices (e.g., ["weapon", "armor"])' }) equipment_category?: string[] @Field(() => EquipmentOrder, { nullable: true, description: 'Specify sorting order for equipment.' }) order?: EquipmentOrder } ================================================ FILE: src/graphql/2014/resolvers/equipment/resolver.ts ================================================ import { Arg, Args, FieldResolver, Query, Resolver, Root } from 'type-graphql' import { AnyEquipment } from '@/graphql/2014/common/unions' import { buildSortPipeline } from '@/graphql/common/args' import { resolveMultipleReferences, resolveSingleReference } from '@/graphql/utils/resolvers' import EquipmentModel, { Content, Equipment } from '@/models/2014/equipment' import WeaponPropertyModel, { WeaponProperty } from '@/models/2014/weaponProperty' import { APIReference } from '@/models/common/apiReference' import { escapeRegExp } from '@/util' import { EQUIPMENT_SORT_FIELD_MAP, EquipmentArgs, EquipmentArgsSchema, EquipmentIndexArgsSchema, EquipmentOrderField } from './args' @Resolver(Equipment) export class EquipmentResolver { @Query(() => [AnyEquipment], { description: 'Gets all equipment, optionally filtered and sorted.' }) async equipments( @Args(() => EquipmentArgs) args: EquipmentArgs ): Promise> { const validatedArgs = EquipmentArgsSchema.parse(args) const query = EquipmentModel.find() const filters: any[] = [] if (validatedArgs.name != null && validatedArgs.name !== '') { filters.push({ name: { $regex: new RegExp(escapeRegExp(validatedArgs.name), 'i') } }) } if (validatedArgs.equipment_category && validatedArgs.equipment_category.length > 0) { filters.push({ 'equipment_category.index': { $in: validatedArgs.equipment_category } }) } if (filters.length > 0) { query.where({ $and: filters }) } const sortQuery = buildSortPipeline({ order: validatedArgs.order, sortFieldMap: EQUIPMENT_SORT_FIELD_MAP, defaultSortField: EquipmentOrderField.NAME }) if (Object.keys(sortQuery).length > 0) { query.sort(sortQuery) } if (validatedArgs.skip) { query.skip(validatedArgs.skip) } if (validatedArgs.limit) { query.limit(validatedArgs.limit) } return query.lean() } @Query(() => AnyEquipment, { nullable: true, description: 'Gets a single piece of equipment by its index.' }) async equipment( @Arg('index', () => String) indexInput: string ): Promise { const { index } = EquipmentIndexArgsSchema.parse({ index: indexInput }) return EquipmentModel.findOne({ index }).lean() } @FieldResolver(() => [WeaponProperty], { nullable: true }) async properties(@Root() equipment: Equipment): Promise { if (!equipment.properties) return null return resolveMultipleReferences(equipment.properties, WeaponPropertyModel) } } @Resolver(Content) export class ContentFieldResolver { @FieldResolver(() => AnyEquipment, { nullable: true, description: 'Resolves the APIReference to the actual Equipment.' }) async item(@Root() content: Content): Promise { const itemRef: APIReference = content.item if (!itemRef?.index) return null return resolveSingleReference(itemRef, EquipmentModel) } } ================================================ FILE: src/graphql/2014/resolvers/equipmentCategory/args.ts ================================================ import { ArgsType, Field, InputType, registerEnumType } from 'type-graphql' import { z } from 'zod' import { BaseFilterArgs, BaseFilterArgsSchema, BaseIndexArgsSchema, BaseOrderInterface } from '@/graphql/common/args' import { OrderByDirection } from '@/graphql/common/enums' export enum EquipmentCategoryOrderField { NAME = 'name' } export const EQUIPMENT_CATEGORY_SORT_FIELD_MAP: Record = { [EquipmentCategoryOrderField.NAME]: 'name' } registerEnumType(EquipmentCategoryOrderField, { name: 'EquipmentCategoryOrderField', description: 'Fields to sort Equipment Categories by' }) @InputType() export class EquipmentCategoryOrder implements BaseOrderInterface { @Field(() => EquipmentCategoryOrderField) by!: EquipmentCategoryOrderField @Field(() => OrderByDirection) direction!: OrderByDirection @Field(() => EquipmentCategoryOrder, { nullable: true }) then_by?: EquipmentCategoryOrder } export const EquipmentCategoryOrderSchema: z.ZodType = z.lazy(() => z.object({ by: z.nativeEnum(EquipmentCategoryOrderField), direction: z.nativeEnum(OrderByDirection), then_by: EquipmentCategoryOrderSchema.optional() }) ) export const EquipmentCategoryArgsSchema = z.object({ ...BaseFilterArgsSchema.shape, order: EquipmentCategoryOrderSchema.optional() }) export const EquipmentCategoryIndexArgsSchema = BaseIndexArgsSchema @ArgsType() export class EquipmentCategoryArgs extends BaseFilterArgs { @Field(() => EquipmentCategoryOrder, { nullable: true, description: 'Specify sorting order for equipment categories.' }) order?: EquipmentCategoryOrder } ================================================ FILE: src/graphql/2014/resolvers/equipmentCategory/resolver.ts ================================================ import { Arg, Args, FieldResolver, Query, Resolver, Root } from 'type-graphql' import { EquipmentOrMagicItem } from '@/graphql/2014/common/unions' import { buildSortPipeline } from '@/graphql/common/args' import EquipmentModel, { Equipment } from '@/models/2014/equipment' import EquipmentCategoryModel, { EquipmentCategory } from '@/models/2014/equipmentCategory' import MagicItemModel, { MagicItem } from '@/models/2014/magicItem' import { escapeRegExp } from '@/util' import { EQUIPMENT_CATEGORY_SORT_FIELD_MAP, EquipmentCategoryArgs, EquipmentCategoryArgsSchema, EquipmentCategoryIndexArgsSchema, EquipmentCategoryOrderField } from './args' @Resolver(EquipmentCategory) export class EquipmentCategoryResolver { @Query(() => [EquipmentCategory], { description: 'Gets all equipment categories, optionally filtered by name and sorted by name.' }) async equipmentCategories( @Args(() => EquipmentCategoryArgs) args: EquipmentCategoryArgs ): Promise { const validatedArgs = EquipmentCategoryArgsSchema.parse(args) const query = EquipmentCategoryModel.find() if (validatedArgs.name != null && validatedArgs.name !== '') { query.where({ name: { $regex: new RegExp(escapeRegExp(validatedArgs.name), 'i') } }) } const sortQuery = buildSortPipeline({ order: validatedArgs.order, sortFieldMap: EQUIPMENT_CATEGORY_SORT_FIELD_MAP, defaultSortField: EquipmentCategoryOrderField.NAME }) if (Object.keys(sortQuery).length > 0) { query.sort(sortQuery) } if (validatedArgs.skip) { query.skip(validatedArgs.skip) } if (validatedArgs.limit) { query.limit(validatedArgs.limit) } return query.lean() } @Query(() => EquipmentCategory, { nullable: true, description: 'Gets a single equipment category by index.' }) async equipmentCategory( @Arg('index', () => String) indexInput: string ): Promise { const { index } = EquipmentCategoryIndexArgsSchema.parse({ index: indexInput }) return EquipmentCategoryModel.findOne({ index }).lean() } @FieldResolver(() => [EquipmentOrMagicItem]) async equipment( @Root() equipmentCategory: EquipmentCategory ): Promise<(Equipment | MagicItem)[]> { if (equipmentCategory.equipment.length === 0) { return [] } const equipmentIndices = equipmentCategory.equipment.map((ref) => ref.index) // Fetch both Equipment and MagicItems matching the indices const [equipments, magicItems] = await Promise.all([ EquipmentModel.find({ index: { $in: equipmentIndices } }).lean(), MagicItemModel.find({ index: { $in: equipmentIndices } }).lean() ]) return [...equipments, ...magicItems] } } ================================================ FILE: src/graphql/2014/resolvers/feat/args.ts ================================================ import { ArgsType, Field, InputType, registerEnumType } from 'type-graphql' import { z } from 'zod' import { BaseFilterArgs, BaseFilterArgsSchema, BaseIndexArgsSchema, BaseOrderInterface } from '@/graphql/common/args' import { OrderByDirection } from '@/graphql/common/enums' export enum FeatOrderField { NAME = 'name' } export const FEAT_SORT_FIELD_MAP: Record = { [FeatOrderField.NAME]: 'name' } registerEnumType(FeatOrderField, { name: 'FeatOrderField', description: 'Fields to sort Feats by' }) @InputType() export class FeatOrder implements BaseOrderInterface { @Field(() => FeatOrderField) by!: FeatOrderField @Field(() => OrderByDirection) direction!: OrderByDirection @Field(() => FeatOrder, { nullable: true }) then_by?: FeatOrder } export const FeatOrderSchema: z.ZodType = z.lazy(() => z.object({ by: z.nativeEnum(FeatOrderField), direction: z.nativeEnum(OrderByDirection), then_by: FeatOrderSchema.optional() }) ) export const FeatArgsSchema = z.object({ ...BaseFilterArgsSchema.shape, order: FeatOrderSchema.optional() }) export const FeatIndexArgsSchema = BaseIndexArgsSchema @ArgsType() export class FeatArgs extends BaseFilterArgs { @Field(() => FeatOrder, { nullable: true, description: 'Specify sorting order for feats.' }) order?: FeatOrder } ================================================ FILE: src/graphql/2014/resolvers/feat/resolver.ts ================================================ import { Arg, Args, FieldResolver, Query, Resolver, Root } from 'type-graphql' import { buildSortPipeline } from '@/graphql/common/args' import { resolveSingleReference } from '@/graphql/utils/resolvers' import AbilityScoreModel, { AbilityScore } from '@/models/2014/abilityScore' import FeatModel, { Feat, Prerequisite } from '@/models/2014/feat' import { escapeRegExp } from '@/util' import { FEAT_SORT_FIELD_MAP, FeatArgs, FeatArgsSchema, FeatIndexArgsSchema, FeatOrderField } from './args' @Resolver(Feat) export class FeatResolver { @Query(() => [Feat], { description: 'Gets all feats, optionally filtered by name and sorted by name.' }) async feats(@Args(() => FeatArgs) args: FeatArgs): Promise { const validatedArgs = FeatArgsSchema.parse(args) const query = FeatModel.find() if (validatedArgs.name != null && validatedArgs.name !== '') { query.where({ name: { $regex: new RegExp(escapeRegExp(validatedArgs.name), 'i') } }) } const sortQuery = buildSortPipeline({ order: validatedArgs.order, sortFieldMap: FEAT_SORT_FIELD_MAP, defaultSortField: FeatOrderField.NAME }) if (Object.keys(sortQuery).length > 0) { query.sort(sortQuery) } if (validatedArgs.skip) { query.skip(validatedArgs.skip) } if (validatedArgs.limit) { query.limit(validatedArgs.limit) } return query.lean() } @Query(() => Feat, { nullable: true, description: 'Gets a single feat by index.' }) async feat(@Arg('index', () => String) indexInput: string): Promise { const { index } = FeatIndexArgsSchema.parse({ index: indexInput }) return FeatModel.findOne({ index }).lean() } } @Resolver(Prerequisite) export class PrerequisiteResolver { @FieldResolver(() => AbilityScore, { nullable: true }) async ability_score(@Root() prerequisite: Prerequisite): Promise { return resolveSingleReference(prerequisite.ability_score, AbilityScoreModel) } } ================================================ FILE: src/graphql/2014/resolvers/feature/args.ts ================================================ import { ArgsType, Field, InputType, registerEnumType } from 'type-graphql' import { z } from 'zod' import { BaseFilterArgs, BaseFilterArgsSchema, BaseIndexArgsSchema, BaseOrderInterface } from '@/graphql/common/args' import { OrderByDirection } from '@/graphql/common/enums' import { NumberFilterInput, NumberFilterInputSchema } from '@/graphql/common/inputs' export enum FeatureOrderField { NAME = 'name', LEVEL = 'level', CLASS = 'class', SUBCLASS = 'subclass' } export const FEATURE_SORT_FIELD_MAP: Record = { [FeatureOrderField.NAME]: 'name', [FeatureOrderField.LEVEL]: 'level', [FeatureOrderField.CLASS]: 'class.name', [FeatureOrderField.SUBCLASS]: 'subclass.name' } registerEnumType(FeatureOrderField, { name: 'FeatureOrderField', description: 'Fields to sort Features by' }) @InputType() export class FeatureOrder implements BaseOrderInterface { @Field(() => FeatureOrderField) by!: FeatureOrderField @Field(() => OrderByDirection) direction!: OrderByDirection @Field(() => FeatureOrder, { nullable: true }) then_by?: FeatureOrder } export const FeatureOrderSchema: z.ZodType = z.lazy(() => z.object({ by: z.nativeEnum(FeatureOrderField), direction: z.nativeEnum(OrderByDirection), then_by: FeatureOrderSchema.optional() }) ) export const FeatureArgsSchema = z.object({ ...BaseFilterArgsSchema.shape, level: NumberFilterInputSchema.optional(), class: z.array(z.string()).optional(), subclass: z.array(z.string()).optional(), order: FeatureOrderSchema.optional() }) export const FeatureIndexArgsSchema = BaseIndexArgsSchema @ArgsType() export class FeatureArgs extends BaseFilterArgs { @Field(() => NumberFilterInput, { nullable: true, description: 'Filter by level. Allows exact match, list, or range.' }) level?: NumberFilterInput @Field(() => [String], { nullable: true, description: 'Filter by one or more associated class indices' }) class?: string[] @Field(() => [String], { nullable: true, description: 'Filter by one or more associated subclass indices' }) subclass?: string[] @Field(() => FeatureOrder, { nullable: true, description: 'Specify sorting order for features.' }) order?: FeatureOrder } ================================================ FILE: src/graphql/2014/resolvers/feature/resolver.ts ================================================ import { Arg, Args, FieldResolver, Query, Resolver, Root } from 'type-graphql' import { FeaturePrerequisiteUnion } from '@/graphql/2014/types/featureTypes' import { buildSortPipeline } from '@/graphql/common/args' import { buildMongoQueryFromNumberFilter } from '@/graphql/common/inputs' import { resolveMultipleReferences, resolveSingleReference } from '@/graphql/utils/resolvers' import ClassModel, { Class } from '@/models/2014/class' import FeatureModel, { Feature, FeaturePrerequisite, FeatureSpecific, LevelPrerequisite, SpellPrerequisite } from '@/models/2014/feature' import SpellModel from '@/models/2014/spell' import SubclassModel, { Subclass } from '@/models/2014/subclass' import { escapeRegExp } from '@/util' import { FEATURE_SORT_FIELD_MAP, FeatureArgs, FeatureArgsSchema, FeatureIndexArgsSchema, FeatureOrderField } from './args' @Resolver(Feature) export class FeatureResolver { @Query(() => [Feature], { description: 'Gets all features, optionally filtered and sorted.' }) async features(@Args(() => FeatureArgs) args: FeatureArgs): Promise { const validatedArgs = FeatureArgsSchema.parse(args) const query = FeatureModel.find() const filters: any[] = [] if (validatedArgs.name != null && validatedArgs.name !== '') { filters.push({ name: { $regex: new RegExp(escapeRegExp(validatedArgs.name), 'i') } }) } if (validatedArgs.level) { const levelQuery = buildMongoQueryFromNumberFilter(validatedArgs.level) if (levelQuery) { filters.push({ level: levelQuery }) } } if (validatedArgs.class && validatedArgs.class.length > 0) { filters.push({ 'class.index': { $in: validatedArgs.class } }) } if (validatedArgs.subclass && validatedArgs.subclass.length > 0) { filters.push({ 'subclass.index': { $in: validatedArgs.subclass } }) } if (filters.length > 0) { query.where({ $and: filters }) } const sortQuery = buildSortPipeline({ order: validatedArgs.order, sortFieldMap: FEATURE_SORT_FIELD_MAP, defaultSortField: FeatureOrderField.NAME }) if (Object.keys(sortQuery).length > 0) { query.sort(sortQuery) } if (validatedArgs.skip) { query.skip(validatedArgs.skip) } if (validatedArgs.limit) { query.limit(validatedArgs.limit) } return query.lean() } @Query(() => Feature, { nullable: true, description: 'Gets a single feature by its index.' }) async feature(@Arg('index', () => String) indexInput: string): Promise { const { index } = FeatureIndexArgsSchema.parse({ index: indexInput }) return FeatureModel.findOne({ index }).lean() } @FieldResolver(() => Class, { nullable: true }) async class(@Root() feature: Feature): Promise { return resolveSingleReference(feature.class, ClassModel) } @FieldResolver(() => Feature, { nullable: true }) async parent(@Root() feature: Feature): Promise { return resolveSingleReference(feature.parent, FeatureModel) } @FieldResolver(() => Subclass, { nullable: true }) async subclass(@Root() feature: Feature): Promise { return resolveSingleReference(feature.subclass, SubclassModel) } @FieldResolver(() => [FeaturePrerequisiteUnion], { nullable: true, description: 'Resolves the prerequisites array, fetching referenced Features or Spells.' }) async prerequisites( @Root() feature: Feature ): Promise | null> { const prereqsData = feature.prerequisites if (!prereqsData || prereqsData.length === 0) { return null } const resolvedPrereqsPromises = prereqsData.map( async ( prereq ): Promise => { switch (prereq.type) { case 'level': { return prereq as LevelPrerequisite } case 'feature': { const featureUrl = (prereq as FeaturePrerequisite).feature const referencedFeature = await FeatureModel.findOne({ url: featureUrl }).lean() if (referencedFeature) { return { type: 'feature', feature: referencedFeature } as unknown as FeaturePrerequisite } else { console.warn(`Could not find prerequisite feature with url: ${featureUrl}`) return null } } case 'spell': { const spellUrl = (prereq as SpellPrerequisite).spell const referencedSpell = await SpellModel.findOne({ url: spellUrl }).lean() if (referencedSpell) { return { type: 'spell', spell: referencedSpell } as unknown as SpellPrerequisite } else { console.warn(`Could not find prerequisite spell with index: ${spellUrl}`) return null } } default: { console.warn(`Unknown prerequisite type found: ${prereq.type}`) return null } } } ) const resolvedPrereqs = (await Promise.all(resolvedPrereqsPromises)).filter( (p) => p !== null ) as Array return resolvedPrereqs.length > 0 ? resolvedPrereqs : null } } @Resolver(FeatureSpecific) export class FeatureSpecificResolver { @FieldResolver(() => [Feature], { nullable: true }) async invocations(@Root() featureSpecific: FeatureSpecific): Promise { return resolveMultipleReferences(featureSpecific.invocations, FeatureModel) } } ================================================ FILE: src/graphql/2014/resolvers/index.ts ================================================ // This file will export an array of all resolver classes import { AbilityScoreResolver } from './abilityScore/resolver' import { AlignmentResolver } from './alignment/resolver' import { BackgroundResolver, EquipmentRefResolver } from './background/resolver' import { ClassEquipmentResolver, ClassResolver, MultiClassingPrereqResolver, MultiClassingResolver } from './class/resolver' import { ConditionResolver } from './condition/resolver' import { DamageTypeResolver } from './damageType/resolver' import { ContentFieldResolver, EquipmentResolver } from './equipment/resolver' import { EquipmentCategoryResolver } from './equipmentCategory/resolver' import { FeatResolver, PrerequisiteResolver } from './feat/resolver' import { FeatureResolver, FeatureSpecificResolver } from './feature/resolver' import { LanguageResolver } from './language/resolver' import { LevelResolver } from './level/resolver' import { MagicItemResolver } from './magicItem/resolver' import { MagicSchoolResolver } from './magicSchool/resolver' import { ArmorClassArmorResolver, ArmorClassConditionResolver, ArmorClassSpellResolver, DifficultyClassResolver, MonsterActionResolver, MonsterProficiencyResolver, MonsterResolver, SpecialAbilitySpellcastingResolver, SpecialAbilitySpellResolver } from './monster/resolver' import { ProficiencyResolver } from './proficiency/resolver' import { RaceAbilityBonusResolver, RaceResolver } from './race/resolver' import { RuleResolver } from './rule/resolver' import { RuleSectionResolver } from './ruleSection/resolver' import { SkillResolver } from './skill/resolver' import { SpellDamageResolver, SpellResolver, SpellDCResolver } from './spell/resolver' import { SubclassResolver, SubclassSpellResolver } from './subclass/resolver' import { SubraceAbilityBonusResolver, SubraceResolver } from './subrace/resolver' import { ActionDamageResolver, TraitResolver, TraitSpecificResolver } from './trait/resolver' import { WeaponPropertyResolver } from './weaponProperty/resolver' const collectionResolvers = [ AbilityScoreResolver, AlignmentResolver, BackgroundResolver, ClassResolver, ConditionResolver, DamageTypeResolver, EquipmentCategoryResolver, EquipmentResolver, FeatResolver, FeatureResolver, LanguageResolver, LevelResolver, MagicItemResolver, MagicSchoolResolver, MonsterResolver, ProficiencyResolver, RaceResolver, RuleResolver, RuleSectionResolver, SkillResolver, SpellResolver, SubclassResolver, SubraceResolver, TraitResolver, WeaponPropertyResolver ] as const const fieldResolvers = [ // Background EquipmentRefResolver, // Feat PrerequisiteResolver, // Trait TraitSpecificResolver, ActionDamageResolver, // Feature FeatureSpecificResolver, // Race RaceAbilityBonusResolver, // Subrace SubraceAbilityBonusResolver, // Class MultiClassingResolver, MultiClassingPrereqResolver, ClassEquipmentResolver, // Subclass SubclassSpellResolver, // Spell SpellDamageResolver, SpellDCResolver, // Equipment ContentFieldResolver, // Monster Field Resolvers ArmorClassArmorResolver, ArmorClassSpellResolver, ArmorClassConditionResolver, MonsterProficiencyResolver, SpecialAbilitySpellcastingResolver, SpecialAbilitySpellResolver, MonsterActionResolver, DifficultyClassResolver ] as const // Export a new mutable array combining the readonly ones export const resolvers = [...collectionResolvers, ...fieldResolvers] as const ================================================ FILE: src/graphql/2014/resolvers/language/args.ts ================================================ import { ArgsType, Field, InputType, registerEnumType } from 'type-graphql' import { z } from 'zod' import { BaseFilterArgs, BaseFilterArgsSchema, BaseIndexArgsSchema, BaseOrderInterface } from '@/graphql/common/args' import { OrderByDirection } from '@/graphql/common/enums' export enum LanguageOrderField { NAME = 'name', TYPE = 'type', SCRIPT = 'script' } export const LANGUAGE_SORT_FIELD_MAP: Record = { [LanguageOrderField.NAME]: 'name', [LanguageOrderField.TYPE]: 'type', [LanguageOrderField.SCRIPT]: 'script' } registerEnumType(LanguageOrderField, { name: 'LanguageOrderField', description: 'Fields to sort Languages by' }) @InputType() export class LanguageOrder implements BaseOrderInterface { @Field(() => LanguageOrderField) by!: LanguageOrderField @Field(() => OrderByDirection) direction!: OrderByDirection @Field(() => LanguageOrder, { nullable: true }) then_by?: LanguageOrder } export const LanguageOrderSchema: z.ZodType = z.lazy(() => z.object({ by: z.nativeEnum(LanguageOrderField), direction: z.nativeEnum(OrderByDirection), then_by: LanguageOrderSchema.optional() }) ) export const LanguageArgsSchema = z.object({ ...BaseFilterArgsSchema.shape, type: z.string().optional(), script: z.array(z.string()).optional(), order: LanguageOrderSchema.optional() }) export const LanguageIndexArgsSchema = BaseIndexArgsSchema @ArgsType() export class LanguageArgs extends BaseFilterArgs { @Field(() => String, { nullable: true, description: 'Filter by language type (e.g., Standard, Exotic) - case-insensitive exact match after normalization' }) type?: string @Field(() => [String], { nullable: true, description: 'Filter by one or more language scripts (e.g., ["Common", "Elvish"])' }) script?: string[] @Field(() => LanguageOrder, { nullable: true, description: 'Specify sorting order for languages.' }) order?: LanguageOrder } ================================================ FILE: src/graphql/2014/resolvers/language/resolver.ts ================================================ import { Arg, Args, Query, Resolver } from 'type-graphql' import { buildSortPipeline } from '@/graphql/common/args' import LanguageModel, { Language } from '@/models/2014/language' import { escapeRegExp } from '@/util' import { LANGUAGE_SORT_FIELD_MAP, LanguageArgs, LanguageArgsSchema, LanguageIndexArgsSchema, LanguageOrderField } from './args' @Resolver(Language) export class LanguageResolver { @Query(() => Language, { nullable: true, description: 'Gets a single language by its index.' }) async language(@Arg('index', () => String) indexInput: string): Promise { const { index } = LanguageIndexArgsSchema.parse({ index: indexInput }) return LanguageModel.findOne({ index }).lean() } @Query(() => [Language], { description: 'Gets all languages, optionally filtered and sorted.' }) async languages(@Args(() => LanguageArgs) args: LanguageArgs): Promise { const validatedArgs = LanguageArgsSchema.parse(args) const query = LanguageModel.find() const filters: any[] = [] if (validatedArgs.name != null && validatedArgs.name !== '') { filters.push({ name: { $regex: new RegExp(escapeRegExp(validatedArgs.name), 'i') } }) } if (validatedArgs.type != null && validatedArgs.type !== '') { filters.push({ type: { $regex: new RegExp(escapeRegExp(validatedArgs.type), 'i') } }) } if (validatedArgs.script && validatedArgs.script.length > 0) { filters.push({ script: { $in: validatedArgs.script } }) } if (filters.length > 0) { query.where({ $and: filters }) } const sortQuery = buildSortPipeline({ order: validatedArgs.order, sortFieldMap: LANGUAGE_SORT_FIELD_MAP, defaultSortField: LanguageOrderField.NAME }) if (Object.keys(sortQuery).length > 0) { query.sort(sortQuery) } if (validatedArgs.skip !== undefined) { query.skip(validatedArgs.skip) } if (validatedArgs.limit !== undefined) { query.limit(validatedArgs.limit) } return query.lean() } } ================================================ FILE: src/graphql/2014/resolvers/level/args.ts ================================================ import { ArgsType, Field, InputType, Int, registerEnumType } from 'type-graphql' import { z } from 'zod' import { BaseIndexArgsSchema, BaseOrderInterface, BasePaginationArgs, BasePaginationArgsSchema } from '@/graphql/common/args' import { OrderByDirection } from '@/graphql/common/enums' import { NumberFilterInput, NumberFilterInputSchema } from '@/graphql/common/inputs' export enum LevelOrderField { LEVEL = 'level', CLASS = 'class', SUBCLASS = 'subclass' } export const LEVEL_SORT_FIELD_MAP: Record = { [LevelOrderField.LEVEL]: 'level', [LevelOrderField.CLASS]: 'class.name', [LevelOrderField.SUBCLASS]: 'subclass.name' } registerEnumType(LevelOrderField, { name: 'LevelOrderField', description: 'Fields to sort Levels by' }) @InputType() export class LevelOrder implements BaseOrderInterface { @Field(() => LevelOrderField) by!: LevelOrderField @Field(() => OrderByDirection) direction!: OrderByDirection @Field(() => LevelOrder, { nullable: true }) then_by?: LevelOrder } export const LevelOrderSchema: z.ZodType = z.lazy(() => z.object({ by: z.nativeEnum(LevelOrderField), direction: z.nativeEnum(OrderByDirection), then_by: LevelOrderSchema.optional() }) ) export const LevelArgsSchema = z.object({ ...BasePaginationArgsSchema.shape, class: z.array(z.string()).optional(), subclass: z.array(z.string()).optional(), level: NumberFilterInputSchema.optional(), ability_score_bonuses: z.number().int().optional(), prof_bonus: z.number().int().optional(), order: LevelOrderSchema.optional() }) export const LevelIndexArgsSchema = BaseIndexArgsSchema @ArgsType() export class LevelArgs extends BasePaginationArgs { @Field(() => [String], { nullable: true, description: 'Filter by one or more class indices' }) class?: string[] @Field(() => [String], { nullable: true, description: 'Filter by one or more subclass indices' }) subclass?: string[] @Field(() => NumberFilterInput, { nullable: true, description: 'Filter by level. Allows exact match, list, or range.' }) level?: NumberFilterInput @Field(() => Int, { nullable: true, description: 'Filter by the exact number of ability score bonuses granted at this level.' }) ability_score_bonuses?: number @Field(() => Int, { nullable: true, description: 'Filter by the exact proficiency bonus at this level.' }) prof_bonus?: number @Field(() => LevelOrder, { nullable: true, description: 'Specify sorting order for levels. Allows nested sorting. Defaults to LEVEL ascending.' }) order?: LevelOrder } ================================================ FILE: src/graphql/2014/resolvers/level/resolver.ts ================================================ import { Arg, Args, FieldResolver, Query, Resolver, Root } from 'type-graphql' import { buildSortPipeline } from '@/graphql/common/args' import { buildMongoQueryFromNumberFilter } from '@/graphql/common/inputs' import { resolveMultipleReferences, resolveSingleReference } from '@/graphql/utils/resolvers' import ClassModel, { Class } from '@/models/2014/class' import FeatureModel, { Feature } from '@/models/2014/feature' import LevelModel, { Level } from '@/models/2014/level' import SubclassModel, { Subclass } from '@/models/2014/subclass' import { LEVEL_SORT_FIELD_MAP, LevelArgs, LevelArgsSchema, LevelIndexArgsSchema, LevelOrderField } from './args' @Resolver(Level) export class LevelResolver { @Query(() => Level, { nullable: true, description: 'Gets a single level by its combined index (e.g., wizard-3-evocation or fighter-5).' }) async level(@Arg('index', () => String) indexInput: string): Promise { const { index } = LevelIndexArgsSchema.parse({ index: indexInput }) return LevelModel.findOne({ index }).lean() } @Query(() => [Level], { description: 'Gets all levels, optionally filtered and sorted.' }) async levels(@Args(() => LevelArgs) args: LevelArgs): Promise { const validatedArgs = LevelArgsSchema.parse(args) let query = LevelModel.find() const filters: any[] = [] if (validatedArgs.class && validatedArgs.class.length > 0) { filters.push({ 'class.index': { $in: validatedArgs.class } }) } if (validatedArgs.subclass && validatedArgs.subclass.length > 0) { filters.push({ 'subclass.index': { $in: validatedArgs.subclass } }) } if (validatedArgs.level) { const levelQuery = buildMongoQueryFromNumberFilter(validatedArgs.level) if (levelQuery) { filters.push({ level: levelQuery }) } } if (validatedArgs.ability_score_bonuses != null) { filters.push({ ability_score_bonuses: validatedArgs.ability_score_bonuses }) } if (validatedArgs.prof_bonus != null) { filters.push({ prof_bonus: validatedArgs.prof_bonus }) } if (filters.length > 0) { query = query.where({ $and: filters }) } const sortQuery = buildSortPipeline({ order: validatedArgs.order, sortFieldMap: LEVEL_SORT_FIELD_MAP, defaultSortField: LevelOrderField.LEVEL }) if (Object.keys(sortQuery).length > 0) { query = query.sort(sortQuery) } if (validatedArgs.skip !== undefined) { query = query.skip(validatedArgs.skip) } if (validatedArgs.limit !== undefined) { query = query.limit(validatedArgs.limit) } return query.lean() } @FieldResolver(() => Class, { nullable: true }) async class(@Root() level: Level): Promise { return resolveSingleReference(level.class, ClassModel) } @FieldResolver(() => Subclass, { nullable: true }) async subclass(@Root() level: Level): Promise { return resolveSingleReference(level.subclass, SubclassModel) } @FieldResolver(() => [Feature]) async features(@Root() level: Level): Promise { return resolveMultipleReferences(level.features, FeatureModel) } } ================================================ FILE: src/graphql/2014/resolvers/magicItem/args.ts ================================================ import { ArgsType, Field, InputType, registerEnumType } from 'type-graphql' import { z } from 'zod' import { BaseFilterArgs, BaseFilterArgsSchema, BaseIndexArgsSchema, BaseOrderInterface } from '@/graphql/common/args' import { OrderByDirection } from '@/graphql/common/enums' export enum MagicItemOrderField { NAME = 'name', EQUIPMENT_CATEGORY = 'equipment_category', RARITY = 'rarity' } export const MAGIC_ITEM_SORT_FIELD_MAP: Record = { [MagicItemOrderField.NAME]: 'name', [MagicItemOrderField.EQUIPMENT_CATEGORY]: 'equipment_category.name', [MagicItemOrderField.RARITY]: 'rarity.name' } registerEnumType(MagicItemOrderField, { name: 'MagicItemOrderField', description: 'Fields to sort Magic Items by' }) @InputType() export class MagicItemOrder implements BaseOrderInterface { @Field(() => MagicItemOrderField) by!: MagicItemOrderField @Field(() => OrderByDirection) direction!: OrderByDirection @Field(() => MagicItemOrder, { nullable: true }) then_by?: MagicItemOrder } export const MagicItemOrderSchema: z.ZodType = z.lazy(() => z.object({ by: z.nativeEnum(MagicItemOrderField), direction: z.nativeEnum(OrderByDirection), then_by: MagicItemOrderSchema.optional() }) ) export const MagicItemArgsSchema = z.object({ ...BaseFilterArgsSchema.shape, equipment_category: z.array(z.string()).optional(), rarity: z.array(z.string()).optional(), order: MagicItemOrderSchema.optional() }) export const MagicItemIndexArgsSchema = BaseIndexArgsSchema @ArgsType() export class MagicItemArgs extends BaseFilterArgs { @Field(() => [String], { nullable: true, description: 'Filter by one or more equipment category indices (e.g., ["armor", "weapon"])' }) equipment_category?: string[] @Field(() => [String], { nullable: true, description: 'Filter by one or more rarity names (e.g., ["Rare", "Legendary"])' }) rarity?: string[] @Field(() => MagicItemOrder, { nullable: true, description: 'Specify sorting order for magic items.' }) order?: MagicItemOrder } ================================================ FILE: src/graphql/2014/resolvers/magicItem/resolver.ts ================================================ import { Arg, Args, FieldResolver, Query, Resolver, Root } from 'type-graphql' import { buildSortPipeline } from '@/graphql/common/args' import { resolveMultipleReferences, resolveSingleReference } from '@/graphql/utils/resolvers' import EquipmentCategoryModel, { EquipmentCategory } from '@/models/2014/equipmentCategory' import MagicItemModel, { MagicItem } from '@/models/2014/magicItem' import { escapeRegExp } from '@/util' import { MAGIC_ITEM_SORT_FIELD_MAP, MagicItemArgs, MagicItemArgsSchema, MagicItemIndexArgsSchema, MagicItemOrderField } from './args' @Resolver(MagicItem) export class MagicItemResolver { @Query(() => [MagicItem], { description: 'Gets all magic items, optionally filtered and sorted.' }) async magicItems(@Args(() => MagicItemArgs) args: MagicItemArgs): Promise { const validatedArgs = MagicItemArgsSchema.parse(args) let query = MagicItemModel.find() const filters: any[] = [] if (validatedArgs.name != null && validatedArgs.name !== '') { filters.push({ name: { $regex: new RegExp(escapeRegExp(validatedArgs.name), 'i') } }) } if (validatedArgs.equipment_category && validatedArgs.equipment_category.length > 0) { filters.push({ 'equipment_category.index': { $in: validatedArgs.equipment_category } }) } if (validatedArgs.rarity && validatedArgs.rarity.length > 0) { filters.push({ 'rarity.name': { $in: validatedArgs.rarity } }) } if (filters.length > 0) { query = query.where({ $and: filters }) } const sortQuery = buildSortPipeline({ order: validatedArgs.order, sortFieldMap: MAGIC_ITEM_SORT_FIELD_MAP, defaultSortField: MagicItemOrderField.NAME }) if (Object.keys(sortQuery).length > 0) { query = query.sort(sortQuery) } if (validatedArgs.skip !== undefined) { query = query.skip(validatedArgs.skip) } if (validatedArgs.limit !== undefined) { query = query.limit(validatedArgs.limit) } return query.lean() } @Query(() => MagicItem, { nullable: true, description: 'Gets a single magic item by index.' }) async magicItem(@Arg('index', () => String) indexInput: string): Promise { const { index } = MagicItemIndexArgsSchema.parse({ index: indexInput }) return MagicItemModel.findOne({ index }).lean() } @FieldResolver(() => EquipmentCategory, { nullable: true }) async equipment_category(@Root() magicItem: MagicItem): Promise { return resolveSingleReference(magicItem.equipment_category, EquipmentCategoryModel) } @FieldResolver(() => [MagicItem], { nullable: true }) async variants(@Root() magicItem: MagicItem): Promise { return resolveMultipleReferences(magicItem.variants, MagicItemModel) } } ================================================ FILE: src/graphql/2014/resolvers/magicSchool/args.ts ================================================ import { ArgsType, Field, InputType, registerEnumType } from 'type-graphql' import { z } from 'zod' import { BaseFilterArgs, BaseFilterArgsSchema, BaseIndexArgsSchema, BaseOrderInterface } from '@/graphql/common/args' import { OrderByDirection } from '@/graphql/common/enums' export enum MagicSchoolOrderField { NAME = 'name' } export const MAGIC_SCHOOL_SORT_FIELD_MAP: Record = { [MagicSchoolOrderField.NAME]: 'name' } registerEnumType(MagicSchoolOrderField, { name: 'MagicSchoolOrderField', description: 'Fields to sort Magic Schools by' }) @InputType() export class MagicSchoolOrder implements BaseOrderInterface { @Field(() => MagicSchoolOrderField) by!: MagicSchoolOrderField @Field(() => OrderByDirection) direction!: OrderByDirection @Field(() => MagicSchoolOrder, { nullable: true }) then_by?: MagicSchoolOrder } export const MagicSchoolOrderSchema: z.ZodType = z.lazy(() => z.object({ by: z.nativeEnum(MagicSchoolOrderField), direction: z.nativeEnum(OrderByDirection), then_by: MagicSchoolOrderSchema.optional() }) ) export const MagicSchoolArgsSchema = z.object({ ...BaseFilterArgsSchema.shape, order: MagicSchoolOrderSchema.optional() }) export const MagicSchoolIndexArgsSchema = BaseIndexArgsSchema @ArgsType() export class MagicSchoolArgs extends BaseFilterArgs { @Field(() => MagicSchoolOrder, { nullable: true, description: 'Specify sorting order for magic schools.' }) order?: MagicSchoolOrder } ================================================ FILE: src/graphql/2014/resolvers/magicSchool/resolver.ts ================================================ import { Arg, Args, Query, Resolver } from 'type-graphql' import { buildSortPipeline } from '@/graphql/common/args' import MagicSchoolModel, { MagicSchool } from '@/models/2014/magicSchool' import { escapeRegExp } from '@/util' import { MAGIC_SCHOOL_SORT_FIELD_MAP, MagicSchoolArgs, MagicSchoolArgsSchema, MagicSchoolIndexArgsSchema, MagicSchoolOrderField } from './args' @Resolver(MagicSchool) export class MagicSchoolResolver { @Query(() => [MagicSchool], { description: 'Gets all magic schools, optionally filtered by name and sorted by name.' }) async magicSchools(@Args(() => MagicSchoolArgs) args: MagicSchoolArgs): Promise { const validatedArgs = MagicSchoolArgsSchema.parse(args) const query = MagicSchoolModel.find() if (validatedArgs.name != null && validatedArgs.name !== '') { query.where({ name: { $regex: new RegExp(escapeRegExp(validatedArgs.name), 'i') } }) } const sortQuery = buildSortPipeline({ order: validatedArgs.order, sortFieldMap: MAGIC_SCHOOL_SORT_FIELD_MAP, defaultSortField: MagicSchoolOrderField.NAME }) if (Object.keys(sortQuery).length > 0) { query.sort(sortQuery) } if (validatedArgs.skip) { query.skip(validatedArgs.skip) } if (validatedArgs.limit) { query.limit(validatedArgs.limit) } return query.lean() } @Query(() => MagicSchool, { nullable: true, description: 'Gets a single magic school by index.' }) async magicSchool(@Arg('index', () => String) indexInput: string): Promise { const { index } = MagicSchoolIndexArgsSchema.parse({ index: indexInput }) return MagicSchoolModel.findOne({ index }).lean() } } ================================================ FILE: src/graphql/2014/resolvers/monster/args.ts ================================================ import { ArgsType, Field, InputType, registerEnumType } from 'type-graphql' import { z } from 'zod' import { BaseFilterArgs, BaseFilterArgsSchema, BaseIndexArgsSchema, BaseOrderInterface } from '@/graphql/common/args' import { OrderByDirection } from '@/graphql/common/enums' import { NumberFilterInput, NumberFilterInputSchema } from '@/graphql/common/inputs' export enum MonsterOrderField { NAME = 'name', TYPE = 'type', SIZE = 'size', CHALLENGE_RATING = 'challenge_rating', STRENGTH = 'strength', DEXTERITY = 'dexterity', CONSTITUTION = 'constitution', INTELLIGENCE = 'intelligence', WISDOM = 'wisdom', CHARISMA = 'charisma' } export const MONSTER_SORT_FIELD_MAP: Record = { [MonsterOrderField.NAME]: 'name', [MonsterOrderField.TYPE]: 'type', [MonsterOrderField.SIZE]: 'size', [MonsterOrderField.CHALLENGE_RATING]: 'challenge_rating', [MonsterOrderField.STRENGTH]: 'strength', [MonsterOrderField.DEXTERITY]: 'dexterity', [MonsterOrderField.CONSTITUTION]: 'constitution', [MonsterOrderField.INTELLIGENCE]: 'intelligence', [MonsterOrderField.WISDOM]: 'wisdom', [MonsterOrderField.CHARISMA]: 'charisma' } registerEnumType(MonsterOrderField, { name: 'MonsterOrderField', description: 'Fields to sort Monsters by' }) @InputType() export class MonsterOrder implements BaseOrderInterface { @Field(() => MonsterOrderField) by!: MonsterOrderField @Field(() => OrderByDirection) direction!: OrderByDirection @Field(() => MonsterOrder, { nullable: true }) then_by?: MonsterOrder } export const MonsterOrderSchema: z.ZodType = z.lazy(() => z.object({ by: z.nativeEnum(MonsterOrderField), direction: z.nativeEnum(OrderByDirection), then_by: MonsterOrderSchema.optional() }) ) export const MonsterArgsSchema = z.object({ ...BaseFilterArgsSchema.shape, type: z.string().optional(), subtype: z.string().optional(), challenge_rating: NumberFilterInputSchema.optional(), size: z.string().optional(), xp: NumberFilterInputSchema.optional(), strength: NumberFilterInputSchema.optional(), dexterity: NumberFilterInputSchema.optional(), constitution: NumberFilterInputSchema.optional(), intelligence: NumberFilterInputSchema.optional(), wisdom: NumberFilterInputSchema.optional(), charisma: NumberFilterInputSchema.optional(), damage_vulnerabilities: z.array(z.string()).optional(), damage_resistances: z.array(z.string()).optional(), damage_immunities: z.array(z.string()).optional(), condition_immunities: z.array(z.string()).optional(), order: MonsterOrderSchema.optional() }) export const MonsterIndexArgsSchema = BaseIndexArgsSchema @ArgsType() export class MonsterArgs extends BaseFilterArgs { @Field(() => String, { nullable: true, description: 'Filter by monster type (case-insensitive, exact match, e.g., "beast")' }) type?: string @Field(() => String, { nullable: true, description: 'Filter by monster subtype (case-insensitive, exact match, e.g., "goblinoid")' }) subtype?: string @Field(() => NumberFilterInput, { nullable: true, description: 'Filter by challenge rating' }) challenge_rating?: NumberFilterInput @Field(() => String, { nullable: true, description: 'Filter by monster size (exact match, e.g., "Medium")' }) size?: string @Field(() => NumberFilterInput, { nullable: true, description: 'Filter by monster XP' }) xp?: NumberFilterInput @Field(() => NumberFilterInput, { nullable: true, description: 'Filter by strength score' }) strength?: NumberFilterInput @Field(() => NumberFilterInput, { nullable: true, description: 'Filter by dexterity score' }) dexterity?: NumberFilterInput @Field(() => NumberFilterInput, { nullable: true, description: 'Filter by constitution score' }) constitution?: NumberFilterInput @Field(() => NumberFilterInput, { nullable: true, description: 'Filter by intelligence score' }) intelligence?: NumberFilterInput @Field(() => NumberFilterInput, { nullable: true, description: 'Filter by wisdom score' }) wisdom?: NumberFilterInput @Field(() => NumberFilterInput, { nullable: true, description: 'Filter by charisma score' }) charisma?: NumberFilterInput @Field(() => [String], { nullable: true, description: 'Filter by damage vulnerability indices' }) damage_vulnerabilities?: string[] @Field(() => [String], { nullable: true, description: 'Filter by damage resistance indices' }) damage_resistances?: string[] @Field(() => [String], { nullable: true, description: 'Filter by damage immunity indices' }) damage_immunities?: string[] @Field(() => [String], { nullable: true, description: 'Filter by condition immunity indices' }) condition_immunities?: string[] @Field(() => MonsterOrder, { nullable: true, description: 'Specify sorting order for monsters.' }) order?: MonsterOrder } ================================================ FILE: src/graphql/2014/resolvers/monster/resolver.ts ================================================ import { Arg, Args, FieldResolver, Query, Resolver, Root } from 'type-graphql' import { Armor } from '@/graphql/2014/common/equipmentTypes' import { DamageOrDamageChoiceUnion, ActionChoice, ActionChoiceOption, BreathChoice, BreathChoiceOption, DamageChoice, DamageChoiceOption, MultipleActionChoiceOption, MonsterArmorClassUnion } from '@/graphql/2014/types/monsterTypes' import { normalizeCount } from '@/graphql/2014/utils/helpers' import { buildSortPipeline } from '@/graphql/common/args' import { buildMongoQueryFromNumberFilter } from '@/graphql/common/inputs' import { SpellSlotCount } from '@/graphql/common/types' import { resolveMultipleReferences, resolveSingleReference } from '@/graphql/utils/resolvers' import AbilityScoreModel, { AbilityScore } from '@/models/2014/abilityScore' import ConditionModel, { Condition } from '@/models/2014/condition' import DamageTypeModel, { DamageType } from '@/models/2014/damageType' import EquipmentModel from '@/models/2014/equipment' import MonsterModel, { ArmorClassArmor, ArmorClassCondition, ArmorClassSpell, Monster, MonsterAction, MonsterProficiency, SpecialAbilitySpell, SpecialAbilitySpellcasting } from '@/models/2014/monster' import ProficiencyModel, { Proficiency } from '@/models/2014/proficiency' import SpellModel, { Spell } from '@/models/2014/spell' import { APIReference } from '@/models/common/apiReference' import { ActionOption, BreathOption, Choice, DamageOption, OptionsArrayOptionSet } from '@/models/common/choice' import { Damage } from '@/models/common/damage' import { DifficultyClass } from '@/models/common/difficultyClass' import { escapeRegExp } from '@/util' import { MONSTER_SORT_FIELD_MAP, MonsterArgs, MonsterArgsSchema, MonsterIndexArgsSchema, MonsterOrderField } from './args' @Resolver(Monster) export class MonsterResolver { @Query(() => [Monster], { description: 'Gets all monsters, optionally filtered and sorted.' }) async monsters(@Args(() => MonsterArgs) args: MonsterArgs): Promise { const validatedArgs = MonsterArgsSchema.parse(args) let query = MonsterModel.find() const filters: any[] = [] if (validatedArgs.name != null && validatedArgs.name !== '') { filters.push({ name: { $regex: new RegExp(escapeRegExp(validatedArgs.name), 'i') } }) } if (validatedArgs.type != null && validatedArgs.type !== '') { filters.push({ type: { $regex: new RegExp(`^${escapeRegExp(validatedArgs.type)}$`, 'i') } }) } if (validatedArgs.subtype != null && validatedArgs.subtype !== '') { filters.push({ subtype: { $regex: new RegExp(`^${escapeRegExp(validatedArgs.subtype)}$`, 'i') } }) } if (validatedArgs.challenge_rating) { const crQuery = buildMongoQueryFromNumberFilter(validatedArgs.challenge_rating) if (crQuery) filters.push({ challenge_rating: crQuery }) } if (validatedArgs.size != null && validatedArgs.size !== '') { filters.push({ size: validatedArgs.size }) } if (validatedArgs.xp) { const xpQuery = buildMongoQueryFromNumberFilter(validatedArgs.xp) if (xpQuery) filters.push({ xp: xpQuery }) } const abilityScores = [ 'strength', 'dexterity', 'constitution', 'intelligence', 'wisdom', 'charisma' ] as const for (const score of abilityScores) { if (validatedArgs[score]) { const scoreQuery = buildMongoQueryFromNumberFilter(validatedArgs[score]!) if (scoreQuery) filters.push({ [score]: scoreQuery }) } } if (validatedArgs.damage_vulnerabilities && validatedArgs.damage_vulnerabilities.length > 0) { filters.push({ 'damage_vulnerabilities.index': { $in: validatedArgs.damage_vulnerabilities } }) } if (validatedArgs.damage_resistances && validatedArgs.damage_resistances.length > 0) { filters.push({ 'damage_resistances.index': { $in: validatedArgs.damage_resistances } }) } if (validatedArgs.damage_immunities && validatedArgs.damage_immunities.length > 0) { filters.push({ 'damage_immunities.index': { $in: validatedArgs.damage_immunities } }) } if (validatedArgs.condition_immunities && validatedArgs.condition_immunities.length > 0) { filters.push({ 'condition_immunities.index': { $in: validatedArgs.condition_immunities } }) } if (filters.length > 0) { query = query.where({ $and: filters }) } const sortQuery = buildSortPipeline({ order: validatedArgs.order, sortFieldMap: MONSTER_SORT_FIELD_MAP, defaultSortField: MonsterOrderField.NAME }) if (Object.keys(sortQuery).length > 0) { query = query.sort(sortQuery) } if (validatedArgs.skip !== undefined) { query = query.skip(validatedArgs.skip) } if (validatedArgs.limit !== undefined) { query = query.limit(validatedArgs.limit) } return query.lean() } @Query(() => Monster, { nullable: true, description: 'Gets a single monster by its index.' }) async monster(@Arg('index', () => String) indexInput: string): Promise { const { index } = MonsterIndexArgsSchema.parse({ index: indexInput }) return MonsterModel.findOne({ index }).lean() } @FieldResolver(() => [Condition]) async condition_immunities(@Root() monster: Monster): Promise { return resolveMultipleReferences(monster.condition_immunities, ConditionModel) } @FieldResolver(() => [Monster]) async forms(@Root() monster: Monster): Promise { if (!monster.forms) return null return resolveMultipleReferences(monster.forms, MonsterModel) } @FieldResolver(() => [MonsterArmorClassUnion], { name: 'armor_class' }) async armor_class(@Root() monster: Monster): Promise> { return monster.armor_class as Array } } @Resolver(ArmorClassArmor) export class ArmorClassArmorResolver { @FieldResolver(() => [Armor], { name: 'armor', nullable: true }) async armor(@Root() acArmor: ArmorClassArmor): Promise> { if (!acArmor.armor) return [] return resolveMultipleReferences(acArmor.armor, EquipmentModel) as Promise< Array > } } @Resolver(ArmorClassSpell) export class ArmorClassSpellResolver { @FieldResolver(() => Spell, { name: 'spell', nullable: true }) async spell(@Root() acSpell: ArmorClassSpell): Promise { return resolveSingleReference(acSpell.spell, SpellModel) } } @Resolver(ArmorClassCondition) export class ArmorClassConditionResolver { @FieldResolver(() => Condition, { name: 'condition', nullable: true }) async condition(@Root() acCondition: ArmorClassCondition): Promise { return resolveSingleReference(acCondition.condition, ConditionModel) } } @Resolver(MonsterProficiency) export class MonsterProficiencyResolver { @FieldResolver(() => Proficiency, { name: 'proficiency' }) async proficiency(@Root() monsterProficiency: MonsterProficiency): Promise { return resolveSingleReference(monsterProficiency.proficiency, ProficiencyModel) } } @Resolver(DifficultyClass) export class DifficultyClassResolver { @FieldResolver(() => AbilityScore, { name: 'dc_type', description: 'The ability score associated with this DC, resolved from its API reference.' }) async dc_type(@Root() difficultyClass: DifficultyClass): Promise { return resolveSingleReference(difficultyClass.dc_type as APIReference, AbilityScoreModel) } } @Resolver(SpecialAbilitySpellcasting) export class SpecialAbilitySpellcastingResolver { @FieldResolver(() => AbilityScore, { name: 'ability' }) async ability(@Root() spellcasting: SpecialAbilitySpellcasting): Promise { return resolveSingleReference(spellcasting.ability, AbilityScoreModel) } @FieldResolver(() => [SpellSlotCount], { name: 'slots', nullable: true }) async slots(@Root() spellcasting: SpecialAbilitySpellcasting): Promise { if (!spellcasting.slots) { return null } const slotCounts: SpellSlotCount[] = [] for (const levelKey in spellcasting.slots) { if (Object.prototype.hasOwnProperty.call(spellcasting.slots, levelKey)) { const count = spellcasting.slots[levelKey] const slotLevel = parseInt(levelKey, 10) if (!isNaN(slotLevel)) { const slotCount = new SpellSlotCount() slotCount.slot_level = slotLevel slotCount.count = count slotCounts.push(slotCount) } } } return slotCounts.sort((a, b) => a.slot_level - b.slot_level) } } @Resolver(SpecialAbilitySpell) export class SpecialAbilitySpellResolver { @FieldResolver(() => Spell, { name: 'spell', description: 'The resolved spell object.' }) async resolveSpell(@Root() abilitySpell: SpecialAbilitySpell): Promise { const spellIndex = abilitySpell.url.substring(abilitySpell.url.lastIndexOf('/') + 1) if (!spellIndex) return null return SpellModel.findOne({ index: spellIndex }) } } @Resolver(MonsterAction) export class MonsterActionResolver { @FieldResolver(() => [DamageOrDamageChoiceUnion], { nullable: true }) async damage(@Root() action: MonsterAction): Promise<(Damage | DamageChoice)[] | undefined> { if (!action.damage) { return undefined } const resolvedDamage = await Promise.all( action.damage.map(async (item: Damage | Choice) => { if ('choose' in item) { return resolveDamageChoice(item as Choice) } return item as Damage }) ) return resolvedDamage.filter((item): item is Damage | DamageChoice => item !== null) } @FieldResolver(() => ActionChoice, { nullable: true }) async action_options(@Root() action: MonsterAction): Promise { return resolveActionChoice(action.action_options) } @FieldResolver(() => BreathChoice, { nullable: true }) async options(@Root() action: MonsterAction): Promise { return resolveBreathChoice(action.options) } } async function resolveBreathChoice( choiceData: Choice | undefined | null ): Promise { if (!choiceData || !('options' in choiceData.from)) { return null } const options = (choiceData.from as OptionsArrayOptionSet).options const validOptions: BreathChoiceOption[] = [] for (const option of options) { if (option.option_type === 'breath') { const breathOption = option as BreathOption const abilityScore = await resolveSingleReference(breathOption.dc.dc_type, AbilityScoreModel) const currentResolvedOption: Partial = { option_type: breathOption.option_type, name: breathOption.name, dc: { dc_type: abilityScore as AbilityScore, dc_value: breathOption.dc.dc_value, success_type: breathOption.dc.success_type } } if (breathOption.damage && breathOption.damage.length > 0) { const resolvedDamageItems = await Promise.all( breathOption.damage.map(async (damageItem) => { const resolvedDamageType = await resolveSingleReference( damageItem.damage_type, DamageTypeModel ) if (resolvedDamageType !== null) { return { damage_dice: damageItem.damage_dice, damage_type: resolvedDamageType as DamageType } } return null }) ) const filteredDamageItems = resolvedDamageItems.filter((item) => item !== null) if (filteredDamageItems.length > 0) { currentResolvedOption.damage = filteredDamageItems as any[] } } validOptions.push(currentResolvedOption as BreathChoiceOption) } } if (validOptions.length === 0) { return null } return { choose: choiceData.choose, type: choiceData.type, from: { option_set_type: choiceData.from.option_set_type, options: validOptions }, desc: choiceData.desc } } async function resolveDamageChoice( choiceData: Choice | undefined | null ): Promise { if (!choiceData || !('options' in choiceData.from)) { return null } const options = (choiceData.from as OptionsArrayOptionSet).options const validOptions: DamageChoiceOption[] = [] for (const option of options) { if (option.option_type === 'damage') { const damageOption = option as DamageOption const damageType = await resolveSingleReference(damageOption.damage_type, DamageTypeModel) if (damageType !== null) { const resolvedOption: DamageChoiceOption = { option_type: damageOption.option_type, damage: { damage_dice: damageOption.damage_dice, damage_type: damageType as DamageType } } validOptions.push(resolvedOption) } } } if (validOptions.length === 0) { return null } return { choose: choiceData.choose, type: choiceData.type, from: { option_set_type: choiceData.from.option_set_type, options: validOptions }, desc: choiceData.desc } } async function resolveActionChoice( choiceData: Choice | undefined | null ): Promise { if (!choiceData || !('options' in choiceData.from)) { return null } const options = (choiceData.from as OptionsArrayOptionSet).options const validOptions: Array = [] for (const option of options) { if (option.option_type === 'multiple') { const multipleOption = option as { option_type: string; items: ActionOption[] } const resolvedItems = multipleOption.items.map((item) => ({ option_type: item.option_type, action_name: item.action_name, count: normalizeCount(item.count), type: item.type })) validOptions.push({ option_type: multipleOption.option_type, items: resolvedItems }) } else if (option.option_type === 'action') { const actionOption = option as ActionOption const resolvedOption: ActionChoiceOption = { option_type: actionOption.option_type, action_name: actionOption.action_name, count: normalizeCount(actionOption.count), type: actionOption.type } validOptions.push(resolvedOption) } } if (validOptions.length === 0) { return null } return { choose: choiceData.choose, type: choiceData.type, from: { option_set_type: choiceData.from.option_set_type, options: validOptions }, desc: choiceData.desc } } ================================================ FILE: src/graphql/2014/resolvers/proficiency/args.ts ================================================ import { ArgsType, Field, InputType, registerEnumType } from 'type-graphql' import { z } from 'zod' import { BaseFilterArgs, BaseFilterArgsSchema, BaseIndexArgsSchema, BaseOrderInterface } from '@/graphql/common/args' import { OrderByDirection } from '@/graphql/common/enums' export enum ProficiencyOrderField { NAME = 'name', TYPE = 'type' } export const PROFICIENCY_SORT_FIELD_MAP: Record = { [ProficiencyOrderField.NAME]: 'name', [ProficiencyOrderField.TYPE]: 'type' } registerEnumType(ProficiencyOrderField, { name: 'ProficiencyOrderField', description: 'Fields to sort Proficiencies by' }) @InputType() export class ProficiencyOrder implements BaseOrderInterface { @Field(() => ProficiencyOrderField) by!: ProficiencyOrderField @Field(() => OrderByDirection) direction!: OrderByDirection @Field(() => ProficiencyOrder, { nullable: true }) then_by?: ProficiencyOrder } export const ProficiencyOrderSchema: z.ZodType = z.lazy(() => z.object({ by: z.nativeEnum(ProficiencyOrderField), direction: z.nativeEnum(OrderByDirection), then_by: ProficiencyOrderSchema.optional() }) ) export const ProficiencyArgsSchema = z.object({ ...BaseFilterArgsSchema.shape, class: z.array(z.string()).optional(), race: z.array(z.string()).optional(), type: z.array(z.string()).optional(), order: ProficiencyOrderSchema.optional() }) export const ProficiencyIndexArgsSchema = BaseIndexArgsSchema // Define ArgsType for the proficiencies query @ArgsType() export class ProficiencyArgs extends BaseFilterArgs { @Field(() => [String], { nullable: true, description: 'Filter by class index (e.g., ["barbarian", "bard"])' }) class?: string[] @Field(() => [String], { nullable: true, description: 'Filter by race index (e.g., ["dragonborn", "dwarf"])' }) race?: string[] @Field(() => [String], { nullable: true, description: 'Filter by proficiency type (exact match, e.g., ["ARMOR", "WEAPONS"])' }) type?: string[] @Field(() => ProficiencyOrder, { nullable: true, description: 'Specify sorting order for proficiencies. Allows nested sorting.' }) order?: ProficiencyOrder } ================================================ FILE: src/graphql/2014/resolvers/proficiency/resolver.ts ================================================ import { Arg, Args, FieldResolver, Query, Resolver, Root } from 'type-graphql' import { ProficiencyReference } from '@/graphql/2014/types/proficiencyTypes' import { buildSortPipeline } from '@/graphql/common/args' import { resolveMultipleReferences, resolveSingleReference } from '@/graphql/utils/resolvers' import AbilityScoreModel from '@/models/2014/abilityScore' import ClassModel, { Class } from '@/models/2014/class' import EquipmentModel from '@/models/2014/equipment' import EquipmentCategoryModel from '@/models/2014/equipmentCategory' import ProficiencyModel, { Proficiency } from '@/models/2014/proficiency' import RaceModel, { Race } from '@/models/2014/race' import SkillModel from '@/models/2014/skill' import { escapeRegExp } from '@/util' import { PROFICIENCY_SORT_FIELD_MAP, ProficiencyArgs, ProficiencyArgsSchema, ProficiencyIndexArgsSchema, ProficiencyOrderField } from './args' @Resolver(Proficiency) export class ProficiencyResolver { @Query(() => [Proficiency], { description: 'Query all Proficiencies, optionally filtered and sorted.' }) async proficiencies(@Args(() => ProficiencyArgs) args: ProficiencyArgs): Promise { const validatedArgs = ProficiencyArgsSchema.parse(args) let query = ProficiencyModel.find() const filters: any[] = [] if (validatedArgs.name != null && validatedArgs.name !== '') { filters.push({ name: { $regex: new RegExp(escapeRegExp(validatedArgs.name), 'i') } }) } if (validatedArgs.class && validatedArgs.class.length > 0) { filters.push({ 'classes.index': { $in: validatedArgs.class } }) } if (validatedArgs.race && validatedArgs.race.length > 0) { filters.push({ 'races.index': { $in: validatedArgs.race } }) } if (validatedArgs.type && validatedArgs.type.length > 0) { filters.push({ type: { $in: validatedArgs.type } }) } if (filters.length > 0) { query = query.where({ $and: filters }) } const sortQuery = buildSortPipeline({ order: validatedArgs.order, sortFieldMap: PROFICIENCY_SORT_FIELD_MAP, defaultSortField: ProficiencyOrderField.NAME }) if (Object.keys(sortQuery).length > 0) { query.sort(sortQuery) } if (validatedArgs.skip !== undefined) { query = query.skip(validatedArgs.skip) } if (validatedArgs.limit !== undefined) { query = query.limit(validatedArgs.limit) } return query.lean() } @Query(() => Proficiency, { nullable: true, description: 'Gets a single proficiency by index.' }) async proficiency(@Arg('index', () => String) indexInput: string): Promise { const { index } = ProficiencyIndexArgsSchema.parse({ index: indexInput }) return ProficiencyModel.findOne({ index }).lean() } @FieldResolver(() => [Class], { nullable: true }) async classes(@Root() proficiency: Proficiency): Promise { return resolveMultipleReferences(proficiency.classes, ClassModel) } @FieldResolver(() => [Race], { nullable: true }) async races(@Root() proficiency: Proficiency): Promise { return resolveMultipleReferences(proficiency.races, RaceModel) } @FieldResolver(() => ProficiencyReference, { nullable: true }) async reference(@Root() proficiency: Proficiency): Promise { const ref = proficiency.reference if (!ref?.index || !ref.url) { return null } if (ref.url.includes('/equipment-categories/')) { return resolveSingleReference(ref, EquipmentCategoryModel) } if (ref.url.includes('/skills/')) { return resolveSingleReference(ref, SkillModel) } if (ref.url.includes('/ability-scores/')) { return resolveSingleReference(ref, AbilityScoreModel) } if (ref.url.includes('/equipment/')) { return resolveSingleReference(ref, EquipmentModel) } console.warn( `Unable to determine reference type from URL: ${ref.url} (Proficiency index: ${proficiency.index})` ) return null } } ================================================ FILE: src/graphql/2014/resolvers/race/args.ts ================================================ import { ArgsType, Field, InputType, registerEnumType } from 'type-graphql' import { z } from 'zod' import { BaseFilterArgs, BaseFilterArgsSchema, BaseIndexArgsSchema, BaseOrderInterface } from '@/graphql/common/args' import { OrderByDirection } from '@/graphql/common/enums' import { NumberFilterInput, NumberFilterInputSchema } from '@/graphql/common/inputs' export enum RaceOrderField { NAME = 'name' } export const RACE_SORT_FIELD_MAP: Record = { [RaceOrderField.NAME]: 'name' } registerEnumType(RaceOrderField, { name: 'RaceOrderField', description: 'Fields to sort Races by' }) @InputType() export class RaceOrder implements BaseOrderInterface { @Field(() => RaceOrderField) by!: RaceOrderField @Field(() => OrderByDirection) direction!: OrderByDirection @Field(() => RaceOrder, { nullable: true }) then_by?: RaceOrder } export const RaceOrderSchema: z.ZodType = z.lazy(() => z.object({ by: z.nativeEnum(RaceOrderField), direction: z.nativeEnum(OrderByDirection), then_by: RaceOrderSchema.optional() }) ) export const RaceArgsSchema = z.object({ ...BaseFilterArgsSchema.shape, ability_bonus: z.array(z.string()).optional(), size: z.array(z.string()).optional(), language: z.array(z.string()).optional(), speed: NumberFilterInputSchema.optional(), order: RaceOrderSchema.optional() }) export const RaceIndexArgsSchema = BaseIndexArgsSchema @ArgsType() export class RaceArgs extends BaseFilterArgs { @Field(() => [String], { nullable: true, description: 'Filter by one or more ability score indices that provide a bonus' }) ability_bonus?: string[] @Field(() => [String], { nullable: true, description: 'Filter by one or more race sizes (e.g., ["Medium", "Small"])' }) size?: string[] @Field(() => [String], { nullable: true, description: 'Filter by one or more language indices spoken by the race' }) language?: string[] @Field(() => NumberFilterInput, { nullable: true, description: 'Filter by race speed. Allows exact match, list, or range.' }) speed?: NumberFilterInput @Field(() => RaceOrder, { nullable: true, description: 'Specify sorting order for races. Allows nested sorting.' }) order?: RaceOrder } ================================================ FILE: src/graphql/2014/resolvers/race/resolver.ts ================================================ import { Arg, Args, FieldResolver, Query, Resolver, Root } from 'type-graphql' import { AbilityScoreBonusChoice, AbilityScoreBonusChoiceOption, LanguageChoice } from '@/graphql/2014/common/choiceTypes' import { resolveLanguageChoice } from '@/graphql/2014/utils/resolvers' import { buildSortPipeline } from '@/graphql/common/args' import { buildMongoQueryFromNumberFilter } from '@/graphql/common/inputs' import { resolveMultipleReferences, resolveSingleReference } from '@/graphql/utils/resolvers' import AbilityScoreModel, { AbilityScore } from '@/models/2014/abilityScore' import LanguageModel, { Language } from '@/models/2014/language' import RaceModel, { Race, RaceAbilityBonus } from '@/models/2014/race' import SubraceModel, { Subrace } from '@/models/2014/subrace' import TraitModel, { Trait } from '@/models/2014/trait' import { AbilityBonusOption, Choice, OptionsArrayOptionSet } from '@/models/common/choice' import { escapeRegExp } from '@/util' import { RACE_SORT_FIELD_MAP, RaceArgs, RaceArgsSchema, RaceIndexArgsSchema, RaceOrderField } from './args' @Resolver(() => Race) export class RaceResolver { @Query(() => [Race], { description: 'Gets all races, optionally filtered by name and sorted.' }) async races(@Args(() => RaceArgs) args: RaceArgs): Promise { const validatedArgs = RaceArgsSchema.parse(args) const query = RaceModel.find() const filters: any[] = [] if (validatedArgs.name != null && validatedArgs.name !== '') { filters.push({ name: { $regex: new RegExp(escapeRegExp(validatedArgs.name), 'i') } }) } if (validatedArgs.ability_bonus && validatedArgs.ability_bonus.length > 0) { filters.push({ 'ability_bonuses.ability_score.index': { $in: validatedArgs.ability_bonus } }) } if (validatedArgs.size && validatedArgs.size.length > 0) { filters.push({ size: { $in: validatedArgs.size } }) } if (validatedArgs.language && validatedArgs.language.length > 0) { filters.push({ 'languages.index': { $in: validatedArgs.language } }) } if (validatedArgs.speed) { const speedQuery = buildMongoQueryFromNumberFilter(validatedArgs.speed) if (speedQuery) { filters.push({ speed: speedQuery }) } } if (filters.length > 0) { query.where({ $and: filters }) } const sortQuery = buildSortPipeline({ order: validatedArgs.order, sortFieldMap: RACE_SORT_FIELD_MAP, defaultSortField: RaceOrderField.NAME }) if (Object.keys(sortQuery).length > 0) { query.sort(sortQuery) } if (validatedArgs.skip !== undefined) { query.skip(validatedArgs.skip) } if (validatedArgs.limit !== undefined) { query.limit(validatedArgs.limit) } return query.lean() } @Query(() => Race, { nullable: true, description: 'Gets a single race by its index.' }) async race(@Arg('index', () => String) indexInput: string): Promise { const { index } = RaceIndexArgsSchema.parse({ index: indexInput }) return RaceModel.findOne({ index }).lean() } @FieldResolver(() => [Language], { nullable: true }) async languages(@Root() race: Race): Promise { return resolveMultipleReferences(race.languages, LanguageModel) } @FieldResolver(() => [Subrace], { nullable: true }) async subraces(@Root() race: Race): Promise { return resolveMultipleReferences(race.subraces, SubraceModel) } @FieldResolver(() => [Trait], { nullable: true }) async traits(@Root() race: Race): Promise { return resolveMultipleReferences(race.traits, TraitModel) } @FieldResolver(() => LanguageChoice, { nullable: true }) async language_options(@Root() race: Race): Promise { return resolveLanguageChoice(race.language_options as Choice) } @FieldResolver(() => AbilityScoreBonusChoice, { nullable: true }) async ability_bonus_options(@Root() race: Race): Promise { return resolveAbilityScoreBonusChoice(race.ability_bonus_options, AbilityScoreModel) } } @Resolver(RaceAbilityBonus) export class RaceAbilityBonusResolver { @FieldResolver(() => AbilityScore, { nullable: true }) async ability_score(@Root() raceAbilityBonus: RaceAbilityBonus): Promise { return resolveSingleReference(raceAbilityBonus.ability_score, AbilityScoreModel) } } async function resolveAbilityScoreBonusChoice( choiceData: Choice | undefined, TargetAbilityScoreModel: typeof AbilityScoreModel ): Promise { if (!choiceData || !choiceData.type || typeof choiceData.choose !== 'number') { return null } const resolvedOptions: AbilityScoreBonusChoiceOption[] = [] const from = choiceData.from as OptionsArrayOptionSet for (const option of from.options) { if (option.option_type === 'ability_bonus') { const abilityScore = await resolveSingleReference( (option as AbilityBonusOption).ability_score, TargetAbilityScoreModel ) if (abilityScore !== null) { resolvedOptions.push({ option_type: option.option_type, ability_score: abilityScore as AbilityScore, bonus: (option as AbilityBonusOption).bonus }) } } } if (resolvedOptions.length === 0 && from.options.length > 0) { return null } return { choose: choiceData.choose, type: choiceData.type, from: { option_set_type: from.option_set_type, options: resolvedOptions }, desc: choiceData.desc } } ================================================ FILE: src/graphql/2014/resolvers/rule/args.ts ================================================ import { ArgsType, Field, InputType, registerEnumType } from 'type-graphql' import { z } from 'zod' import { BaseFilterArgs, BaseFilterArgsSchema, BaseIndexArgsSchema, BaseOrderInterface } from '@/graphql/common/args' import { OrderByDirection } from '@/graphql/common/enums' export enum RuleOrderField { NAME = 'name' } export const RULE_SORT_FIELD_MAP: Record = { [RuleOrderField.NAME]: 'name' } registerEnumType(RuleOrderField, { name: 'RuleOrderField', description: 'Fields to sort Rules by' }) @InputType() export class RuleOrder implements BaseOrderInterface { @Field(() => RuleOrderField) by!: RuleOrderField @Field(() => OrderByDirection) direction!: OrderByDirection @Field(() => RuleOrder, { nullable: true }) then_by?: RuleOrder } export const RuleOrderSchema: z.ZodType = z.lazy(() => z.object({ by: z.nativeEnum(RuleOrderField), direction: z.nativeEnum(OrderByDirection), then_by: RuleOrderSchema.optional() }) ) export const RuleArgsSchema = z.object({ ...BaseFilterArgsSchema.shape, order: RuleOrderSchema.optional() }) export const RuleIndexArgsSchema = BaseIndexArgsSchema @ArgsType() export class RuleArgs extends BaseFilterArgs { @Field(() => RuleOrder, { nullable: true, description: 'Specify sorting order for rules. Allows nested sorting.' }) order?: RuleOrder } ================================================ FILE: src/graphql/2014/resolvers/rule/resolver.ts ================================================ import { Arg, Args, FieldResolver, Query, Resolver, Root } from 'type-graphql' import { buildSortPipeline } from '@/graphql/common/args' import { resolveMultipleReferences } from '@/graphql/utils/resolvers' import RuleModel, { Rule } from '@/models/2014/rule' import RuleSectionModel, { RuleSection } from '@/models/2014/ruleSection' import { escapeRegExp } from '@/util' import { RULE_SORT_FIELD_MAP, RuleArgs, RuleArgsSchema, RuleIndexArgsSchema, RuleOrderField } from './args' @Resolver(Rule) export class RuleResolver { @Query(() => [Rule], { description: 'Gets all rules, optionally filtered by name and sorted by name.' }) async rules(@Args(() => RuleArgs) args: RuleArgs): Promise { const validatedArgs = RuleArgsSchema.parse(args) const query = RuleModel.find() if (validatedArgs.name != null && validatedArgs.name !== '') { query.where({ name: { $regex: new RegExp(escapeRegExp(validatedArgs.name), 'i') } }) } const sortQuery = buildSortPipeline({ order: validatedArgs.order, sortFieldMap: RULE_SORT_FIELD_MAP }) if (Object.keys(sortQuery).length > 0) { query.sort(sortQuery) } if (validatedArgs.skip) { query.skip(validatedArgs.skip) } if (validatedArgs.limit) { query.limit(validatedArgs.limit) } return query.lean() } @Query(() => Rule, { nullable: true, description: 'Gets a single rule by index.' }) async rule(@Arg('index', () => String) indexInput: string): Promise { const { index } = RuleIndexArgsSchema.parse({ index: indexInput }) return RuleModel.findOne({ index }).lean() } @FieldResolver(() => [RuleSection]) async subsections(@Root() rule: Rule): Promise { return resolveMultipleReferences(rule.subsections, RuleSectionModel) } } ================================================ FILE: src/graphql/2014/resolvers/ruleSection/args.ts ================================================ import { ArgsType, Field, InputType, registerEnumType } from 'type-graphql' import { z } from 'zod' import { BaseFilterArgs, BaseFilterArgsSchema, BaseIndexArgsSchema, BaseOrderInterface } from '@/graphql/common/args' import { OrderByDirection } from '@/graphql/common/enums' export enum RuleSectionOrderField { NAME = 'name' } export const RULE_SECTION_SORT_FIELD_MAP: Record = { [RuleSectionOrderField.NAME]: 'name' } registerEnumType(RuleSectionOrderField, { name: 'RuleSectionOrderField', description: 'Fields to sort Rule Sections by' }) @InputType() export class RuleSectionOrder implements BaseOrderInterface { @Field(() => RuleSectionOrderField) by!: RuleSectionOrderField @Field(() => OrderByDirection) direction!: OrderByDirection @Field(() => RuleSectionOrder, { nullable: true }) then_by?: RuleSectionOrder } export const RuleSectionOrderSchema: z.ZodType = z.lazy(() => z.object({ by: z.nativeEnum(RuleSectionOrderField), direction: z.nativeEnum(OrderByDirection), then_by: RuleSectionOrderSchema.optional() }) ) export const RuleSectionArgsSchema = z.object({ ...BaseFilterArgsSchema.shape, order: RuleSectionOrderSchema.optional() }) export const RuleSectionIndexArgsSchema = BaseIndexArgsSchema @ArgsType() export class RuleSectionArgs extends BaseFilterArgs { @Field(() => RuleSectionOrder, { nullable: true, description: 'Specify sorting order for rule sections. Allows nested sorting.' }) order?: RuleSectionOrder } ================================================ FILE: src/graphql/2014/resolvers/ruleSection/resolver.ts ================================================ import { Arg, Args, Query, Resolver } from 'type-graphql' import { buildSortPipeline } from '@/graphql/common/args' import RuleSectionModel, { RuleSection } from '@/models/2014/ruleSection' import { escapeRegExp } from '@/util' import { RULE_SECTION_SORT_FIELD_MAP, RuleSectionArgs, RuleSectionArgsSchema, RuleSectionIndexArgsSchema, RuleSectionOrderField } from './args' @Resolver(RuleSection) export class RuleSectionResolver { @Query(() => [RuleSection], { description: 'Gets all rule sections, optionally filtered by name and sorted by name.' }) async ruleSections(@Args(() => RuleSectionArgs) args: RuleSectionArgs): Promise { const validatedArgs = RuleSectionArgsSchema.parse(args) const query = RuleSectionModel.find() if (validatedArgs.name != null && validatedArgs.name !== '') { query.where({ name: { $regex: new RegExp(escapeRegExp(validatedArgs.name), 'i') } }) } const sortQuery = buildSortPipeline({ order: validatedArgs.order, sortFieldMap: RULE_SECTION_SORT_FIELD_MAP }) if (Object.keys(sortQuery).length > 0) { query.sort(sortQuery) } if (validatedArgs.skip) { query.skip(validatedArgs.skip) } if (validatedArgs.limit) { query.limit(validatedArgs.limit) } return query.lean() } @Query(() => RuleSection, { nullable: true, description: 'Gets a single rule section by index.' }) async ruleSection(@Arg('index', () => String) indexInput: string): Promise { const { index } = RuleSectionIndexArgsSchema.parse({ index: indexInput }) return RuleSectionModel.findOne({ index }).lean() } } ================================================ FILE: src/graphql/2014/resolvers/skill/args.ts ================================================ import { ArgsType, Field, InputType, registerEnumType } from 'type-graphql' import { z } from 'zod' import { BaseFilterArgs, BaseFilterArgsSchema, BaseIndexArgsSchema, BaseOrderInterface } from '@/graphql/common/args' import { OrderByDirection } from '@/graphql/common/enums' export enum SkillOrderField { NAME = 'name', ABILITY_SCORE = 'ability_score' } export const SKILL_SORT_FIELD_MAP: Record = { [SkillOrderField.NAME]: 'name', [SkillOrderField.ABILITY_SCORE]: 'ability_score.name' } registerEnumType(SkillOrderField, { name: 'SkillOrderField', description: 'Fields to sort Skills by' }) @InputType() export class SkillOrder implements BaseOrderInterface { @Field(() => SkillOrderField) by!: SkillOrderField @Field(() => OrderByDirection) direction!: OrderByDirection @Field(() => SkillOrder, { nullable: true }) then_by?: SkillOrder } export const SkillOrderSchema: z.ZodType = z.lazy(() => z.object({ by: z.nativeEnum(SkillOrderField), direction: z.nativeEnum(OrderByDirection), then_by: SkillOrderSchema.optional() }) ) export const SkillArgsSchema = z.object({ ...BaseFilterArgsSchema.shape, ability_score: z.array(z.string()).optional(), order: SkillOrderSchema.optional() }) export const SkillIndexArgsSchema = BaseIndexArgsSchema @ArgsType() export class SkillArgs extends BaseFilterArgs { @Field(() => [String], { nullable: true, description: 'Filter by ability score index (e.g., ["str", "dex"])' }) ability_score?: string[] @Field(() => SkillOrder, { nullable: true, description: 'Specify sorting order for skills.' }) order?: SkillOrder } ================================================ FILE: src/graphql/2014/resolvers/skill/resolver.ts ================================================ import { Arg, Args, FieldResolver, Query, Resolver, Root } from 'type-graphql' import { buildSortPipeline } from '@/graphql/common/args' import { resolveSingleReference } from '@/graphql/utils/resolvers' import AbilityScoreModel, { AbilityScore } from '@/models/2014/abilityScore' import SkillModel, { Skill } from '@/models/2014/skill' import { escapeRegExp } from '@/util' import { SKILL_SORT_FIELD_MAP, SkillArgs, SkillArgsSchema, SkillIndexArgsSchema, SkillOrderField } from './args' @Resolver(Skill) export class SkillResolver { @Query(() => [Skill], { description: 'Gets all skills, optionally filtered by name and sorted by name.' }) async skills(@Args(() => SkillArgs) args: SkillArgs): Promise { const validatedArgs = SkillArgsSchema.parse(args) const query = SkillModel.find() const filters: any[] = [] if (validatedArgs.name != null && validatedArgs.name !== '') { filters.push({ name: { $regex: new RegExp(escapeRegExp(validatedArgs.name), 'i') } }) } if (validatedArgs.ability_score && validatedArgs.ability_score.length > 0) { filters.push({ 'ability_score.index': { $in: validatedArgs.ability_score } }) } if (filters.length > 0) { query.where({ $and: filters }) } const sortQuery = buildSortPipeline({ order: validatedArgs.order, sortFieldMap: SKILL_SORT_FIELD_MAP, defaultSortField: SkillOrderField.NAME }) if (Object.keys(sortQuery).length > 0) { query.sort(sortQuery) } if (validatedArgs.skip !== undefined) { query.skip(validatedArgs.skip) } if (validatedArgs.limit !== undefined) { query.limit(validatedArgs.limit) } return query.lean() } @Query(() => Skill, { nullable: true, description: 'Gets a single skill by index.' }) async skill(@Arg('index', () => String) indexInput: string): Promise { const { index } = SkillIndexArgsSchema.parse({ index: indexInput }) return SkillModel.findOne({ index }).lean() } @FieldResolver(() => AbilityScore) async ability_score(@Root() skill: Skill): Promise { return resolveSingleReference(skill.ability_score, AbilityScoreModel) } } ================================================ FILE: src/graphql/2014/resolvers/spell/args.ts ================================================ import { ArgsType, Field, InputType, Int, registerEnumType } from 'type-graphql' import { z } from 'zod' import { BaseFilterArgs, BaseFilterArgsSchema, BaseIndexArgsSchema, BaseOrderInterface } from '@/graphql/common/args' import { OrderByDirection } from '@/graphql/common/enums' import { NumberFilterInput, NumberFilterInputSchema } from '@/graphql/common/inputs' const AreaOfEffectFilterInputSchema = z.object({ type: z.array(z.string()).optional(), size: NumberFilterInputSchema.optional() }) @InputType({ description: 'Input for filtering by area of effect properties.' }) export class AreaOfEffectFilterInput { @Field(() => [String], { nullable: true, description: 'Filter by area of effect type (e.g., ["sphere", "cone"])' }) type?: string[] @Field(() => NumberFilterInput, { nullable: true, description: 'Filter by area of effect size (in feet).' }) size?: NumberFilterInput } // Enum for Spell sortable fields export enum SpellOrderField { NAME = 'name', LEVEL = 'level', SCHOOL = 'school', AREA_OF_EFFECT_SIZE = 'area_of_effect_size' // Matches old API } export const SPELL_SORT_FIELD_MAP: Record = { [SpellOrderField.NAME]: 'name', [SpellOrderField.LEVEL]: 'level', [SpellOrderField.SCHOOL]: 'school.name', [SpellOrderField.AREA_OF_EFFECT_SIZE]: 'area_of_effect.size' } registerEnumType(SpellOrderField, { name: 'SpellOrderField', description: 'Fields to sort Spells by' }) @InputType() export class SpellOrder implements BaseOrderInterface { @Field(() => SpellOrderField) by!: SpellOrderField @Field(() => OrderByDirection) direction!: OrderByDirection @Field(() => SpellOrder, { nullable: true }) then_by?: SpellOrder } export const SpellOrderSchema: z.ZodType = z.lazy(() => z.object({ by: z.nativeEnum(SpellOrderField), direction: z.nativeEnum(OrderByDirection), then_by: SpellOrderSchema.optional() }) ) export const SpellArgsSchema = z.object({ ...BaseFilterArgsSchema.shape, level: z.array(z.number().int().min(0).max(9)).optional(), school: z.array(z.string()).optional(), class: z.array(z.string()).optional(), subclass: z.array(z.string()).optional(), concentration: z.boolean().optional(), ritual: z.boolean().optional(), attack_type: z.array(z.string()).optional(), casting_time: z.array(z.string()).optional(), area_of_effect: AreaOfEffectFilterInputSchema.optional(), damage_type: z.array(z.string()).optional(), dc_type: z.array(z.string()).optional(), range: z.array(z.string()).optional(), order: SpellOrderSchema.optional() }) export const SpellIndexArgsSchema = BaseIndexArgsSchema @ArgsType() export class SpellArgs extends BaseFilterArgs { @Field(() => [Int], { nullable: true, description: 'Filter by spell level (e.g., [0, 9])' }) level?: number[] @Field(() => [String], { nullable: true, description: 'Filter by magic school index (e.g., ["evocation"])' }) school?: string[] @Field(() => [String], { nullable: true, description: 'Filter by class index that can cast the spell (e.g., ["wizard"])' }) class?: string[] @Field(() => [String], { nullable: true, description: 'Filter by subclass index that can cast the spell (e.g., ["lore"])' }) subclass?: string[] @Field(() => Boolean, { nullable: true, description: 'Filter by concentration requirement' }) concentration?: boolean @Field(() => Boolean, { nullable: true, description: 'Filter by ritual requirement' }) ritual?: boolean @Field(() => [String], { nullable: true, description: 'Filter by attack type (e.g., ["ranged", "melee"])' }) attack_type?: string[] @Field(() => [String], { nullable: true, description: 'Filter by casting time (e.g., ["1 action"])' }) casting_time?: string[] @Field(() => AreaOfEffectFilterInput, { nullable: true, description: 'Filter by area of effect properties' }) area_of_effect?: AreaOfEffectFilterInput @Field(() => [String], { nullable: true, description: 'Filter by damage type index (e.g., ["fire"])' }) damage_type?: string[] @Field(() => [String], { nullable: true, description: 'Filter by saving throw DC type index (e.g., ["dex"])' }) dc_type?: string[] @Field(() => [String], { nullable: true, description: 'Filter by spell range (e.g., ["Self", "Touch"])' }) range?: string[] @Field(() => SpellOrder, { nullable: true, description: 'Specify sorting order for spells.' }) order?: SpellOrder } ================================================ FILE: src/graphql/2014/resolvers/spell/resolver.ts ================================================ import { Arg, Args, FieldResolver, Query, Resolver, Root } from 'type-graphql' import { mapLevelObjectToArray } from '@/graphql/2014/utils/helpers' import { buildSortPipeline } from '@/graphql/common/args' import { buildMongoQueryFromNumberFilter } from '@/graphql/common/inputs' import { LevelValue } from '@/graphql/common/types' import { resolveMultipleReferences, resolveSingleReference } from '@/graphql/utils/resolvers' import AbilityScoreModel, { AbilityScore } from '@/models/2014/abilityScore' import ClassModel, { Class } from '@/models/2014/class' import DamageTypeModel, { DamageType } from '@/models/2014/damageType' import MagicSchoolModel, { MagicSchool } from '@/models/2014/magicSchool' import SpellModel, { Spell, SpellDamage, SpellDC } from '@/models/2014/spell' import SubclassModel, { Subclass } from '@/models/2014/subclass' import { escapeRegExp } from '@/util' import { SPELL_SORT_FIELD_MAP, SpellArgs, SpellArgsSchema, SpellIndexArgsSchema, SpellOrderField } from './args' @Resolver(Spell) export class SpellResolver { @Query(() => [Spell], { description: 'Gets all spells, optionally filtered and sorted.' }) async spells(@Args(() => SpellArgs) args: SpellArgs): Promise { const validatedArgs = SpellArgsSchema.parse(args) const query = SpellModel.find() const filters: any[] = [] if (validatedArgs.name != null && validatedArgs.name !== '') { filters.push({ name: { $regex: new RegExp(escapeRegExp(validatedArgs.name), 'i') } }) } if (validatedArgs.level && validatedArgs.level.length > 0) { filters.push({ level: { $in: validatedArgs.level } }) } if (validatedArgs.school && validatedArgs.school.length > 0) { filters.push({ 'school.index': { $in: validatedArgs.school } }) } if (validatedArgs.class && validatedArgs.class.length > 0) { filters.push({ 'classes.index': { $in: validatedArgs.class } }) } if (validatedArgs.subclass && validatedArgs.subclass.length > 0) { filters.push({ 'subclasses.index': { $in: validatedArgs.subclass } }) } if (typeof validatedArgs.concentration === 'boolean') { filters.push({ concentration: validatedArgs.concentration }) } if (typeof validatedArgs.ritual === 'boolean') { filters.push({ ritual: validatedArgs.ritual }) } if (validatedArgs.attack_type && validatedArgs.attack_type.length > 0) { filters.push({ attack_type: { $in: validatedArgs.attack_type } }) } if (validatedArgs.casting_time && validatedArgs.casting_time.length > 0) { filters.push({ casting_time: { $in: validatedArgs.casting_time } }) } if (validatedArgs.area_of_effect) { if (validatedArgs.area_of_effect.type && validatedArgs.area_of_effect.type.length > 0) { filters.push({ 'area_of_effect.type': { $in: validatedArgs.area_of_effect.type } }) } if (validatedArgs.area_of_effect.size) { const sizeFilter = buildMongoQueryFromNumberFilter(validatedArgs.area_of_effect.size) if (sizeFilter) { filters.push({ 'area_of_effect.size': sizeFilter }) } } } if (validatedArgs.damage_type && validatedArgs.damage_type.length > 0) { filters.push({ 'damage.damage_type.index': { $in: validatedArgs.damage_type } }) } if (validatedArgs.dc_type && validatedArgs.dc_type.length > 0) { filters.push({ 'dc.dc_type.index': { $in: validatedArgs.dc_type } }) } if (validatedArgs.range && validatedArgs.range.length > 0) { filters.push({ range: { $in: validatedArgs.range } }) } if (filters.length > 0) { query.where({ $and: filters }) } const sortQuery = buildSortPipeline({ order: validatedArgs.order, sortFieldMap: SPELL_SORT_FIELD_MAP, defaultSortField: SpellOrderField.NAME }) if (Object.keys(sortQuery).length > 0) { query.sort(sortQuery) } if (validatedArgs.skip !== undefined) { query.skip(validatedArgs.skip) } if (validatedArgs.limit !== undefined) { query.limit(validatedArgs.limit) } return query.lean() } @Query(() => Spell, { nullable: true, description: 'Gets a single spell by its index.' }) async spell(@Arg('index', () => String) indexInput: string): Promise { const { index } = SpellIndexArgsSchema.parse({ index: indexInput }) return SpellModel.findOne({ index }).lean() } @FieldResolver(() => [Class], { nullable: true }) async classes(@Root() spell: Spell): Promise { return resolveMultipleReferences(spell.classes, ClassModel) } @FieldResolver(() => MagicSchool, { nullable: true }) async school(@Root() spell: Spell): Promise { return resolveSingleReference(spell.school, MagicSchoolModel) } @FieldResolver(() => [Subclass], { nullable: true }) async subclasses(@Root() spell: Spell): Promise { return resolveMultipleReferences(spell.subclasses, SubclassModel) } @FieldResolver(() => [LevelValue], { nullable: true, description: 'Healing amount based on spell slot level, transformed from raw data.' }) async heal_at_slot_level(@Root() spell: Spell): Promise { return mapLevelObjectToArray(spell.heal_at_slot_level) } } @Resolver(SpellDamage) export class SpellDamageResolver { @FieldResolver(() => DamageType, { nullable: true }) async damage_type(@Root() spellDamage: SpellDamage): Promise { return resolveSingleReference(spellDamage.damage_type, DamageTypeModel) } @FieldResolver(() => [LevelValue]) async damage_at_slot_level(@Root() spellDamage: SpellDamage): Promise { return mapLevelObjectToArray(spellDamage.damage_at_slot_level) } @FieldResolver(() => [LevelValue], { nullable: true, description: 'Damage scaling based on character level, transformed from raw data.' }) async damage_at_character_level(@Root() spellDamage: SpellDamage): Promise { return mapLevelObjectToArray(spellDamage.damage_at_character_level) } } @Resolver(SpellDC) export class SpellDCResolver { @FieldResolver(() => AbilityScore, { nullable: true }) async dc_type(@Root() spellDC: SpellDC): Promise { return resolveSingleReference(spellDC.dc_type, AbilityScoreModel) } } ================================================ FILE: src/graphql/2014/resolvers/subclass/args.ts ================================================ import { ArgsType, Field, InputType, registerEnumType } from 'type-graphql' import { z } from 'zod' import { BaseFilterArgs, BaseFilterArgsSchema, BaseIndexArgsSchema, BaseOrderInterface } from '@/graphql/common/args' import { OrderByDirection } from '@/graphql/common/enums' export enum SubclassOrderField { NAME = 'name' } export const SUBCLASS_SORT_FIELD_MAP: Record = { [SubclassOrderField.NAME]: 'name' } registerEnumType(SubclassOrderField, { name: 'SubclassOrderField', description: 'Fields to sort Subclasses by' }) @InputType() export class SubclassOrder implements BaseOrderInterface { @Field(() => SubclassOrderField) by!: SubclassOrderField @Field(() => OrderByDirection) direction!: OrderByDirection @Field(() => SubclassOrder, { nullable: true }) then_by?: SubclassOrder } export const SubclassOrderSchema: z.ZodType = z.lazy(() => z.object({ by: z.nativeEnum(SubclassOrderField), direction: z.nativeEnum(OrderByDirection), then_by: SubclassOrderSchema.optional() }) ) export const SubclassArgsSchema = z.object({ ...BaseFilterArgsSchema.shape, order: SubclassOrderSchema.optional() }) export const SubclassIndexArgsSchema = BaseIndexArgsSchema @ArgsType() export class SubclassArgs extends BaseFilterArgs { @Field(() => SubclassOrder, { nullable: true, description: 'Specify sorting order for subclasses.' }) order?: SubclassOrder } ================================================ FILE: src/graphql/2014/resolvers/subclass/resolver.ts ================================================ import { Arg, Args, FieldResolver, Query, Resolver, Root } from 'type-graphql' import { SubclassSpellPrerequisiteUnion } from '@/graphql/2014/types/subclassTypes' import { buildSortPipeline } from '@/graphql/common/args' import { resolveSingleReference } from '@/graphql/utils/resolvers' import ClassModel, { Class } from '@/models/2014/class' import FeatureModel, { Feature } from '@/models/2014/feature' import LevelModel, { Level } from '@/models/2014/level' import SpellModel, { Spell } from '@/models/2014/spell' import SubclassModel, { Subclass, SubclassSpell } from '@/models/2014/subclass' import { escapeRegExp } from '@/util' import { SUBCLASS_SORT_FIELD_MAP, SubclassArgs, SubclassArgsSchema, SubclassIndexArgsSchema, SubclassOrderField } from './args' @Resolver(Subclass) export class SubclassResolver { @Query(() => [Subclass], { description: 'Gets all subclasses, optionally filtered by name and sorted.' }) async subclasses(@Args(() => SubclassArgs) args: SubclassArgs): Promise { const validatedArgs = SubclassArgsSchema.parse(args) const query = SubclassModel.find() if (validatedArgs.name != null && validatedArgs.name !== '') { query.where({ name: { $regex: new RegExp(escapeRegExp(validatedArgs.name), 'i') } }) } const sortQuery = buildSortPipeline({ order: validatedArgs.order, sortFieldMap: SUBCLASS_SORT_FIELD_MAP, defaultSortField: SubclassOrderField.NAME }) if (Object.keys(sortQuery).length > 0) { query.sort(sortQuery) } if (validatedArgs.skip) { query.skip(validatedArgs.skip) } if (validatedArgs.limit) { query.limit(validatedArgs.limit) } return query.lean() } @Query(() => Subclass, { nullable: true, description: 'Gets a single subclass by its index.' }) async subclass(@Arg('index', () => String) indexInput: string): Promise { const { index } = SubclassIndexArgsSchema.parse({ index: indexInput }) return SubclassModel.findOne({ index }).lean() } @FieldResolver(() => Class, { nullable: true }) async class(@Root() subclass: Subclass): Promise { return resolveSingleReference(subclass.class, ClassModel) } @FieldResolver(() => [Level], { nullable: true }) async subclass_levels(@Root() subclass: Subclass): Promise { if (!subclass.index) return [] return LevelModel.find({ 'subclass.index': subclass.index }).sort({ level: 1 }).lean() } } @Resolver(SubclassSpell) export class SubclassSpellResolver { @FieldResolver(() => [SubclassSpellPrerequisiteUnion], { description: 'Resolves the prerequisites to actual Level or Feature objects.', nullable: true }) async prerequisites( @Root() subclassSpell: SubclassSpell ): Promise | null> { const prereqsData = subclassSpell.prerequisites if (prereqsData.length === 0) { return null } const resolvedPrereqs: Array = [] for (const prereq of prereqsData) { if (prereq.type === 'level') { const level = await LevelModel.findOne({ index: prereq.index }).lean() if (level !== null) { resolvedPrereqs.push(level) } } else if (prereq.type === 'feature') { const feature = await FeatureModel.findOne({ index: prereq.index }).lean() if (feature !== null) { resolvedPrereqs.push(feature) } } } return resolvedPrereqs.length > 0 ? resolvedPrereqs : null } @FieldResolver(() => Spell, { description: 'The spell gained.', nullable: false }) async spell( @Root() subclassSpell: SubclassSpell ): Promise { return SpellModel.findOne({ 'index': subclassSpell.spell.index }).lean() } } ================================================ FILE: src/graphql/2014/resolvers/subrace/args.ts ================================================ import { ArgsType, Field, InputType, registerEnumType } from 'type-graphql' import { z } from 'zod' import { BaseFilterArgs, BaseFilterArgsSchema, BaseIndexArgsSchema, BaseOrderInterface } from '@/graphql/common/args' import { OrderByDirection } from '@/graphql/common/enums' export enum SubraceOrderField { NAME = 'name' } export const SUBRACE_SORT_FIELD_MAP: Record = { [SubraceOrderField.NAME]: 'name' } registerEnumType(SubraceOrderField, { name: 'SubraceOrderField', description: 'Fields to sort Subraces by' }) @InputType() export class SubraceOrder implements BaseOrderInterface { @Field(() => SubraceOrderField) by!: SubraceOrderField @Field(() => OrderByDirection) direction!: OrderByDirection @Field(() => SubraceOrder, { nullable: true }) then_by?: SubraceOrder } export const SubraceOrderSchema: z.ZodType = z.lazy(() => z.object({ by: z.nativeEnum(SubraceOrderField), direction: z.nativeEnum(OrderByDirection), then_by: SubraceOrderSchema.optional() }) ) export const SubraceArgsSchema = z.object({ ...BaseFilterArgsSchema.shape, order: SubraceOrderSchema.optional() }) export const SubraceIndexArgsSchema = BaseIndexArgsSchema @ArgsType() export class SubraceArgs extends BaseFilterArgs { @Field(() => SubraceOrder, { nullable: true, description: 'Specify sorting order for subraces.' }) order?: SubraceOrder } ================================================ FILE: src/graphql/2014/resolvers/subrace/resolver.ts ================================================ import { Arg, Args, FieldResolver, Query, Resolver, Root } from 'type-graphql' import { buildSortPipeline } from '@/graphql/common/args' import { resolveMultipleReferences, resolveSingleReference } from '@/graphql/utils/resolvers' import AbilityScoreModel, { AbilityScore } from '@/models/2014/abilityScore' import RaceModel, { Race } from '@/models/2014/race' import SubraceModel, { Subrace, SubraceAbilityBonus } from '@/models/2014/subrace' import TraitModel, { Trait } from '@/models/2014/trait' import { escapeRegExp } from '@/util' import { SUBRACE_SORT_FIELD_MAP, SubraceArgs, SubraceArgsSchema, SubraceIndexArgsSchema, SubraceOrderField } from './args' @Resolver(Subrace) export class SubraceResolver { @Query(() => [Subrace], { description: 'Gets all subraces, optionally filtered by name and sorted by name.' }) async subraces(@Args(() => SubraceArgs) args: SubraceArgs): Promise { const validatedArgs = SubraceArgsSchema.parse(args) const query = SubraceModel.find() if (validatedArgs.name != null && validatedArgs.name !== '') { query.where({ name: { $regex: new RegExp(escapeRegExp(validatedArgs.name), 'i') } }) } const sortQuery = buildSortPipeline({ order: validatedArgs.order, sortFieldMap: SUBRACE_SORT_FIELD_MAP, defaultSortField: SubraceOrderField.NAME }) if (Object.keys(sortQuery).length > 0) { query.sort(sortQuery) } if (validatedArgs.skip !== undefined) { query.skip(validatedArgs.skip) } if (validatedArgs.limit !== undefined) { query.limit(validatedArgs.limit) } return query.lean() } @Query(() => Subrace, { nullable: true, description: 'Gets a single subrace by index.' }) async subrace(@Arg('index', () => String) indexInput: string): Promise { const { index } = SubraceIndexArgsSchema.parse({ index: indexInput }) return SubraceModel.findOne({ index }).lean() } @FieldResolver(() => Race, { nullable: true }) async race(@Root() subrace: Subrace): Promise { return resolveSingleReference(subrace.race, RaceModel) } @FieldResolver(() => [Trait], { nullable: true }) async racial_traits(@Root() subrace: Subrace): Promise { return resolveMultipleReferences(subrace.racial_traits, TraitModel) } } @Resolver(SubraceAbilityBonus) export class SubraceAbilityBonusResolver { @FieldResolver(() => AbilityScore, { nullable: true }) async ability_score( @Root() subraceAbilityBonus: SubraceAbilityBonus ): Promise { return resolveSingleReference(subraceAbilityBonus.ability_score, AbilityScoreModel) } } ================================================ FILE: src/graphql/2014/resolvers/trait/args.ts ================================================ import { ArgsType, Field, InputType, registerEnumType } from 'type-graphql' import { z } from 'zod' import { BaseFilterArgs, BaseFilterArgsSchema, BaseIndexArgsSchema, BaseOrderInterface } from '@/graphql/common/args' import { OrderByDirection } from '@/graphql/common/enums' export enum TraitOrderField { NAME = 'name' } export const TRAIT_SORT_FIELD_MAP: Record = { [TraitOrderField.NAME]: 'name' } registerEnumType(TraitOrderField, { name: 'TraitOrderField', description: 'Fields to sort Traits by' }) @InputType() export class TraitOrder implements BaseOrderInterface { @Field(() => TraitOrderField) by!: TraitOrderField @Field(() => OrderByDirection) direction!: OrderByDirection @Field(() => TraitOrder, { nullable: true }) then_by?: TraitOrder } export const TraitOrderSchema: z.ZodType = z.lazy(() => z.object({ by: z.nativeEnum(TraitOrderField), direction: z.nativeEnum(OrderByDirection), then_by: TraitOrderSchema.optional() }) ) export const TraitArgsSchema = z.object({ ...BaseFilterArgsSchema.shape, order: TraitOrderSchema.optional() }) export const TraitIndexArgsSchema = BaseIndexArgsSchema @ArgsType() export class TraitArgs extends BaseFilterArgs { @Field(() => TraitOrder, { nullable: true, description: 'Specify sorting order for traits.' }) order?: TraitOrder } ================================================ FILE: src/graphql/2014/resolvers/trait/resolver.ts ================================================ import { Arg, Args, FieldResolver, Query, Resolver, Root } from 'type-graphql' import { LanguageChoice, ProficiencyChoice } from '@/graphql/2014/common/choiceTypes' import { SpellChoice, SpellChoiceOption, SpellChoiceOptionSet, TraitChoice, TraitChoiceOption, TraitChoiceOptionSet } from '@/graphql/2014/types/traitTypes' import { mapLevelObjectToArray } from '@/graphql/2014/utils/helpers' import { resolveLanguageChoice, resolveProficiencyChoice } from '@/graphql/2014/utils/resolvers' import { buildSortPipeline } from '@/graphql/common/args' import { LevelValue } from '@/graphql/common/types' import { resolveMultipleReferences, resolveSingleReference, resolveReferenceOptionArray } from '@/graphql/utils/resolvers' import DamageTypeModel, { DamageType } from '@/models/2014/damageType' import ProficiencyModel, { Proficiency } from '@/models/2014/proficiency' import RaceModel, { Race } from '@/models/2014/race' import SpellModel from '@/models/2014/spell' import SubraceModel, { Subrace } from '@/models/2014/subrace' import TraitModel, { ActionDamage, Trait, TraitSpecific } from '@/models/2014/trait' import { Choice, OptionsArrayOptionSet } from '@/models/common/choice' import { escapeRegExp } from '@/util' import { TRAIT_SORT_FIELD_MAP, TraitArgs, TraitArgsSchema, TraitIndexArgsSchema, TraitOrderField } from './args' @Resolver(Trait) export class TraitResolver { @Query(() => [Trait], { description: 'Gets all traits, optionally filtered by name and sorted by name.' }) async traits(@Args(() => TraitArgs) args: TraitArgs): Promise { const validatedArgs = TraitArgsSchema.parse(args) const query = TraitModel.find() if (validatedArgs.name != null && validatedArgs.name !== '') { query.where({ name: { $regex: new RegExp(escapeRegExp(validatedArgs.name), 'i') } }) } const sortQuery = buildSortPipeline({ order: validatedArgs.order, sortFieldMap: TRAIT_SORT_FIELD_MAP, defaultSortField: TraitOrderField.NAME }) if (Object.keys(sortQuery).length > 0) { query.sort(sortQuery) } if (validatedArgs.skip !== undefined) { query.skip(validatedArgs.skip) } if (validatedArgs.limit !== undefined) { query.limit(validatedArgs.limit) } return query.lean() } @Query(() => Trait, { nullable: true, description: 'Gets a single trait by index.' }) async trait(@Arg('index', () => String) indexInput: string): Promise { const { index } = TraitIndexArgsSchema.parse({ index: indexInput }) return TraitModel.findOne({ index }).lean() } @FieldResolver(() => [Proficiency], { nullable: true }) async proficiencies(@Root() trait: Trait): Promise { return resolveMultipleReferences(trait.proficiencies, ProficiencyModel) } @FieldResolver(() => [Race], { nullable: true }) async races(@Root() trait: Trait): Promise { return resolveMultipleReferences(trait.races, RaceModel) } @FieldResolver(() => [Subrace], { nullable: true }) async subraces(@Root() trait: Trait): Promise { return resolveMultipleReferences(trait.subraces, SubraceModel) } @FieldResolver(() => Trait, { nullable: true }) async parent(@Root() trait: Trait): Promise { return resolveSingleReference(trait.parent, TraitModel) } @FieldResolver(() => LanguageChoice, { nullable: true }) async language_options(@Root() trait: Trait): Promise { return resolveLanguageChoice(trait.language_options as Choice) } @FieldResolver(() => ProficiencyChoice, { nullable: true }) async proficiency_choices(@Root() trait: Trait): Promise { return resolveProficiencyChoice(trait.proficiency_choices) } } // Separate resolver for nested TraitSpecific type @Resolver(TraitSpecific) export class TraitSpecificResolver { @FieldResolver(() => DamageType, { nullable: true }) async damage_type(@Root() traitSpecific: TraitSpecific): Promise { return resolveSingleReference(traitSpecific.damage_type, DamageTypeModel) } @FieldResolver(() => TraitChoice, { nullable: true }) async subtrait_options(@Root() traitSpecific: TraitSpecific) { return resolveTraitChoice(traitSpecific.subtrait_options) } @FieldResolver(() => SpellChoice, { nullable: true }) async spell_options(@Root() traitSpecific: TraitSpecific) { return resolveSpellChoice(traitSpecific.spell_options) } } @Resolver(ActionDamage) export class ActionDamageResolver { @FieldResolver(() => [LevelValue], { nullable: true, description: 'Damage scaling based on character level, transformed from the raw data object.' }) async damage_at_character_level( @Root() actionDamage: ActionDamage ): Promise { return mapLevelObjectToArray(actionDamage.damage_at_character_level) } } async function resolveTraitChoice( choiceData: Choice | undefined | null ): Promise { if (!choiceData) { return null } const optionsArraySet = choiceData.from as OptionsArrayOptionSet const gqlEmbeddedOptions = await resolveReferenceOptionArray( optionsArraySet, TraitModel, (item, optionType) => ({ option_type: optionType, item }) as TraitChoiceOption ) const gqlOptionSet: TraitChoiceOptionSet = { option_set_type: choiceData.from.option_set_type, options: gqlEmbeddedOptions } return { choose: choiceData.choose, type: choiceData.type, from: gqlOptionSet } } async function resolveSpellChoice( choiceData: Choice | undefined | null ): Promise { if (!choiceData) { return null } const optionsArraySet = choiceData.from as OptionsArrayOptionSet const gqlEmbeddedOptions = await resolveReferenceOptionArray( optionsArraySet, SpellModel, (item, optionType) => ({ option_type: optionType, item }) as SpellChoiceOption ) const gqlOptionSet: SpellChoiceOptionSet = { option_set_type: choiceData.from.option_set_type, options: gqlEmbeddedOptions } return { choose: choiceData.choose, type: choiceData.type, from: gqlOptionSet } } ================================================ FILE: src/graphql/2014/resolvers/weaponProperty/args.ts ================================================ import { ArgsType, Field, InputType, registerEnumType } from 'type-graphql' import { z } from 'zod' import { BaseFilterArgs, BaseFilterArgsSchema, BaseIndexArgsSchema, BaseOrderInterface } from '@/graphql/common/args' import { OrderByDirection } from '@/graphql/common/enums' export enum WeaponPropertyOrderField { NAME = 'name' } export const WEAPON_PROPERTY_SORT_FIELD_MAP: Record = { [WeaponPropertyOrderField.NAME]: 'name' } registerEnumType(WeaponPropertyOrderField, { name: 'WeaponPropertyOrderField', description: 'Fields to sort Weapon Properties by' }) @InputType() export class WeaponPropertyOrder implements BaseOrderInterface { @Field(() => WeaponPropertyOrderField) by!: WeaponPropertyOrderField @Field(() => OrderByDirection) direction!: OrderByDirection @Field(() => WeaponPropertyOrder, { nullable: true }) then_by?: WeaponPropertyOrder } export const WeaponPropertyOrderSchema: z.ZodType = z.lazy(() => z.object({ by: z.nativeEnum(WeaponPropertyOrderField), direction: z.nativeEnum(OrderByDirection), then_by: WeaponPropertyOrderSchema.optional() }) ) export const WeaponPropertyArgsSchema = z.object({ ...BaseFilterArgsSchema.shape, order: WeaponPropertyOrderSchema.optional() }) export const WeaponPropertyIndexArgsSchema = BaseIndexArgsSchema @ArgsType() export class WeaponPropertyArgs extends BaseFilterArgs { @Field(() => WeaponPropertyOrder, { nullable: true, description: 'Specify sorting order for weapon properties.' }) order?: WeaponPropertyOrder } ================================================ FILE: src/graphql/2014/resolvers/weaponProperty/resolver.ts ================================================ import { Arg, Args, Query, Resolver } from 'type-graphql' import { buildSortPipeline } from '@/graphql/common/args' import WeaponPropertyModel, { WeaponProperty } from '@/models/2014/weaponProperty' import { escapeRegExp } from '@/util' import { WEAPON_PROPERTY_SORT_FIELD_MAP, WeaponPropertyArgs, WeaponPropertyArgsSchema, WeaponPropertyIndexArgsSchema, WeaponPropertyOrderField } from './args' @Resolver(WeaponProperty) export class WeaponPropertyResolver { @Query(() => [WeaponProperty], { description: 'Gets all weapon properties, optionally filtered by name and sorted by name.' }) async weaponProperties( @Args(() => WeaponPropertyArgs) args: WeaponPropertyArgs ): Promise { const validatedArgs = WeaponPropertyArgsSchema.parse(args) const query = WeaponPropertyModel.find() if (validatedArgs.name != null && validatedArgs.name !== '') { query.where({ name: { $regex: new RegExp(escapeRegExp(validatedArgs.name), 'i') } }) } const sortQuery = buildSortPipeline({ order: validatedArgs.order, sortFieldMap: WEAPON_PROPERTY_SORT_FIELD_MAP, defaultSortField: WeaponPropertyOrderField.NAME }) if (Object.keys(sortQuery).length > 0) { query.sort(sortQuery) } if (validatedArgs.skip) { query.skip(validatedArgs.skip) } if (validatedArgs.limit) { query.limit(validatedArgs.limit) } return query.lean() } @Query(() => WeaponProperty, { nullable: true, description: 'Gets a single weapon property by index.' }) async weaponProperty( @Arg('index', () => String) indexInput: string ): Promise { const { index } = WeaponPropertyIndexArgsSchema.parse({ index: indexInput }) return WeaponPropertyModel.findOne({ index }).lean() } } ================================================ FILE: src/graphql/2014/types/backgroundTypes.ts ================================================ import { Field, Int, ObjectType } from 'type-graphql' import { Alignment } from '@/models/2014/alignment' // --- Background Ideal Choice Types --- @ObjectType({ description: 'Represents a single ideal option for a background.' }) export class IdealOption { @Field(() => String, { description: 'The type of the ideal option (e.g., ideal).' }) option_type!: string @Field(() => String, { description: 'The description of the ideal.' }) desc!: string @Field(() => [Alignment], { description: 'Alignments associated with this ideal.' }) alignments!: Alignment[] } @ObjectType({ description: 'Represents a set of ideal options for a background.' }) export class IdealOptionSet { @Field(() => String, { description: 'The type of the ideal option set (e.g., options_array).' }) option_set_type!: string @Field(() => [IdealOption], { description: 'The list of ideal options available.' }) options!: IdealOption[] } @ObjectType({ description: 'Represents the choice structure for background ideals.' }) export class IdealChoice { @Field(() => Int, { description: 'The number of ideals to choose from this list.' }) choose!: number @Field(() => String, { description: 'The type of choice (e.g., ideals).' }) type!: string @Field(() => IdealOptionSet, { description: 'The set of ideal options available.' }) from!: IdealOptionSet } ================================================ FILE: src/graphql/2014/types/featureTypes.ts ================================================ import { createUnionType } from 'type-graphql' import { FeaturePrerequisite, LevelPrerequisite, SpellPrerequisite } from '@/models/2014/feature' export const FeaturePrerequisiteUnion = createUnionType({ name: 'FeaturePrerequisiteUnion', types: () => [LevelPrerequisite, FeaturePrerequisite, SpellPrerequisite] as const, resolveType: (value) => { if (value.type === 'level') { return LevelPrerequisite } if (value.type === 'feature') { return FeaturePrerequisite } if (value.type === 'spell') { return SpellPrerequisite } console.warn('Could not resolve type for FeaturePrerequisiteUnion:', value) throw new Error('Could not resolve type for FeaturePrerequisiteUnion') } }) ================================================ FILE: src/graphql/2014/types/monsterTypes.ts ================================================ import { createUnionType, Field, Int, ObjectType } from 'type-graphql' import { ArmorClassArmor, ArmorClassCondition, ArmorClassDex, ArmorClassNatural, ArmorClassSpell } from '@/models/2014/monster' import { Damage } from '@/models/common/damage' import { DifficultyClass } from '@/models/common/difficultyClass' // --- Breath Choice Types --- @ObjectType({ description: 'A single breath option within a breath choice' }) export class BreathChoiceOption { @Field(() => String, { description: 'The type of option (e.g., breath).' }) option_type!: string @Field(() => String, { description: 'The name of the breath option.' }) name!: string @Field(() => DifficultyClass, { description: 'The difficulty class for the breath.' }) dc!: DifficultyClass @Field(() => [Damage], { nullable: true, description: 'The damage dealt by the breath.' }) damage?: Damage[] } @ObjectType({ description: 'A set of breath options to choose from' }) export class BreathChoiceOptionSet { @Field(() => String, { description: 'The type of option set.' }) option_set_type!: string @Field(() => [BreathChoiceOption], { description: 'The available breath options.' }) options!: BreathChoiceOption[] } @ObjectType({ description: 'A choice of breath options for a monster action' }) export class BreathChoice { @Field(() => Int, { description: 'Number of breath options to choose.' }) choose!: number @Field(() => String, { description: 'Type of breath options to choose from.' }) type!: string @Field(() => BreathChoiceOptionSet, { description: 'The options to choose from.' }) from!: BreathChoiceOptionSet @Field(() => String, { nullable: true, description: 'Description of the breath choice.' }) desc?: string } // --- Damage Choice Types --- @ObjectType({ description: 'A single damage option in a damage choice' }) export class DamageChoiceOption { @Field(() => String, { description: 'The type of option.' }) option_type!: string @Field(() => Damage, { description: 'The damage for this option.' }) damage!: Damage } @ObjectType({ description: 'A set of damage options' }) export class DamageChoiceOptionSet { @Field(() => String, { description: 'The type of option set.' }) option_set_type!: string @Field(() => [DamageChoiceOption], { description: 'The options in this set.' }) options!: DamageChoiceOption[] } @ObjectType({ description: 'A choice of damage options' }) export class DamageChoice { @Field(() => Number, { description: 'The number of options to choose.' }) choose!: number @Field(() => String, { description: 'The type of choice.' }) type!: string @Field(() => DamageChoiceOptionSet, { description: 'The options to choose from.' }) from!: DamageChoiceOptionSet @Field(() => String, { nullable: true, description: 'The description of the choice.' }) desc?: string } // --- Action Option Choice Types --- @ObjectType({ description: 'A single action option within a choice' }) export class ActionChoiceOption { @Field(() => String, { description: 'The type of option.' }) option_type!: string @Field(() => String, { description: 'The name of the action.' }) action_name!: string @Field(() => Int, { description: 'Number of times the action can be used.' }) count!: number @Field(() => String, { description: 'The type of action.' }) type!: 'melee' | 'ranged' | 'ability' | 'magic' @Field(() => String, { nullable: true, description: 'Additional notes about the action.' }) notes?: string } @ObjectType({ description: 'A multiple action option containing a set of actions' }) export class MultipleActionChoiceOption { @Field(() => String, { description: 'The type of option.' }) option_type!: string @Field(() => [ActionChoiceOption], { description: 'The set of actions in this option.' }) items!: ActionChoiceOption[] } @ObjectType({ description: 'A set of action options to choose from' }) export class ActionChoiceOptionSet { @Field(() => String, { description: 'The type of option set.' }) option_set_type!: string @Field(() => [ActionOptionUnion], { description: 'The available options.' }) options!: Array } @ObjectType({ description: 'A choice of actions for a monster' }) export class ActionChoice { @Field(() => Int, { description: 'Number of actions to choose.' }) choose!: number @Field(() => String, { description: 'Type of actions to choose from.' }) type!: string @Field(() => ActionChoiceOptionSet, { description: 'The options to choose from.' }) from!: ActionChoiceOptionSet @Field(() => String, { nullable: true, description: 'Description of the action choice.' }) desc?: string } // --- Unions --- export const ActionOptionUnion = createUnionType({ name: 'ActionOptionUnion', types: () => [ActionChoiceOption, MultipleActionChoiceOption], resolveType(value) { if ('items' in value) { return MultipleActionChoiceOption } return ActionChoiceOption } }) export const DamageOrDamageChoiceUnion = createUnionType({ name: 'DamageOrDamageChoice', types: () => [Damage, DamageChoice], resolveType: (value: Damage | DamageChoice) => { if ('choose' in value) { return DamageChoice } return Damage } }) export const MonsterArmorClassUnion = createUnionType({ name: 'MonsterArmorClass', types: () => [ ArmorClassDex, ArmorClassNatural, ArmorClassArmor, ArmorClassSpell, ArmorClassCondition ] as const, resolveType: (value: any) => { if (value == null || typeof value.type !== 'string') { console.warn('Cannot resolve MonsterArmorClass: type field is missing or invalid', value) throw new Error('Cannot resolve MonsterArmorClass: type field is missing or invalid') } switch (value.type) { case 'dex': return ArmorClassDex case 'natural': return ArmorClassNatural case 'armor': return ArmorClassArmor case 'spell': return ArmorClassSpell case 'condition': return ArmorClassCondition default: console.warn('Could not resolve type for MonsterArmorClassUnion:', value) throw new Error( 'Could not resolve type for MonsterArmorClassUnion: Unknown type ' + value.type ) } } }) ================================================ FILE: src/graphql/2014/types/proficiencyTypes.ts ================================================ import { createUnionType } from 'type-graphql' import { AbilityScore } from '@/models/2014/abilityScore' import { Equipment } from '@/models/2014/equipment' import { EquipmentCategory } from '@/models/2014/equipmentCategory' import { Skill } from '@/models/2014/skill' export const ProficiencyReference = createUnionType({ name: 'ProficiencyReference', types: () => [Equipment, EquipmentCategory, AbilityScore, Skill] as const, resolveType: (value) => { if ('equipment' in value) { return EquipmentCategory } if ('full_name' in value) { return AbilityScore } if ('desc' in value && Array.isArray(value.desc)) { return Skill } return Equipment } }) ================================================ FILE: src/graphql/2014/types/startingEquipment/choice.ts ================================================ import { Field, Int, ObjectType } from 'type-graphql' import { EquipmentCategorySet } from './common' import { EquipmentOptionSet, StartingEquipmentFromUnion } from './optionSet' @ObjectType({ description: 'Represents a choice for starting equipment.' }) export class StartingEquipmentChoice { @Field(() => Int, { description: 'The number of items or options to choose.' }) choose!: number @Field(() => String, { nullable: true, description: 'A description of the choice presented to the user.' }) desc?: string // desc can be optional based on some data models @Field(() => String, { description: "The type of choice, e.g., 'equipment'." }) type!: string // This will use a forward reference to StartingEquipmentFromUnion defined in optionSet.ts @Field(() => StartingEquipmentFromUnion, { description: 'The set of options or category to choose from.' }) from!: EquipmentCategorySet | EquipmentOptionSet } ================================================ FILE: src/graphql/2014/types/startingEquipment/common.ts ================================================ import { createUnionType, Field, Int, ObjectType } from 'type-graphql' import { Equipment } from '@/models/2014/equipment' import { EquipmentCategory } from '@/models/2014/equipmentCategory' import { Proficiency } from '@/models/2014/proficiency' @ObjectType({ description: 'A prerequisite for an equipment option, typically requiring a specific proficiency.' }) export class ProficiencyPrerequisite { @Field(() => String, { description: "The type of prerequisite, e.g., 'proficiency'." }) type!: string @Field(() => Proficiency, { description: 'The specific proficiency required.' }) proficiency!: Proficiency } @ObjectType({ description: 'Represents a specific piece of equipment with a quantity.' }) export class CountedReferenceOption { @Field(() => String, { description: "The type of this option, e.g., 'counted_reference'." }) option_type!: string // This helps in discriminating union types if used directly @Field(() => Int, { description: 'The quantity of the equipment.' }) count!: number @Field(() => Equipment, { description: 'The referenced equipment item.' }) of!: Equipment @Field(() => [ProficiencyPrerequisite], { nullable: true, description: 'Prerequisites for choosing this option.' }) prerequisites?: ProficiencyPrerequisite[] } // Definition for EquipmentCategorySet, moved from optionSet.ts and placed before its usage @ObjectType({ description: 'A set of equipment choices derived directly from an equipment category.' }) export class EquipmentCategorySet { @Field(() => String, { description: "Indicates the type of option set, e.g., 'equipment_category'." }) option_set_type!: string @Field(() => EquipmentCategory, { description: 'The equipment category to choose from.' }) equipment_category!: EquipmentCategory } // Describes the details of a choice that is specifically from an equipment category. // This is used inside EquipmentCategoryChoiceOption. @ObjectType({ description: 'Details of a choice limited to an equipment category.' }) export class EquipmentCategoryChoice { @Field(() => Int, { description: 'Number of items to choose from the category.' }) choose!: number @Field(() => String, { nullable: true, description: 'An optional description for this choice.' }) desc?: string @Field(() => String, { description: "Type of choice, e.g., 'equipment'." }) type!: string @Field(() => EquipmentCategorySet, { description: 'The equipment category to choose from.' }) from!: EquipmentCategorySet } @ObjectType({ description: 'An option that represents a choice from a single equipment category.' }) export class EquipmentCategoryChoiceOption { @Field(() => String, { description: "The type of this option, e.g., 'choice' or 'equipment_category_choice'." }) option_type!: string // Data might say 'choice', resolver will map to this type if structure matches @Field(() => EquipmentCategoryChoice, { description: 'The details of the choice from an equipment category.' }) choice!: EquipmentCategoryChoice } // This union represents the types of items that can be part of a 'multiple' bundle. export const MultipleItemUnion = createUnionType({ name: 'MultipleItemUnion', types: () => [CountedReferenceOption, EquipmentCategoryChoiceOption] as const, resolveType: (value) => { if (value.option_type === 'counted_reference') { return CountedReferenceOption } if (value.option_type === 'choice' || value.option_type === 'equipment_category_choice') { return EquipmentCategoryChoiceOption } return undefined } }) @ObjectType({ description: 'Represents a bundle of multiple equipment items or equipment category choices.' }) export class MultipleItemsOption { @Field(() => String, { description: "The type of this option, e.g., 'multiple'." }) option_type!: string @Field(() => [MultipleItemUnion], { description: 'The list of items or category choices included in this bundle.' }) items!: Array } ================================================ FILE: src/graphql/2014/types/startingEquipment/index.ts ================================================ export * from './choice' export * from './common' export * from './optionSet' ================================================ FILE: src/graphql/2014/types/startingEquipment/optionSet.ts ================================================ import { createUnionType, Field, ObjectType } from 'type-graphql' import { CountedReferenceOption, EquipmentCategoryChoiceOption, EquipmentCategorySet, MultipleItemsOption } from './common' @ObjectType({ description: 'A set of explicitly listed equipment options.' }) export class EquipmentOptionSet { @Field(() => String, { description: "Indicates the type of option set, e.g., 'options_array'." }) option_set_type!: string @Field(() => [EquipmentOptionUnion], { description: 'A list of specific equipment options.' }) options!: Array } // Union for the `from` field of StartingEquipmentChoice export const StartingEquipmentFromUnion = createUnionType({ name: 'StartingEquipmentFromUnion', types: () => [EquipmentCategorySet, EquipmentOptionSet] as const, resolveType: (value) => { if (value.option_set_type === 'equipment_category') { return EquipmentCategorySet } if (value.option_set_type === 'options_array') { return EquipmentOptionSet } return undefined } }) // Union for items within EquipmentOptionSet.options export const EquipmentOptionUnion = createUnionType({ name: 'EquipmentOptionUnion', types: () => [CountedReferenceOption, EquipmentCategoryChoiceOption, MultipleItemsOption] as const, resolveType: (value) => { if (value.option_type === 'counted_reference') { return CountedReferenceOption } if (value.option_type === 'choice' || value.option_type === 'equipment_category_choice') { return EquipmentCategoryChoiceOption } if (value.option_type === 'multiple') { return MultipleItemsOption } return undefined } }) ================================================ FILE: src/graphql/2014/types/subclassTypes.ts ================================================ import { createUnionType } from 'type-graphql' import { Feature } from '@/models/2014/feature' import { Level } from '@/models/2014/level' export const SubclassSpellPrerequisiteUnion = createUnionType({ name: 'SubclassSpellPrerequisite', types: () => [Level, Feature] as const, resolveType: (value) => { if ('prof_bonus' in value || 'spellcasting' in value || 'features' in value) { return Level } if ('subclass' in value || 'feature_specific' in value || 'prerequisites' in value) { return Feature } console.warn('Could not reliably resolve type for SubclassSpellPrerequisiteUnion:', value) throw new Error('Could not resolve type for SubclassSpellPrerequisiteUnion') } }) ================================================ FILE: src/graphql/2014/types/traitTypes.ts ================================================ import { Field, Int, ObjectType } from 'type-graphql' import { Spell } from '@/models/2014/spell' import { Trait } from '@/models/2014/trait' @ObjectType({ description: 'Represents a reference to a Trait within a choice option set.' }) export class TraitChoiceOption { @Field(() => String, { description: 'The type of this option (e.g., "reference").' }) option_type!: string @Field(() => Trait, { description: 'The resolved Trait object.' }) item!: Trait } @ObjectType({ description: 'Represents a set of Trait options for a choice.' }) export class TraitChoiceOptionSet { @Field(() => String, { description: 'The type of the option set (e.g., resource_list, options_array).' }) option_set_type!: string @Field(() => [TraitChoiceOption], { description: 'The list of Trait options available.' }) options!: TraitChoiceOption[] } @ObjectType({ description: 'Represents a choice from a list of Traits.' }) export class TraitChoice { @Field(() => Int, { description: 'The number of Traits to choose from this list.' }) choose!: number @Field(() => String, { description: 'The type of choice (e.g., subtraits).' }) type!: string @Field(() => TraitChoiceOptionSet, { description: 'The set of Trait options available.' }) from!: TraitChoiceOptionSet } @ObjectType({ description: 'Represents a reference to a Spell within a choice option set.' }) export class SpellChoiceOption { @Field(() => String, { description: 'The type of this option (e.g., "reference").' }) option_type!: string @Field(() => Spell, { description: 'The resolved Spell object.' }) item!: Spell } @ObjectType({ description: 'Represents a set of Spell options for a choice.' }) export class SpellChoiceOptionSet { @Field(() => String, { description: 'The type of the option set (e.g., resource_list, options_array).' }) option_set_type!: string @Field(() => [SpellChoiceOption], { description: 'The list of Spell options available.' }) options!: SpellChoiceOption[] } @ObjectType({ description: 'Represents a choice from a list of Spells.' }) export class SpellChoice { @Field(() => Int, { description: 'The number of Spells to choose from this list.' }) choose!: number @Field(() => String, { description: 'The type of choice (e.g., spells).' }) type!: string @Field(() => SpellChoiceOptionSet, { description: 'The set of Spell options available.' }) from!: SpellChoiceOptionSet } ================================================ FILE: src/graphql/2014/utils/helpers.ts ================================================ import { LevelValue } from '@/graphql/common/types' /** * Converts a Record or Record (where keys are number strings) * into an array of { level: number, value: string } objects, sorted by level. * Returns null if the input is invalid or empty. */ export const mapLevelObjectToArray = ( data: Record | Record | undefined ): LevelValue[] | null => { if (!data || typeof data !== 'object') { return null } const levelValueArray: LevelValue[] = [] const stringData = data as Record for (const levelKey in stringData) { if (Object.prototype.hasOwnProperty.call(stringData, levelKey)) { const level = parseInt(levelKey, 10) const value = stringData[levelKey] if (!isNaN(level) && typeof value === 'string') { levelValueArray.push({ level, value }) } } } levelValueArray.sort((a, b) => a.level - b.level) return levelValueArray.length > 0 ? levelValueArray : null } /** * Normalizes a count value (which can be a string or number) to a number. * Uses parseInt with radix 10 for strings. */ export function normalizeCount(count: string | number): number { if (typeof count === 'string') { const num = parseInt(count, 10) return isNaN(num) ? 0 : num } return count } ================================================ FILE: src/graphql/2014/utils/resolvers.ts ================================================ import { LanguageChoice, LanguageChoiceOption, LanguageChoiceOptionSet, ProficiencyChoice, ProficiencyChoiceOption, ProficiencyChoiceOptionSet } from '@/graphql/2014/common/choiceTypes' import { resolveSingleReference, resolveReferenceOptionArray } from '@/graphql/utils/resolvers' import LanguageModel, { Language } from '@/models/2014/language' import ProficiencyModel, { Proficiency } from '@/models/2014/proficiency' import { Choice, ChoiceOption, OptionsArrayOptionSet, ReferenceOption } from '@/models/common/choice' export async function resolveLanguageChoice( choiceData: Choice | null ): Promise { const gqlEmbeddedOptions: LanguageChoiceOption[] = [] if (!choiceData) { return null } if (choiceData.from.option_set_type === 'resource_list') { const allItems = (await LanguageModel.find({}).lean()) as Language[] for (const item of allItems) { gqlEmbeddedOptions.push({ option_type: 'reference', item: item }) } } else if (choiceData.from.option_set_type === 'options_array') { const optionsArraySet = choiceData.from as OptionsArrayOptionSet const resolvedOptions = await resolveReferenceOptionArray( optionsArraySet, LanguageModel, (item, optionType) => ({ option_type: optionType, item }) as LanguageChoiceOption ) gqlEmbeddedOptions.push(...resolvedOptions) } const gqlOptionSet: LanguageChoiceOptionSet = { option_set_type: choiceData.from.option_set_type, options: gqlEmbeddedOptions } return { choose: choiceData.choose, type: choiceData.type, from: gqlOptionSet } } export async function resolveProficiencyChoice( choiceData: Choice | undefined | null ): Promise { if (!choiceData || !choiceData.type) { return null } const gqlEmbeddedOptions: ProficiencyChoiceOption[] = [] const optionsArraySet = choiceData.from as OptionsArrayOptionSet for (const dbOption of optionsArraySet.options) { if (dbOption.option_type === 'choice') { // For nested choices, use ChoiceOption const choiceOpt = dbOption as ChoiceOption const nestedChoice = await resolveProficiencyChoice(choiceOpt.choice) if (nestedChoice) { gqlEmbeddedOptions.push({ option_type: choiceOpt.option_type, item: nestedChoice }) } } else { // Handle regular proficiency reference const dbRefOpt = dbOption as ReferenceOption const resolvedItem = await resolveSingleReference(dbRefOpt.item, ProficiencyModel) if (resolvedItem !== null) { gqlEmbeddedOptions.push({ option_type: dbRefOpt.option_type, item: resolvedItem as Proficiency }) } } } const gqlOptionSet: ProficiencyChoiceOptionSet = { option_set_type: choiceData.from.option_set_type, options: gqlEmbeddedOptions } return { choose: choiceData.choose, type: choiceData.type, from: gqlOptionSet, desc: choiceData.desc } } export async function resolveProficiencyChoiceArray( choices: Choice[] | undefined | null ): Promise { if (!choices || !Array.isArray(choices)) { return [] } const resolvedChoices: ProficiencyChoice[] = [] for (const choice of choices) { const resolvedChoice = await resolveProficiencyChoice(choice) if (resolvedChoice) { resolvedChoices.push(resolvedChoice) } } return resolvedChoices } ================================================ FILE: src/graphql/2014/utils/startingEquipmentResolver.ts ================================================ import EquipmentModel, { Equipment } from '@/models/2014/equipment' import EquipmentCategoryModel, { EquipmentCategory } from '@/models/2014/equipmentCategory' import ProficiencyModel, { Proficiency } from '@/models/2014/proficiency' import { Choice, ChoiceOption, CountedReferenceOption, EquipmentCategoryOptionSet, MultipleOption, OptionsArrayOptionSet, OptionSet } from '@/models/common/choice' import { CountedReferenceOption as ResolvedCountedReferenceOption, EquipmentCategoryChoiceOption, EquipmentCategorySet, EquipmentOptionSet, MultipleItemsOption, ProficiencyPrerequisite as ResolvedProficiencyPrerequisite, StartingEquipmentChoice } from '../types/startingEquipment' interface ProficiencyPrerequisite { type: string proficiency: { index: string; name: string; url: string } } // --- Main Resolver Function --- export async function resolveStartingEquipmentChoices( choices: Choice[] | undefined | null ): Promise { if (!choices) { return [] } const resolvedChoices: StartingEquipmentChoice[] = [] for (const choice of choices) { const resolvedChoice = await resolveStartingEquipmentChoice(choice) if (resolvedChoice) { resolvedChoices.push(resolvedChoice) } } return resolvedChoices } // --- Helper to map a single DB Choice to GraphQL StartingEquipmentChoice --- async function resolveStartingEquipmentChoice( choice: Choice ): Promise { const resolvedFrom = await resolveStartingEquipmentOptionSet(choice.from as OptionSet) if (!resolvedFrom) { return null } return { choose: choice.choose, desc: choice.desc, type: choice.type, from: resolvedFrom } } // Maps the 'from' part of a DB choice to either GQL EquipmentCategorySet or EquipmentOptionSet async function resolveStartingEquipmentOptionSet( optionSet: OptionSet ): Promise { if (optionSet.option_set_type === 'equipment_category') { const equipmentCategoryOptionSet = optionSet as EquipmentCategoryOptionSet const category = await EquipmentCategoryModel.findOne({ index: equipmentCategoryOptionSet.equipment_category.index }).lean() if (!category) return null return { option_set_type: equipmentCategoryOptionSet.option_set_type, equipment_category: category as EquipmentCategory } as EquipmentCategorySet } else if (optionSet.option_set_type === 'options_array') { return await resolveEquipmentOptionSet(optionSet as OptionsArrayOptionSet) } return null } async function resolveEquipmentOptionSet( optionSet: OptionsArrayOptionSet ): Promise { const resolvedOptions: Array< ResolvedCountedReferenceOption | EquipmentCategoryChoiceOption | MultipleItemsOption > = [] for (const option of optionSet.options) { const resolvedOption = await resolveEquipmentOptionUnion( option as CountedReferenceOption | ChoiceOption | MultipleOption ) if (resolvedOption) { resolvedOptions.push(resolvedOption) } } return { option_set_type: optionSet.option_set_type, options: resolvedOptions } as EquipmentOptionSet } async function resolveEquipmentOptionUnion( option: CountedReferenceOption | ChoiceOption | MultipleOption ): Promise< ResolvedCountedReferenceOption | EquipmentCategoryChoiceOption | MultipleItemsOption | null > { if (!option.option_type) return null switch (option.option_type) { case 'counted_reference': return resolveCountedReferenceOption(option as CountedReferenceOption) case 'choice': return resolveEquipmentCategoryChoiceOption(option as ChoiceOption) case 'multiple': return resolveMultipleItemsOption(option as MultipleOption) default: console.warn(`Unknown option.option_type: ${option.option_type}`) return null } } async function resolveProficiencyPrerequisites( prerequisites: ProficiencyPrerequisite[] | undefined ): Promise { if (!prerequisites || prerequisites.length === 0) { return [] } const resolvedPrerequisites: ResolvedProficiencyPrerequisite[] = [] for (const prereq of prerequisites) { const proficiency = await ProficiencyModel.findOne({ index: prereq.proficiency.index }).lean() if (proficiency) { resolvedPrerequisites.push({ type: prereq.type, proficiency: proficiency as Proficiency }) } } return resolvedPrerequisites } async function resolveCountedReferenceOption( countedReferenceOption: CountedReferenceOption ): Promise { if (!countedReferenceOption.of.index) return null const equipment = await EquipmentModel.findOne({ index: countedReferenceOption.of.index }).lean() if (!equipment) return null const resolvedPrerequisites = await resolveProficiencyPrerequisites( countedReferenceOption.prerequisites as ProficiencyPrerequisite[] | undefined ) return { option_type: countedReferenceOption.option_type, count: countedReferenceOption.count, of: equipment as Equipment, prerequisites: resolvedPrerequisites.length > 0 ? resolvedPrerequisites : undefined } as ResolvedCountedReferenceOption } async function resolveEquipmentCategoryChoiceOption( choiceOption: ChoiceOption ): Promise { const nestedChoice = choiceOption.choice const nestedFrom = nestedChoice.from as EquipmentCategoryOptionSet if (nestedFrom.option_set_type !== 'equipment_category' || !nestedFrom.equipment_category.index) { console.warn('ChoiceOption.choice.from is not a valid EquipmentCategoryOptionSet:', nestedFrom) return null } const equipmentCategory = await EquipmentCategoryModel.findOne({ index: nestedFrom.equipment_category.index }).lean() if (!equipmentCategory) { console.warn(`Equipment category not found: ${nestedFrom.equipment_category.index}`) return null } return { option_type: choiceOption.option_type, choice: { choose: nestedChoice.choose, desc: nestedChoice.desc, type: nestedChoice.type, from: { option_set_type: nestedFrom.option_set_type, equipment_category: equipmentCategory as EquipmentCategory } as EquipmentCategorySet } } as EquipmentCategoryChoiceOption } async function resolveMultipleItemsOption( multipleOption: MultipleOption ): Promise { if (multipleOption.items.length === 0) { console.warn('Invalid MultipleOption data:', multipleOption) return null } const resolvedItems: Array = [] for (const item of multipleOption.items) { if (!item.option_type) { console.warn('Invalid item within MultipleOption items array:', item) continue } let resolvedItem: ResolvedCountedReferenceOption | EquipmentCategoryChoiceOption | null = null if (item.option_type === 'counted_reference') { resolvedItem = await resolveCountedReferenceOption(item as CountedReferenceOption) } else if (item.option_type === 'choice') { resolvedItem = await resolveEquipmentCategoryChoiceOption(item as ChoiceOption) } else { console.warn(`Unknown option_type within MultipleOption items: ${item.option_type}`) } if (resolvedItem) { resolvedItems.push(resolvedItem) } } if (resolvedItems.length === 0 && multipleOption.items.length > 0) { console.warn('All items within MultipleOption failed to resolve:', multipleOption) return null } return { option_type: multipleOption.option_type, items: resolvedItems } as MultipleItemsOption } ================================================ FILE: src/graphql/2024/common/choiceTypes.ts ================================================ import { Field, Int, ObjectType } from 'type-graphql' import { AbilityScore2024 } from '@/models/2024/abilityScore' import { Proficiency2024 } from '@/models/2024/proficiency' // --- Score Prerequisite Choice Types (for Feat2024.prerequisite_options) --- @ObjectType({ description: 'A score prerequisite option within a feat prerequisite choice.' }) export class ScorePrerequisiteOption2024 { @Field(() => String, { description: 'The type of this option.' }) option_type!: string @Field(() => AbilityScore2024, { description: 'The ability score required.' }) ability_score!: AbilityScore2024 @Field(() => Int, { description: 'The minimum score required.' }) minimum_score!: number } @ObjectType({ description: 'The set of score prerequisite options for a feat.' }) export class ScorePrerequisiteOptionSet2024 { @Field(() => String, { description: 'The type of the option set.' }) option_set_type!: string @Field(() => [ScorePrerequisiteOption2024], { description: 'The available prerequisite options.' }) options!: ScorePrerequisiteOption2024[] } @ObjectType({ description: 'A prerequisite choice for a 2024 feat (ability score requirements).' }) export class ScorePrerequisiteChoice2024 { @Field(() => String, { nullable: true, description: 'Description of the prerequisite choice.' }) desc?: string @Field(() => Int, { description: 'Number of options to choose.' }) choose!: number @Field(() => String, { description: 'The type of choice.' }) type!: string @Field(() => ScorePrerequisiteOptionSet2024, { description: 'The set of options.' }) from!: ScorePrerequisiteOptionSet2024 } // --- Proficiency Choice Types (for Background2024.proficiency_choices) --- @ObjectType({ description: 'A reference to a 2024 proficiency within a choice option set.' }) export class Proficiency2024ChoiceOption { @Field(() => String, { description: 'The type of this option.' }) option_type!: string @Field(() => Proficiency2024, { description: 'The resolved Proficiency2024 object.' }) item!: Proficiency2024 } @ObjectType({ description: 'The set of proficiency options for a background choice.' }) export class Proficiency2024ChoiceOptionSet { @Field(() => String, { description: 'The type of the option set.' }) option_set_type!: string @Field(() => [Proficiency2024ChoiceOption], { description: 'The available proficiency options.' }) options!: Proficiency2024ChoiceOption[] } @ObjectType({ description: 'A proficiency choice for a 2024 background.' }) export class Proficiency2024Choice { @Field(() => String, { nullable: true, description: 'Description of the choice.' }) desc?: string @Field(() => Int, { description: 'Number of proficiencies to choose.' }) choose!: number @Field(() => String, { description: 'The type of choice.' }) type!: string @Field(() => Proficiency2024ChoiceOptionSet, { description: 'The set of proficiency options.' }) from!: Proficiency2024ChoiceOptionSet } ================================================ FILE: src/graphql/2024/common/equipmentTypes.ts ================================================ import { Field, Int, ObjectType } from 'type-graphql' import { AbilityScore2024 } from '@/models/2024/abilityScore' import { ArmorClass, Content, Equipment2024, Range, ThrowRange, Utilize } from '@/models/2024/equipment' import { WeaponProperty2024 } from '@/models/2024/weaponProperty' import { APIReference } from '@/models/common/apiReference' import { Damage } from '@/models/common/damage' import { IEquipment } from './interfaces' import { AnyEquipment } from './unions' @ObjectType({ description: 'Represents Armor equipment', implements: IEquipment }) export class Armor extends Equipment2024 { @Field(() => ArmorClass, { description: 'Armor export Class details for this armor.' }) declare armor_class: ArmorClass @Field(() => String, { description: 'Time to doff the armor.' }) declare doff_time: string @Field(() => String, { description: 'Time to don the armor.' }) declare don_time: string @Field(() => Int, { nullable: true, description: 'Minimum Strength score required to use this armor effectively.' }) declare str_minimum: number @Field(() => Boolean, { nullable: true, description: 'Whether wearing the armor imposes disadvantage on Stealth checks.' }) declare stealth_disadvantage: boolean } @ObjectType({ description: 'Represents Weapon equipment', implements: IEquipment }) export class Weapon extends Equipment2024 { @Field(() => Damage, { nullable: true, description: 'Primary damage dealt by the weapon.' }) declare damage?: Damage @Field(() => Damage, { nullable: true, description: 'Damage dealt when using the weapon with two hands.' }) declare two_handed_damage?: Damage @Field(() => Range, { nullable: true, description: 'Weapon range details.' }) declare range?: Range @Field(() => ThrowRange, { nullable: true, description: 'Range when the weapon is thrown.' }) declare throw_range?: ThrowRange @Field(() => [WeaponProperty2024], { nullable: true, description: 'Properties of the weapon.' }) declare properties?: APIReference[] // Resolved externally } @ObjectType({ description: 'Represents Gear equipment (general purpose)', implements: IEquipment }) export class AdventuringGear extends Equipment2024 {} @ObjectType({ description: "Represents Gear that contains other items (e.g., Explorer's Pack)", implements: IEquipment }) export class Pack extends AdventuringGear { @Field(() => [Content], { nullable: true, description: 'Items contained within the pack.' }) declare contents?: Content[] } @ObjectType({ description: 'Represents Ammunition equipment', implements: IEquipment }) export class Ammunition extends AdventuringGear { @Field(() => Int, { description: 'Quantity of ammunition in the bundle.' }) declare quantity: number @Field(() => Equipment2024, { nullable: true, description: 'Storage of the ammunition.' }) declare storage?: APIReference } @ObjectType({ description: 'Represents Tool equipment', implements: IEquipment }) export class Tool extends Equipment2024 { @Field(() => AbilityScore2024, { nullable: true, description: 'Ability score required to use the tool.' }) declare ability?: APIReference @Field(() => [AnyEquipment], { nullable: true, description: 'Equipment that can be crafted with the tool.' }) declare craft?: APIReference[] @Field(() => [Utilize], { nullable: true, description: 'How to utilize the tool.' }) declare utilize?: Utilize[] } ================================================ FILE: src/graphql/2024/common/interfaces.ts ================================================ import { Field, Float, InterfaceType } from 'type-graphql' import { Cost } from '@/models/2024/equipment' @InterfaceType({ description: 'Common fields shared by all types of equipment and magic items.' }) export abstract class IEquipment { @Field(() => String, { description: 'The unique identifier for this equipment.' }) index!: string @Field(() => String, { description: 'The name of the equipment.' }) name!: string @Field(() => Cost, { description: 'Cost of the equipment in coinage.' }) cost!: Cost @Field(() => Float, { nullable: true, description: 'Weight of the equipment in pounds.' }) weight?: number @Field(() => [String], { nullable: true, description: 'Description of the equipment.' }) description?: string[] } ================================================ FILE: src/graphql/2024/common/resolver.ts ================================================ import { FieldResolver, Resolver, Root } from 'type-graphql' import { resolveSingleReference } from '@/graphql/utils/resolvers' import AbilityScoreModel, { AbilityScore2024 } from '@/models/2024/abilityScore' import { DifficultyClass } from '@/models/common/difficultyClass' @Resolver(() => DifficultyClass) export class DifficultyClassResolver { @FieldResolver(() => AbilityScore2024, { nullable: true }) async dc_type(@Root() dc: DifficultyClass): Promise { return resolveSingleReference(dc.dc_type, AbilityScoreModel) } } ================================================ FILE: src/graphql/2024/common/unions.ts ================================================ import { createUnionType } from 'type-graphql' import { APIReference } from '@/models/common/apiReference' import { Ammunition, AdventuringGear, Armor, Pack, Weapon, Tool } from './equipmentTypes' function resolveEquipmentType( value: any ): | typeof Weapon | typeof AdventuringGear | typeof Ammunition | typeof Armor | typeof Pack | typeof Tool | null { if ( value.equipment_categories?.some((category: APIReference) => category.index === 'weapons') === true ) { return Weapon } if ( value.equipment_categories?.some( (category: APIReference) => category.index === 'ammunition' ) === true ) { return Ammunition } if ( value.equipment_categories?.some((category: APIReference) => category.index === 'armor') === true ) { return Armor } if ( value.equipment_categories?.some( (category: APIReference) => category.index === 'equipment-packs' ) === true ) { return Pack } if ( value.equipment_categories?.some( (category: APIReference) => category.index === 'adventuring-gear' ) === true ) { return AdventuringGear } if ( value.equipment_categories?.some((category: APIReference) => category.index === 'tools') === true ) { return Tool } return null } export const AnyEquipment = createUnionType({ name: 'AnyEquipment', types: () => { return [Weapon, AdventuringGear, Ammunition, Armor, Pack, Tool] as const }, resolveType: (value) => { const equipmentType = resolveEquipmentType(value) if (equipmentType) { return equipmentType } console.warn('Could not resolve type for AnyEquipment:', value) return AdventuringGear } }) ================================================ FILE: src/graphql/2024/resolvers/abilityScore/args.ts ================================================ import { ArgsType, Field, InputType, registerEnumType } from 'type-graphql' import { z } from 'zod' import { BaseFilterArgs, BaseFilterArgsSchema, BaseIndexArgsSchema, BaseOrderInterface } from '@/graphql/common/args' import { OrderByDirection } from '@/graphql/common/enums' export enum AbilityScoreOrderField { NAME = 'name', FULL_NAME = 'full_name' } export const ABILITY_SCORE_SORT_FIELD_MAP: Record = { [AbilityScoreOrderField.NAME]: 'name', [AbilityScoreOrderField.FULL_NAME]: 'full_name' } registerEnumType(AbilityScoreOrderField, { name: 'AbilityScoreOrderField', description: 'Fields to sort Ability Scores by' }) @InputType() export class AbilityScoreOrder implements BaseOrderInterface { @Field(() => AbilityScoreOrderField) by!: AbilityScoreOrderField @Field(() => OrderByDirection) direction!: OrderByDirection @Field(() => AbilityScoreOrder, { nullable: true }) then_by?: AbilityScoreOrder } export const AbilityScoreOrderSchema: z.ZodType = z.lazy(() => z.object({ by: z.nativeEnum(AbilityScoreOrderField), direction: z.nativeEnum(OrderByDirection), then_by: AbilityScoreOrderSchema.optional() }) ) export const AbilityScoreArgsSchema = z.object({ ...BaseFilterArgsSchema.shape, full_name: z.string().optional(), order: AbilityScoreOrderSchema.optional() }) export const AbilityScoreIndexArgsSchema = BaseIndexArgsSchema @ArgsType() export class AbilityScoreArgs extends BaseFilterArgs { @Field(() => String, { nullable: true, description: 'Filter by ability score full name (case-insensitive, partial match)' }) full_name?: string @Field(() => AbilityScoreOrder, { nullable: true, description: 'Specify sorting order for ability scores.' }) order?: AbilityScoreOrder } ================================================ FILE: src/graphql/2024/resolvers/abilityScore/resolver.ts ================================================ import { Arg, Args, FieldResolver, Query, Resolver, Root } from 'type-graphql' import { buildSortPipeline } from '@/graphql/common/args' import { resolveMultipleReferences } from '@/graphql/utils/resolvers' import AbilityScoreModel, { AbilityScore2024 } from '@/models/2024/abilityScore' import SkillModel, { Skill2024 } from '@/models/2024/skill' import { escapeRegExp } from '@/util' import { ABILITY_SCORE_SORT_FIELD_MAP, AbilityScoreArgs, AbilityScoreArgsSchema, AbilityScoreIndexArgsSchema, AbilityScoreOrderField } from './args' @Resolver(AbilityScore2024) export class AbilityScoreResolver { @Query(() => [AbilityScore2024], { description: 'Gets all ability scores, optionally filtered by name and sorted.' }) async abilityScores( @Args(() => AbilityScoreArgs) args: AbilityScoreArgs ): Promise { const validatedArgs = AbilityScoreArgsSchema.parse(args) const query = AbilityScoreModel.find() const filters: Record[] = [] if (validatedArgs.name != null && validatedArgs.name !== '') { filters.push({ name: { $regex: new RegExp(escapeRegExp(validatedArgs.name), 'i') } }) } if (validatedArgs.full_name != null && validatedArgs.full_name !== '') { filters.push({ full_name: { $regex: new RegExp(escapeRegExp(validatedArgs.full_name), 'i') } }) } if (filters.length > 0) { query.where({ $and: filters }) } const sortQuery = buildSortPipeline({ order: validatedArgs.order, sortFieldMap: ABILITY_SCORE_SORT_FIELD_MAP, defaultSortField: AbilityScoreOrderField.NAME }) if (Object.keys(sortQuery).length > 0) { query.sort(sortQuery) } if (validatedArgs.skip) { query.skip(validatedArgs.skip) } if (validatedArgs.limit) { query.limit(validatedArgs.limit) } return query.lean() } @Query(() => AbilityScore2024, { nullable: true, description: 'Gets a single ability score by index.' }) async abilityScore( @Arg('index', () => String) indexInput: string ): Promise { const { index } = AbilityScoreIndexArgsSchema.parse({ index: indexInput }) return AbilityScoreModel.findOne({ index }).lean() } @FieldResolver(() => [Skill2024]) async skills(@Root() abilityScore: AbilityScore2024): Promise { return resolveMultipleReferences(abilityScore.skills, SkillModel) } } ================================================ FILE: src/graphql/2024/resolvers/alignment/args.ts ================================================ import { ArgsType, Field, InputType, registerEnumType } from 'type-graphql' import { z } from 'zod' import { BaseFilterArgs, BaseFilterArgsSchema, BaseIndexArgsSchema, BaseOrderInterface } from '@/graphql/common/args' import { OrderByDirection } from '@/graphql/common/enums' export enum AlignmentOrderField { NAME = 'name' } export const ALIGNMENT_SORT_FIELD_MAP: Record = { [AlignmentOrderField.NAME]: 'name' } registerEnumType(AlignmentOrderField, { name: 'AlignmentOrderField', description: 'Fields to sort Alignments by' }) @InputType() export class AlignmentOrder implements BaseOrderInterface { @Field(() => AlignmentOrderField) by!: AlignmentOrderField @Field(() => OrderByDirection) direction!: OrderByDirection @Field(() => AlignmentOrder, { nullable: true }) then_by?: AlignmentOrder } export const AlignmentOrderSchema: z.ZodType = z.lazy(() => z.object({ by: z.nativeEnum(AlignmentOrderField), direction: z.nativeEnum(OrderByDirection), then_by: AlignmentOrderSchema.optional() }) ) export const AlignmentArgsSchema = z.object({ ...BaseFilterArgsSchema.shape, order: AlignmentOrderSchema.optional() }) export const AlignmentIndexArgsSchema = BaseIndexArgsSchema @ArgsType() export class AlignmentArgs extends BaseFilterArgs { @Field(() => AlignmentOrder, { nullable: true, description: 'Specify sorting order for alignments.' }) order?: AlignmentOrder } ================================================ FILE: src/graphql/2024/resolvers/alignment/resolver.ts ================================================ import { Arg, Args, Query, Resolver } from 'type-graphql' import { buildSortPipeline } from '@/graphql/common/args' import AlignmentModel, { Alignment2024 } from '@/models/2024/alignment' import { escapeRegExp } from '@/util' import { ALIGNMENT_SORT_FIELD_MAP, AlignmentArgs, AlignmentArgsSchema, AlignmentIndexArgsSchema, AlignmentOrderField } from './args' @Resolver(Alignment2024) export class AlignmentResolver { @Query(() => [Alignment2024], { description: 'Gets all alignments, optionally filtered by name and sorted.' }) async alignments(@Args(() => AlignmentArgs) args: AlignmentArgs): Promise { const validatedArgs = AlignmentArgsSchema.parse(args) const query = AlignmentModel.find() if (validatedArgs.name != null && validatedArgs.name !== '') { query.where({ name: { $regex: new RegExp(escapeRegExp(validatedArgs.name), 'i') } }) } const sortQuery = buildSortPipeline({ order: validatedArgs.order, sortFieldMap: ALIGNMENT_SORT_FIELD_MAP, defaultSortField: AlignmentOrderField.NAME }) if (Object.keys(sortQuery).length > 0) { query.sort(sortQuery) } if (validatedArgs.skip) { query.skip(validatedArgs.skip) } if (validatedArgs.limit) { query.limit(validatedArgs.limit) } return query.lean() } @Query(() => Alignment2024, { nullable: true, description: 'Gets a single alignment by index.' }) async alignment(@Arg('index', () => String) indexInput: string): Promise { const { index } = AlignmentIndexArgsSchema.parse({ index: indexInput }) return AlignmentModel.findOne({ index }).lean() } } ================================================ FILE: src/graphql/2024/resolvers/background/args.ts ================================================ import { ArgsType, Field, InputType, registerEnumType } from 'type-graphql' import { z } from 'zod' import { BaseFilterArgs, BaseFilterArgsSchema, BaseIndexArgsSchema, BaseOrderInterface } from '@/graphql/common/args' import { OrderByDirection } from '@/graphql/common/enums' export enum BackgroundOrderField { NAME = 'name' } export const BACKGROUND_SORT_FIELD_MAP: Record = { [BackgroundOrderField.NAME]: 'name' } registerEnumType(BackgroundOrderField, { name: 'BackgroundOrderField', description: 'Fields to sort Backgrounds by' }) @InputType() export class BackgroundOrder implements BaseOrderInterface { @Field(() => BackgroundOrderField) by!: BackgroundOrderField @Field(() => OrderByDirection) direction!: OrderByDirection @Field(() => BackgroundOrder, { nullable: true }) then_by?: BackgroundOrder } export const BackgroundOrderSchema: z.ZodType = z.lazy(() => z.object({ by: z.nativeEnum(BackgroundOrderField), direction: z.nativeEnum(OrderByDirection), then_by: BackgroundOrderSchema.optional() }) ) export const BackgroundArgsSchema = z.object({ ...BaseFilterArgsSchema.shape, order: BackgroundOrderSchema.optional() }) export const BackgroundIndexArgsSchema = BaseIndexArgsSchema @ArgsType() export class BackgroundArgs extends BaseFilterArgs { @Field(() => BackgroundOrder, { nullable: true, description: 'Specify sorting order for backgrounds.' }) order?: BackgroundOrder } ================================================ FILE: src/graphql/2024/resolvers/background/resolver.ts ================================================ import { Arg, Args, FieldResolver, Query, Resolver, Root } from 'type-graphql' import { Proficiency2024Choice } from '@/graphql/2024/common/choiceTypes' import { BackgroundEquipmentChoice2024 } from '@/graphql/2024/types/backgroundEquipment' import { resolveBackgroundEquipmentChoices } from '@/graphql/2024/utils/backgroundEquipmentResolver' import { resolveProficiency2024ChoiceArray } from '@/graphql/2024/utils/choiceResolvers' import { buildSortPipeline } from '@/graphql/common/args' import { resolveMultipleReferences, resolveSingleReference } from '@/graphql/utils/resolvers' import AbilityScoreModel, { AbilityScore2024 } from '@/models/2024/abilityScore' import BackgroundModel, { Background2024, BackgroundFeatReference } from '@/models/2024/background' import FeatModel, { Feat2024 } from '@/models/2024/feat' import ProficiencyModel, { Proficiency2024 } from '@/models/2024/proficiency' import { escapeRegExp } from '@/util' import { BACKGROUND_SORT_FIELD_MAP, BackgroundArgs, BackgroundArgsSchema, BackgroundIndexArgsSchema, BackgroundOrderField } from './args' @Resolver(Background2024) export class BackgroundResolver { @Query(() => [Background2024], { description: 'Gets all backgrounds, optionally filtered by name.' }) async backgrounds(@Args(() => BackgroundArgs) args: BackgroundArgs): Promise { const validatedArgs = BackgroundArgsSchema.parse(args) const query = BackgroundModel.find() if (validatedArgs.name != null && validatedArgs.name !== '') { query.where({ name: { $regex: new RegExp(escapeRegExp(validatedArgs.name), 'i') } }) } const sortQuery = buildSortPipeline({ order: validatedArgs.order, sortFieldMap: BACKGROUND_SORT_FIELD_MAP, defaultSortField: BackgroundOrderField.NAME }) if (Object.keys(sortQuery).length > 0) { query.sort(sortQuery) } if (validatedArgs.skip !== undefined) { query.skip(validatedArgs.skip) } if (validatedArgs.limit !== undefined) { query.limit(validatedArgs.limit) } return query.lean() } @Query(() => Background2024, { nullable: true, description: 'Gets a single background by index.' }) async background( @Arg('index', () => String) indexInput: string ): Promise { const { index } = BackgroundIndexArgsSchema.parse({ index: indexInput }) return BackgroundModel.findOne({ index }).lean() } @FieldResolver(() => [AbilityScore2024]) async ability_scores(@Root() background: Background2024): Promise { return resolveMultipleReferences(background.ability_scores, AbilityScoreModel) } @FieldResolver(() => Feat2024) async feat(@Root() background: Background2024): Promise { return resolveSingleReference(background.feat as BackgroundFeatReference, FeatModel) } @FieldResolver(() => [Proficiency2024]) async proficiencies(@Root() background: Background2024): Promise { return resolveMultipleReferences(background.proficiencies, ProficiencyModel) } @FieldResolver(() => [Proficiency2024Choice], { nullable: true }) async proficiency_choices( @Root() background: Background2024 ): Promise { return resolveProficiency2024ChoiceArray(background.proficiency_choices) } @FieldResolver(() => [BackgroundEquipmentChoice2024], { nullable: true }) async equipment_options( @Root() background: Background2024 ): Promise { return resolveBackgroundEquipmentChoices(background.equipment_options) } } ================================================ FILE: src/graphql/2024/resolvers/condition/args.ts ================================================ import { ArgsType, Field, InputType, registerEnumType } from 'type-graphql' import { z } from 'zod' import { BaseFilterArgs, BaseFilterArgsSchema, BaseIndexArgsSchema, BaseOrderInterface } from '@/graphql/common/args' import { OrderByDirection } from '@/graphql/common/enums' export enum ConditionOrderField { NAME = 'name' } export const CONDITION_SORT_FIELD_MAP: Record = { [ConditionOrderField.NAME]: 'name' } registerEnumType(ConditionOrderField, { name: 'ConditionOrderField', description: 'Fields to sort Conditions by' }) @InputType() export class ConditionOrder implements BaseOrderInterface { @Field(() => ConditionOrderField) by!: ConditionOrderField @Field(() => OrderByDirection) direction!: OrderByDirection @Field(() => ConditionOrder, { nullable: true }) then_by?: ConditionOrder } export const ConditionOrderSchema: z.ZodType = z.lazy(() => z.object({ by: z.nativeEnum(ConditionOrderField), direction: z.nativeEnum(OrderByDirection), then_by: ConditionOrderSchema.optional() }) ) export const ConditionArgsSchema = z.object({ ...BaseFilterArgsSchema.shape, order: ConditionOrderSchema.optional() }) export const ConditionIndexArgsSchema = BaseIndexArgsSchema @ArgsType() export class ConditionArgs extends BaseFilterArgs { @Field(() => ConditionOrder, { nullable: true, description: 'Specify sorting order for conditions.' }) order?: ConditionOrder } ================================================ FILE: src/graphql/2024/resolvers/condition/resolver.ts ================================================ import { Arg, Args, Query, Resolver } from 'type-graphql' import { buildSortPipeline } from '@/graphql/common/args' import ConditionModel, { Condition2024 } from '@/models/2024/condition' import { escapeRegExp } from '@/util' import { CONDITION_SORT_FIELD_MAP, ConditionArgs, ConditionArgsSchema, ConditionIndexArgsSchema, ConditionOrderField } from './args' @Resolver(Condition2024) export class ConditionResolver { @Query(() => [Condition2024], { description: 'Gets all conditions, optionally filtered by name and sorted by name.' }) async conditions(@Args(() => ConditionArgs) args: ConditionArgs): Promise { const validatedArgs = ConditionArgsSchema.parse(args) const query = ConditionModel.find() if (validatedArgs.name != null && validatedArgs.name !== '') { query.where({ name: { $regex: new RegExp(escapeRegExp(validatedArgs.name), 'i') } }) } const sortQuery = buildSortPipeline({ order: validatedArgs.order, sortFieldMap: CONDITION_SORT_FIELD_MAP, defaultSortField: ConditionOrderField.NAME }) if (Object.keys(sortQuery).length > 0) { query.sort(sortQuery) } if (validatedArgs.skip) { query.skip(validatedArgs.skip) } if (validatedArgs.limit) { query.limit(validatedArgs.limit) } return query.lean() } @Query(() => Condition2024, { nullable: true, description: 'Gets a single condition by index.' }) async condition(@Arg('index', () => String) indexInput: string): Promise { const { index } = ConditionIndexArgsSchema.parse({ index: indexInput }) return ConditionModel.findOne({ index }).lean() } } ================================================ FILE: src/graphql/2024/resolvers/damageType/args.ts ================================================ import { ArgsType, Field, InputType, registerEnumType } from 'type-graphql' import { z } from 'zod' import { BaseFilterArgs, BaseFilterArgsSchema, BaseIndexArgsSchema, BaseOrderInterface } from '@/graphql/common/args' import { OrderByDirection } from '@/graphql/common/enums' export enum DamageTypeOrderField { NAME = 'name' } export const DAMAGE_TYPE_SORT_FIELD_MAP: Record = { [DamageTypeOrderField.NAME]: 'name' } registerEnumType(DamageTypeOrderField, { name: 'DamageTypeOrderField', description: 'Fields to sort Damage Types by' }) @InputType() export class DamageTypeOrder implements BaseOrderInterface { @Field(() => DamageTypeOrderField) by!: DamageTypeOrderField @Field(() => OrderByDirection) direction!: OrderByDirection @Field(() => DamageTypeOrder, { nullable: true }) then_by?: DamageTypeOrder } export const DamageTypeOrderSchema: z.ZodType = z.lazy(() => z.object({ by: z.nativeEnum(DamageTypeOrderField), direction: z.nativeEnum(OrderByDirection), then_by: DamageTypeOrderSchema.optional() }) ) export const DamageTypeArgsSchema = z.object({ ...BaseFilterArgsSchema.shape, order: DamageTypeOrderSchema.optional() }) export const DamageTypeIndexArgsSchema = BaseIndexArgsSchema @ArgsType() export class DamageTypeArgs extends BaseFilterArgs { @Field(() => DamageTypeOrder, { nullable: true, description: 'Specify sorting order for damage types.' }) order?: DamageTypeOrder } ================================================ FILE: src/graphql/2024/resolvers/damageType/resolver.ts ================================================ import { Arg, Args, Query, Resolver } from 'type-graphql' import { buildSortPipeline } from '@/graphql/common/args' import DamageTypeModel, { DamageType2024 } from '@/models/2024/damageType' import { escapeRegExp } from '@/util' import { DAMAGE_TYPE_SORT_FIELD_MAP, DamageTypeArgs, DamageTypeArgsSchema, DamageTypeIndexArgsSchema, DamageTypeOrderField } from './args' @Resolver(DamageType2024) export class DamageTypeResolver { @Query(() => [DamageType2024], { description: 'Gets all damage types, optionally filtered by name and sorted by name.' }) async damageTypes(@Args(() => DamageTypeArgs) args: DamageTypeArgs): Promise { const validatedArgs = DamageTypeArgsSchema.parse(args) const query = DamageTypeModel.find() if (validatedArgs.name != null && validatedArgs.name !== '') { query.where({ name: { $regex: new RegExp(escapeRegExp(validatedArgs.name), 'i') } }) } const sortQuery = buildSortPipeline({ order: validatedArgs.order, sortFieldMap: DAMAGE_TYPE_SORT_FIELD_MAP, defaultSortField: DamageTypeOrderField.NAME }) if (Object.keys(sortQuery).length > 0) { query.sort(sortQuery) } if (validatedArgs.skip) { query.skip(validatedArgs.skip) } if (validatedArgs.limit) { query.limit(validatedArgs.limit) } return query.lean() } @Query(() => DamageType2024, { nullable: true, description: 'Gets a single damage type by index.' }) async damageType(@Arg('index', () => String) indexInput: string): Promise { const { index } = DamageTypeIndexArgsSchema.parse({ index: indexInput }) return DamageTypeModel.findOne({ index }).lean() } } ================================================ FILE: src/graphql/2024/resolvers/equipment/args.ts ================================================ import { ArgsType, Field, InputType, registerEnumType } from 'type-graphql' import { z } from 'zod' import { BaseFilterArgs, BaseFilterArgsSchema, BaseIndexArgsSchema, BaseOrderInterface } from '@/graphql/common/args' import { OrderByDirection } from '@/graphql/common/enums' export enum EquipmentOrderField { NAME = 'name', WEIGHT = 'weight', COST_QUANTITY = 'cost_quantity' } export const EQUIPMENT_SORT_FIELD_MAP: Record = { [EquipmentOrderField.NAME]: 'name', [EquipmentOrderField.WEIGHT]: 'weight', [EquipmentOrderField.COST_QUANTITY]: 'cost.quantity' } registerEnumType(EquipmentOrderField, { name: 'EquipmentOrderField', description: 'Fields to sort Equipment by' }) @InputType() export class EquipmentOrder implements BaseOrderInterface { @Field(() => EquipmentOrderField) by!: EquipmentOrderField @Field(() => OrderByDirection) direction!: OrderByDirection @Field(() => EquipmentOrder, { nullable: true }) then_by?: EquipmentOrder } export const EquipmentOrderSchema: z.ZodType = z.lazy(() => z.object({ by: z.nativeEnum(EquipmentOrderField), direction: z.nativeEnum(OrderByDirection), then_by: EquipmentOrderSchema.optional() // Simplified }) ) export const EquipmentArgsSchema = BaseFilterArgsSchema.extend({ equipment_category: z.array(z.string()).optional(), order: EquipmentOrderSchema.optional() }) export const EquipmentIndexArgsSchema = BaseIndexArgsSchema @ArgsType() export class EquipmentArgs extends BaseFilterArgs { @Field(() => [String], { nullable: true, description: 'Filter by one or more equipment category indices (e.g., ["weapon", "armor"])' }) equipment_category?: string[] @Field(() => EquipmentOrder, { nullable: true, description: 'Specify sorting order for equipment.' }) order?: EquipmentOrder } ================================================ FILE: src/graphql/2024/resolvers/equipment/resolver.ts ================================================ import { Arg, Args, FieldResolver, Query, Resolver, Root } from 'type-graphql' import { Tool } from '@/graphql/2024/common/equipmentTypes' import { AnyEquipment } from '@/graphql/2024/common/unions' import { buildSortPipeline } from '@/graphql/common/args' import { resolveMultipleReferences, resolveSingleReference } from '@/graphql/utils/resolvers' import AbilityScoreModel, { AbilityScore2024 } from '@/models/2024/abilityScore' import EquipmentModel, { Content, Equipment2024 } from '@/models/2024/equipment' import WeaponPropertyModel, { WeaponProperty2024 } from '@/models/2024/weaponProperty' import { APIReference } from '@/models/common/apiReference' import { escapeRegExp } from '@/util' import { EQUIPMENT_SORT_FIELD_MAP, EquipmentArgs, EquipmentArgsSchema, EquipmentIndexArgsSchema, EquipmentOrderField } from './args' @Resolver(Equipment2024) export class EquipmentResolver { @Query(() => [AnyEquipment], { description: 'Gets all equipment, optionally filtered and sorted.' }) async equipments( @Args(() => EquipmentArgs) args: EquipmentArgs ): Promise> { const validatedArgs = EquipmentArgsSchema.parse(args) const query = EquipmentModel.find() const filters: any[] = [] if (validatedArgs.name != null && validatedArgs.name !== '') { filters.push({ name: { $regex: new RegExp(escapeRegExp(validatedArgs.name), 'i') } }) } if (validatedArgs.equipment_category && validatedArgs.equipment_category.length > 0) { filters.push({ 'equipment_category.index': { $in: validatedArgs.equipment_category } }) } if (filters.length > 0) { query.where({ $and: filters }) } const sortQuery = buildSortPipeline({ order: validatedArgs.order, sortFieldMap: EQUIPMENT_SORT_FIELD_MAP, defaultSortField: EquipmentOrderField.NAME }) if (Object.keys(sortQuery).length > 0) { query.sort(sortQuery) } if (validatedArgs.skip) { query.skip(validatedArgs.skip) } if (validatedArgs.limit) { query.limit(validatedArgs.limit) } return await query.lean() } @Query(() => AnyEquipment, { nullable: true, description: 'Gets a single piece of equipment by its index.' }) async equipment( @Arg('index', () => String) indexInput: string ): Promise { const { index } = EquipmentIndexArgsSchema.parse({ index: indexInput }) return EquipmentModel.findOne({ index }).lean() } @FieldResolver(() => [WeaponProperty2024], { nullable: true }) async properties(@Root() equipment: Equipment2024): Promise { if (!equipment.properties) return null return resolveMultipleReferences(equipment.properties, WeaponPropertyModel) } } @Resolver(Content) export class ContentFieldResolver { @FieldResolver(() => AnyEquipment, { nullable: true, description: 'Resolves the APIReference to the actual Equipment.' }) async item(@Root() content: Content): Promise { const itemRef: APIReference = content.item if (!itemRef?.index) return null return resolveSingleReference(itemRef, EquipmentModel) } } @Resolver(() => Tool) export class ToolResolver { @FieldResolver(() => AbilityScore2024, { nullable: true }) async ability(@Root() tool: Tool): Promise { if (!tool.ability) return null return resolveSingleReference(tool.ability, AbilityScoreModel) } @FieldResolver(() => [AnyEquipment], { nullable: true }) async craft(@Root() tool: Tool): Promise | null> { if (!tool.craft) return null return resolveMultipleReferences(tool.craft, EquipmentModel) } } ================================================ FILE: src/graphql/2024/resolvers/equipmentCategory/args.ts ================================================ import { ArgsType, Field, InputType, registerEnumType } from 'type-graphql' import { z } from 'zod' import { BaseFilterArgs, BaseFilterArgsSchema, BaseIndexArgsSchema, BaseOrderInterface } from '@/graphql/common/args' import { OrderByDirection } from '@/graphql/common/enums' export enum EquipmentCategoryOrderField { NAME = 'name' } export const EQUIPMENT_CATEGORY_SORT_FIELD_MAP: Record = { [EquipmentCategoryOrderField.NAME]: 'name' } registerEnumType(EquipmentCategoryOrderField, { name: 'EquipmentCategoryOrderField', description: 'Fields to sort Equipment Categories by' }) @InputType() export class EquipmentCategoryOrder implements BaseOrderInterface { @Field(() => EquipmentCategoryOrderField) by!: EquipmentCategoryOrderField @Field(() => OrderByDirection) direction!: OrderByDirection @Field(() => EquipmentCategoryOrder, { nullable: true }) then_by?: EquipmentCategoryOrder } export const EquipmentCategoryOrderSchema: z.ZodType = z.lazy(() => z.object({ by: z.nativeEnum(EquipmentCategoryOrderField), direction: z.nativeEnum(OrderByDirection), then_by: EquipmentCategoryOrderSchema.optional() }) ) export const EquipmentCategoryArgsSchema = BaseFilterArgsSchema.extend({ order: EquipmentCategoryOrderSchema.optional() }) export const EquipmentCategoryIndexArgsSchema = BaseIndexArgsSchema @ArgsType() export class EquipmentCategoryArgs extends BaseFilterArgs { @Field(() => EquipmentCategoryOrder, { nullable: true, description: 'Specify sorting order for equipment categories.' }) order?: EquipmentCategoryOrder } ================================================ FILE: src/graphql/2024/resolvers/equipmentCategory/resolver.ts ================================================ import { Arg, Args, FieldResolver, Query, Resolver, Root } from 'type-graphql' import { AnyEquipment } from '@/graphql/2024/common/unions' import { buildSortPipeline } from '@/graphql/common/args' import EquipmentModel, { Equipment2024 } from '@/models/2024/equipment' import EquipmentCategoryModel, { EquipmentCategory2024 } from '@/models/2024/equipmentCategory' import { escapeRegExp } from '@/util' import { EQUIPMENT_CATEGORY_SORT_FIELD_MAP, EquipmentCategoryArgs, EquipmentCategoryArgsSchema, EquipmentCategoryIndexArgsSchema, EquipmentCategoryOrderField } from './args' @Resolver(EquipmentCategory2024) export class EquipmentCategoryResolver { @Query(() => [EquipmentCategory2024], { description: 'Gets all equipment categories, optionally filtered by name and sorted by name.' }) async equipmentCategories( @Args(() => EquipmentCategoryArgs) args: EquipmentCategoryArgs ): Promise { const validatedArgs = EquipmentCategoryArgsSchema.parse(args) const query = EquipmentCategoryModel.find() if (validatedArgs.name != null && validatedArgs.name !== '') { query.where({ name: { $regex: new RegExp(escapeRegExp(validatedArgs.name), 'i') } }) } const sortQuery = buildSortPipeline({ order: validatedArgs.order, sortFieldMap: EQUIPMENT_CATEGORY_SORT_FIELD_MAP, defaultSortField: EquipmentCategoryOrderField.NAME }) if (Object.keys(sortQuery).length > 0) { query.sort(sortQuery) } if (validatedArgs.skip) { query.skip(validatedArgs.skip) } if (validatedArgs.limit) { query.limit(validatedArgs.limit) } return query.lean() } @Query(() => EquipmentCategory2024, { nullable: true, description: 'Gets a single equipment category by index.' }) async equipmentCategory( @Arg('index', () => String) indexInput: string ): Promise { const { index } = EquipmentCategoryIndexArgsSchema.parse({ index: indexInput }) return EquipmentCategoryModel.findOne({ index }).lean() } // TODO: Remove when Magic Items are added @FieldResolver(() => [AnyEquipment]) async equipment(@Root() equipmentCategory: EquipmentCategory2024): Promise { if (equipmentCategory.equipment.length === 0) { return [] } const equipmentIndices = equipmentCategory.equipment.map((ref) => ref.index) const equipments = await EquipmentModel.find({ index: { $in: equipmentIndices } }).lean() return equipments } // TODO: Add Magic Items // @FieldResolver(() => [EquipmentOrMagicItem]) // async equipment( // @Root() equipmentCategory: EquipmentCategory // ): Promise<(Equipment | MagicItem)[]> { // if (equipmentCategory.equipment.length === 0) { // return [] // } // const equipmentIndices = equipmentCategory.equipment.map((ref) => ref.index) // // Fetch both Equipment and MagicItems matching the indices // const [equipments, magicItems] = await Promise.all([ // EquipmentModel.find({ index: { $in: equipmentIndices } }).lean(), // MagicItemModel.find({ index: { $in: equipmentIndices } }).lean() // ]) // return [...equipments, ...magicItems] // } } ================================================ FILE: src/graphql/2024/resolvers/feat/args.ts ================================================ import { ArgsType, Field, InputType, registerEnumType } from 'type-graphql' import { z } from 'zod' import { BaseFilterArgs, BaseFilterArgsSchema, BaseIndexArgsSchema, BaseOrderInterface } from '@/graphql/common/args' import { OrderByDirection } from '@/graphql/common/enums' export enum FeatOrderField { NAME = 'name', TYPE = 'type' } export const FEAT_SORT_FIELD_MAP: Record = { [FeatOrderField.NAME]: 'name', [FeatOrderField.TYPE]: 'type' } registerEnumType(FeatOrderField, { name: 'FeatOrderField', description: 'Fields to sort Feats by' }) @InputType() export class FeatOrder implements BaseOrderInterface { @Field(() => FeatOrderField) by!: FeatOrderField @Field(() => OrderByDirection) direction!: OrderByDirection @Field(() => FeatOrder, { nullable: true }) then_by?: FeatOrder } export const FeatOrderSchema: z.ZodType = z.lazy(() => z.object({ by: z.nativeEnum(FeatOrderField), direction: z.nativeEnum(OrderByDirection), then_by: FeatOrderSchema.optional() }) ) export const FeatArgsSchema = z.object({ ...BaseFilterArgsSchema.shape, type: z.array(z.string()).optional(), order: FeatOrderSchema.optional() }) export const FeatIndexArgsSchema = BaseIndexArgsSchema @ArgsType() export class FeatArgs extends BaseFilterArgs { @Field(() => [String], { nullable: true, description: 'Filter by feat type (e.g., ["origin", "general"])' }) type?: string[] @Field(() => FeatOrder, { nullable: true, description: 'Specify sorting order for feats.' }) order?: FeatOrder } ================================================ FILE: src/graphql/2024/resolvers/feat/resolver.ts ================================================ import { Arg, Args, FieldResolver, Query, Resolver, Root } from 'type-graphql' import { ScorePrerequisiteChoice2024 } from '@/graphql/2024/common/choiceTypes' import { resolveScorePrerequisiteChoice } from '@/graphql/2024/utils/choiceResolvers' import { buildSortPipeline } from '@/graphql/common/args' import FeatModel, { Feat2024 } from '@/models/2024/feat' import { escapeRegExp } from '@/util' import { FEAT_SORT_FIELD_MAP, FeatArgs, FeatArgsSchema, FeatIndexArgsSchema, FeatOrderField } from './args' @Resolver(Feat2024) export class FeatResolver { @Query(() => [Feat2024], { description: 'Gets all feats, optionally filtered by name and type.' }) async feats(@Args(() => FeatArgs) args: FeatArgs): Promise { const validatedArgs = FeatArgsSchema.parse(args) const query = FeatModel.find() const filters: any[] = [] if (validatedArgs.name != null && validatedArgs.name !== '') { filters.push({ name: { $regex: new RegExp(escapeRegExp(validatedArgs.name), 'i') } }) } if (validatedArgs.type && validatedArgs.type.length > 0) { filters.push({ type: { $in: validatedArgs.type } }) } if (filters.length > 0) { query.where({ $and: filters }) } const sortQuery = buildSortPipeline({ order: validatedArgs.order, sortFieldMap: FEAT_SORT_FIELD_MAP, defaultSortField: FeatOrderField.NAME }) if (Object.keys(sortQuery).length > 0) { query.sort(sortQuery) } if (validatedArgs.skip !== undefined) { query.skip(validatedArgs.skip) } if (validatedArgs.limit !== undefined) { query.limit(validatedArgs.limit) } return query.lean() } @Query(() => Feat2024, { nullable: true, description: 'Gets a single feat by index.' }) async feat(@Arg('index', () => String) indexInput: string): Promise { const { index } = FeatIndexArgsSchema.parse({ index: indexInput }) return FeatModel.findOne({ index }).lean() } @FieldResolver(() => ScorePrerequisiteChoice2024, { nullable: true }) async prerequisite_options(@Root() feat: Feat2024): Promise { return resolveScorePrerequisiteChoice(feat.prerequisite_options) } } ================================================ FILE: src/graphql/2024/resolvers/index.ts ================================================ import { DifficultyClassResolver } from '../common/resolver' import { AbilityScoreResolver } from './abilityScore/resolver' import { AlignmentResolver } from './alignment/resolver' import { BackgroundResolver } from './background/resolver' import { ConditionResolver } from './condition/resolver' import { DamageTypeResolver } from './damageType/resolver' import { ContentFieldResolver, EquipmentResolver, ToolResolver } from './equipment/resolver' import { EquipmentCategoryResolver } from './equipmentCategory/resolver' import { FeatResolver } from './feat/resolver' import { LanguageResolver } from './language/resolver' import { MagicItemResolver } from './magicItem/resolver' import { MagicSchoolResolver } from './magicSchool/resolver' import { ProficiencyResolver } from './proficiency/resolver' import { SkillResolver } from './skill/resolver' import { SpeciesResolver } from './species/resolver' import { SubclassResolver } from './subclass/resolver' import { SubspeciesResolver } from './subspecies/resolver' import { TraitResolver } from './trait/resolver' import { WeaponMasteryPropertyResolver } from './weaponMasteryProperty/resolver' import { WeaponPropertyResolver } from './weaponProperty/resolver' const collectionResolvers = [ AbilityScoreResolver, AlignmentResolver, BackgroundResolver, ConditionResolver, DamageTypeResolver, EquipmentResolver, EquipmentCategoryResolver, FeatResolver, LanguageResolver, MagicItemResolver, MagicSchoolResolver, ProficiencyResolver, SkillResolver, SpeciesResolver, SubclassResolver, SubspeciesResolver, TraitResolver, WeaponMasteryPropertyResolver, WeaponPropertyResolver ] as const const fieldResolvers = [ // Equipment ContentFieldResolver, ToolResolver, DifficultyClassResolver ] as const export const resolvers = [...collectionResolvers, ...fieldResolvers] as const ================================================ FILE: src/graphql/2024/resolvers/language/args.ts ================================================ import { ArgsType, Field, InputType, registerEnumType } from 'type-graphql' import { z } from 'zod' import { BaseFilterArgs, BaseFilterArgsSchema, BaseIndexArgsSchema, BaseOrderInterface } from '@/graphql/common/args' import { OrderByDirection } from '@/graphql/common/enums' export enum LanguageOrderField { NAME = 'name', TYPE = 'type', SCRIPT = 'script' } export const LANGUAGE_SORT_FIELD_MAP: Record = { [LanguageOrderField.NAME]: 'name', [LanguageOrderField.TYPE]: 'type', [LanguageOrderField.SCRIPT]: 'script' } registerEnumType(LanguageOrderField, { name: 'LanguageOrderField', description: 'Fields to sort Languages by' }) @InputType() export class LanguageOrder implements BaseOrderInterface { @Field(() => LanguageOrderField) by!: LanguageOrderField @Field(() => OrderByDirection) direction!: OrderByDirection @Field(() => LanguageOrder, { nullable: true }) then_by?: LanguageOrder } export const LanguageOrderSchema: z.ZodType = z.lazy(() => z.object({ by: z.nativeEnum(LanguageOrderField), direction: z.nativeEnum(OrderByDirection), then_by: LanguageOrderSchema.optional() }) ) export const LanguageArgsSchema = z.object({ ...BaseFilterArgsSchema.shape, type: z.string().optional(), script: z.array(z.string()).optional(), order: LanguageOrderSchema.optional() }) export const LanguageIndexArgsSchema = BaseIndexArgsSchema @ArgsType() export class LanguageArgs extends BaseFilterArgs { @Field(() => String, { nullable: true, description: 'Filter by language type (e.g., Standard, Exotic) - case-insensitive exact match after normalization' }) type?: string @Field(() => [String], { nullable: true, description: 'Filter by one or more language scripts (e.g., ["Common", "Elvish"])' }) script?: string[] @Field(() => LanguageOrder, { nullable: true, description: 'Specify sorting order for languages.' }) order?: LanguageOrder } ================================================ FILE: src/graphql/2024/resolvers/language/resolver.ts ================================================ import { Arg, Args, Query, Resolver } from 'type-graphql' import { buildSortPipeline } from '@/graphql/common/args' import LanguageModel, { Language2024 } from '@/models/2024/language' import { escapeRegExp } from '@/util' import { LANGUAGE_SORT_FIELD_MAP, LanguageArgs, LanguageArgsSchema, LanguageIndexArgsSchema, LanguageOrderField } from './args' @Resolver(Language2024) export class LanguageResolver { @Query(() => Language2024, { nullable: true, description: 'Gets a single language by its index.' }) async language(@Arg('index', () => String) indexInput: string): Promise { const { index } = LanguageIndexArgsSchema.parse({ index: indexInput }) return LanguageModel.findOne({ index }).lean() } @Query(() => [Language2024], { description: 'Gets all languages, optionally filtered and sorted.' }) async languages(@Args(() => LanguageArgs) args: LanguageArgs): Promise { const validatedArgs = LanguageArgsSchema.parse(args) const query = LanguageModel.find() const filters: any[] = [] if (validatedArgs.name != null && validatedArgs.name !== '') { filters.push({ name: { $regex: new RegExp(escapeRegExp(validatedArgs.name), 'i') } }) } if (validatedArgs.type != null && validatedArgs.type !== '') { filters.push({ type: { $regex: new RegExp(escapeRegExp(validatedArgs.type), 'i') } }) } if (validatedArgs.script && validatedArgs.script.length > 0) { filters.push({ script: { $in: validatedArgs.script } }) } if (filters.length > 0) { query.where({ $and: filters }) } const sortQuery = buildSortPipeline({ order: validatedArgs.order, sortFieldMap: LANGUAGE_SORT_FIELD_MAP, defaultSortField: LanguageOrderField.NAME }) if (Object.keys(sortQuery).length > 0) { query.sort(sortQuery) } if (validatedArgs.skip !== undefined) { query.skip(validatedArgs.skip) } if (validatedArgs.limit !== undefined) { query.limit(validatedArgs.limit) } return query.lean() } } ================================================ FILE: src/graphql/2024/resolvers/magicItem/args.ts ================================================ import { ArgsType, Field, InputType, registerEnumType } from 'type-graphql' import { z } from 'zod' import { BaseFilterArgs, BaseFilterArgsSchema, BaseIndexArgsSchema, BaseOrderInterface } from '@/graphql/common/args' import { OrderByDirection } from '@/graphql/common/enums' export enum MagicItemOrderField { NAME = 'name' } export const MAGIC_ITEM_SORT_FIELD_MAP: Record = { [MagicItemOrderField.NAME]: 'name' } registerEnumType(MagicItemOrderField, { name: 'MagicItemOrderField', description: 'Fields to sort Magic Items by' }) @InputType() export class MagicItemOrder implements BaseOrderInterface { @Field(() => MagicItemOrderField) by!: MagicItemOrderField @Field(() => OrderByDirection) direction!: OrderByDirection @Field(() => MagicItemOrder, { nullable: true }) then_by?: MagicItemOrder } export const MagicItemOrderSchema: z.ZodType = z.lazy(() => z.object({ by: z.nativeEnum(MagicItemOrderField), direction: z.nativeEnum(OrderByDirection), then_by: MagicItemOrderSchema.optional() }) ) export const MagicItemArgsSchema = z.object({ ...BaseFilterArgsSchema.shape, equipment_category: z.array(z.string()).optional(), rarity: z.array(z.string()).optional(), order: MagicItemOrderSchema.optional() }) export const MagicItemIndexArgsSchema = BaseIndexArgsSchema @ArgsType() export class MagicItemArgs extends BaseFilterArgs { @Field(() => [String], { nullable: true, description: 'Filter by equipment category index (e.g., ["wondrous-items", "armor"])' }) equipment_category?: string[] @Field(() => [String], { nullable: true, description: 'Filter by rarity name (e.g., ["Common", "Rare"])' }) rarity?: string[] @Field(() => MagicItemOrder, { nullable: true, description: 'Specify sorting order for magic items.' }) order?: MagicItemOrder } ================================================ FILE: src/graphql/2024/resolvers/magicItem/resolver.ts ================================================ import { Arg, Args, FieldResolver, Query, Resolver, Root } from 'type-graphql' import { buildSortPipeline } from '@/graphql/common/args' import { resolveSingleReference, resolveMultipleReferences } from '@/graphql/utils/resolvers' import EquipmentCategoryModel, { EquipmentCategory2024 } from '@/models/2024/equipmentCategory' import MagicItemModel, { MagicItem2024 } from '@/models/2024/magicItem' import { escapeRegExp } from '@/util' import { MAGIC_ITEM_SORT_FIELD_MAP, MagicItemArgs, MagicItemArgsSchema, MagicItemIndexArgsSchema, MagicItemOrderField } from './args' @Resolver(MagicItem2024) export class MagicItemResolver { @Query(() => [MagicItem2024], { description: 'Gets all magic items, optionally filtered by name, equipment category, or rarity.' }) async magicItems(@Args(() => MagicItemArgs) args: MagicItemArgs): Promise { const validatedArgs = MagicItemArgsSchema.parse(args) const query = MagicItemModel.find() const filters: any[] = [] if (validatedArgs.name != null && validatedArgs.name !== '') { filters.push({ name: { $regex: new RegExp(escapeRegExp(validatedArgs.name), 'i') } }) } if (validatedArgs.equipment_category && validatedArgs.equipment_category.length > 0) { filters.push({ 'equipment_category.index': { $in: validatedArgs.equipment_category } }) } if (validatedArgs.rarity && validatedArgs.rarity.length > 0) { filters.push({ 'rarity.name': { $in: validatedArgs.rarity } }) } if (filters.length > 0) { query.where({ $and: filters }) } const sortQuery = buildSortPipeline({ order: validatedArgs.order, sortFieldMap: MAGIC_ITEM_SORT_FIELD_MAP, defaultSortField: MagicItemOrderField.NAME }) if (Object.keys(sortQuery).length > 0) { query.sort(sortQuery) } if (validatedArgs.skip) { query.skip(validatedArgs.skip) } if (validatedArgs.limit) { query.limit(validatedArgs.limit) } return query.lean() } @Query(() => MagicItem2024, { nullable: true, description: 'Gets a single magic item by index.' }) async magicItem(@Arg('index', () => String) indexInput: string): Promise { const { index } = MagicItemIndexArgsSchema.parse({ index: indexInput }) return MagicItemModel.findOne({ index }).lean() } @FieldResolver(() => EquipmentCategory2024) async equipment_category(@Root() magicItem: MagicItem2024): Promise { return resolveSingleReference(magicItem.equipment_category, EquipmentCategoryModel) } @FieldResolver(() => [MagicItem2024]) async variants(@Root() magicItem: MagicItem2024): Promise { return resolveMultipleReferences(magicItem.variants, MagicItemModel) } } ================================================ FILE: src/graphql/2024/resolvers/magicSchool/args.ts ================================================ import { ArgsType, Field, InputType, registerEnumType } from 'type-graphql' import { z } from 'zod' import { BaseFilterArgs, BaseFilterArgsSchema, BaseIndexArgsSchema, BaseOrderInterface } from '@/graphql/common/args' import { OrderByDirection } from '@/graphql/common/enums' export enum MagicSchoolOrderField { NAME = 'name' } export const MAGIC_SCHOOL_SORT_FIELD_MAP: Record = { [MagicSchoolOrderField.NAME]: 'name' } registerEnumType(MagicSchoolOrderField, { name: 'MagicSchoolOrderField', description: 'Fields to sort Magic Schools by' }) @InputType() export class MagicSchoolOrder implements BaseOrderInterface { @Field(() => MagicSchoolOrderField) by!: MagicSchoolOrderField @Field(() => OrderByDirection) direction!: OrderByDirection @Field(() => MagicSchoolOrder, { nullable: true }) then_by?: MagicSchoolOrder } export const MagicSchoolOrderSchema: z.ZodType = z.lazy(() => z.object({ by: z.nativeEnum(MagicSchoolOrderField), direction: z.nativeEnum(OrderByDirection), then_by: MagicSchoolOrderSchema.optional() }) ) export const MagicSchoolArgsSchema = z.object({ ...BaseFilterArgsSchema.shape, order: MagicSchoolOrderSchema.optional() }) export const MagicSchoolIndexArgsSchema = BaseIndexArgsSchema @ArgsType() export class MagicSchoolArgs extends BaseFilterArgs { @Field(() => MagicSchoolOrder, { nullable: true, description: 'Specify sorting order for magic schools.' }) order?: MagicSchoolOrder } ================================================ FILE: src/graphql/2024/resolvers/magicSchool/resolver.ts ================================================ import { Arg, Args, Query, Resolver } from 'type-graphql' import { buildSortPipeline } from '@/graphql/common/args' import MagicSchoolModel, { MagicSchool2024 } from '@/models/2024/magicSchool' import { escapeRegExp } from '@/util' import { MAGIC_SCHOOL_SORT_FIELD_MAP, MagicSchoolArgs, MagicSchoolArgsSchema, MagicSchoolIndexArgsSchema, MagicSchoolOrderField } from './args' @Resolver(MagicSchool2024) export class MagicSchoolResolver { @Query(() => [MagicSchool2024], { description: 'Gets all magic schools, optionally filtered by name and sorted by name.' }) async magicSchools( @Args(() => MagicSchoolArgs) args: MagicSchoolArgs ): Promise { const validatedArgs = MagicSchoolArgsSchema.parse(args) const query = MagicSchoolModel.find() if (validatedArgs.name != null && validatedArgs.name !== '') { query.where({ name: { $regex: new RegExp(escapeRegExp(validatedArgs.name), 'i') } }) } const sortQuery = buildSortPipeline({ order: validatedArgs.order, sortFieldMap: MAGIC_SCHOOL_SORT_FIELD_MAP, defaultSortField: MagicSchoolOrderField.NAME }) if (Object.keys(sortQuery).length > 0) { query.sort(sortQuery) } if (validatedArgs.skip) { query.skip(validatedArgs.skip) } if (validatedArgs.limit) { query.limit(validatedArgs.limit) } return query.lean() } @Query(() => MagicSchool2024, { nullable: true, description: 'Gets a single magic school by index.' }) async magicSchool( @Arg('index', () => String) indexInput: string ): Promise { const { index } = MagicSchoolIndexArgsSchema.parse({ index: indexInput }) return MagicSchoolModel.findOne({ index }).lean() } } ================================================ FILE: src/graphql/2024/resolvers/proficiency/args.ts ================================================ import { ArgsType, Field, InputType, registerEnumType } from 'type-graphql' import { z } from 'zod' import { BaseFilterArgs, BaseFilterArgsSchema, BaseIndexArgsSchema, BaseOrderInterface } from '@/graphql/common/args' import { OrderByDirection } from '@/graphql/common/enums' export enum ProficiencyOrderField { NAME = 'name', TYPE = 'type' } export const PROFICIENCY_SORT_FIELD_MAP: Record = { [ProficiencyOrderField.NAME]: 'name', [ProficiencyOrderField.TYPE]: 'type' } registerEnumType(ProficiencyOrderField, { name: 'ProficiencyOrderField', description: 'Fields to sort Proficiencies by' }) @InputType() export class ProficiencyOrder implements BaseOrderInterface { @Field(() => ProficiencyOrderField) by!: ProficiencyOrderField @Field(() => OrderByDirection) direction!: OrderByDirection @Field(() => ProficiencyOrder, { nullable: true }) then_by?: ProficiencyOrder } export const ProficiencyOrderSchema: z.ZodType = z.lazy(() => z.object({ by: z.nativeEnum(ProficiencyOrderField), direction: z.nativeEnum(OrderByDirection), then_by: ProficiencyOrderSchema.optional() }) ) export const ProficiencyArgsSchema = z.object({ ...BaseFilterArgsSchema.shape, type: z.array(z.string()).optional(), order: ProficiencyOrderSchema.optional() }) export const ProficiencyIndexArgsSchema = BaseIndexArgsSchema @ArgsType() export class ProficiencyArgs extends BaseFilterArgs { @Field(() => [String], { nullable: true, description: 'Filter by proficiency type (e.g., ["Skills", "Tools"])' }) type?: string[] @Field(() => ProficiencyOrder, { nullable: true, description: 'Specify sorting order for proficiencies.' }) order?: ProficiencyOrder } ================================================ FILE: src/graphql/2024/resolvers/proficiency/resolver.ts ================================================ import { Arg, Args, FieldResolver, Query, Resolver, Root } from 'type-graphql' import { buildSortPipeline } from '@/graphql/common/args' import { resolveMultipleReferences } from '@/graphql/utils/resolvers' import BackgroundModel, { Background2024 } from '@/models/2024/background' import ProficiencyModel, { Proficiency2024 } from '@/models/2024/proficiency' import { escapeRegExp } from '@/util' import { PROFICIENCY_SORT_FIELD_MAP, ProficiencyArgs, ProficiencyArgsSchema, ProficiencyIndexArgsSchema, ProficiencyOrderField } from './args' @Resolver(Proficiency2024) export class ProficiencyResolver { @Query(() => [Proficiency2024], { description: 'Gets all proficiencies, optionally filtered by name and type.' }) async proficiencies( @Args(() => ProficiencyArgs) args: ProficiencyArgs ): Promise { const validatedArgs = ProficiencyArgsSchema.parse(args) const query = ProficiencyModel.find() const filters: any[] = [] if (validatedArgs.name != null && validatedArgs.name !== '') { filters.push({ name: { $regex: new RegExp(escapeRegExp(validatedArgs.name), 'i') } }) } if (validatedArgs.type && validatedArgs.type.length > 0) { filters.push({ type: { $in: validatedArgs.type } }) } if (filters.length > 0) { query.where({ $and: filters }) } const sortQuery = buildSortPipeline({ order: validatedArgs.order, sortFieldMap: PROFICIENCY_SORT_FIELD_MAP, defaultSortField: ProficiencyOrderField.NAME }) if (Object.keys(sortQuery).length > 0) { query.sort(sortQuery) } if (validatedArgs.skip !== undefined) { query.skip(validatedArgs.skip) } if (validatedArgs.limit !== undefined) { query.limit(validatedArgs.limit) } return query.lean() } @Query(() => Proficiency2024, { nullable: true, description: 'Gets a single proficiency by index.' }) async proficiency( @Arg('index', () => String) indexInput: string ): Promise { const { index } = ProficiencyIndexArgsSchema.parse({ index: indexInput }) return ProficiencyModel.findOne({ index }).lean() } @FieldResolver(() => [Background2024]) async backgrounds(@Root() proficiency: Proficiency2024): Promise { return resolveMultipleReferences(proficiency.backgrounds, BackgroundModel) } } ================================================ FILE: src/graphql/2024/resolvers/skill/args.ts ================================================ import { ArgsType, Field, InputType, registerEnumType } from 'type-graphql' import { z } from 'zod' import { BaseFilterArgs, BaseFilterArgsSchema, BaseIndexArgsSchema, BaseOrderInterface } from '@/graphql/common/args' import { OrderByDirection } from '@/graphql/common/enums' export enum SkillOrderField { NAME = 'name', ABILITY_SCORE = 'ability_score' } export const SKILL_SORT_FIELD_MAP: Record = { [SkillOrderField.NAME]: 'name', [SkillOrderField.ABILITY_SCORE]: 'ability_score.name' } registerEnumType(SkillOrderField, { name: 'SkillOrderField', description: 'Fields to sort Skills by' }) @InputType() export class SkillOrder implements BaseOrderInterface { @Field(() => SkillOrderField) by!: SkillOrderField @Field(() => OrderByDirection) direction!: OrderByDirection @Field(() => SkillOrder, { nullable: true }) then_by?: SkillOrder } export const SkillOrderSchema: z.ZodType = z.lazy(() => z.object({ by: z.nativeEnum(SkillOrderField), direction: z.nativeEnum(OrderByDirection), then_by: SkillOrderSchema.optional() }) ) export const SkillArgsSchema = z.object({ ...BaseFilterArgsSchema.shape, ability_score: z.array(z.string()).optional(), order: SkillOrderSchema.optional() }) export const SkillIndexArgsSchema = BaseIndexArgsSchema @ArgsType() export class SkillArgs extends BaseFilterArgs { @Field(() => [String], { nullable: true, description: 'Filter by ability score index (e.g., ["str", "dex"])' }) ability_score?: string[] @Field(() => SkillOrder, { nullable: true, description: 'Specify sorting order for skills.' }) order?: SkillOrder } ================================================ FILE: src/graphql/2024/resolvers/skill/resolver.ts ================================================ import { Arg, Args, FieldResolver, Query, Resolver, Root } from 'type-graphql' import { buildSortPipeline } from '@/graphql/common/args' import { resolveSingleReference } from '@/graphql/utils/resolvers' import AbilityScoreModel, { AbilityScore2024 } from '@/models/2024/abilityScore' import SkillModel, { Skill2024 } from '@/models/2024/skill' import { escapeRegExp } from '@/util' import { SKILL_SORT_FIELD_MAP, SkillArgs, SkillArgsSchema, SkillIndexArgsSchema, SkillOrderField } from './args' @Resolver(Skill2024) export class SkillResolver { @Query(() => [Skill2024], { description: 'Gets all skills, optionally filtered by name and sorted by name.' }) async skills(@Args(() => SkillArgs) args: SkillArgs): Promise { const validatedArgs = SkillArgsSchema.parse(args) const query = SkillModel.find() const filters: any[] = [] if (validatedArgs.name != null && validatedArgs.name !== '') { filters.push({ name: { $regex: new RegExp(escapeRegExp(validatedArgs.name), 'i') } }) } if (validatedArgs.ability_score && validatedArgs.ability_score.length > 0) { filters.push({ 'ability_score.index': { $in: validatedArgs.ability_score } }) } if (filters.length > 0) { query.where({ $and: filters }) } const sortQuery = buildSortPipeline({ order: validatedArgs.order, sortFieldMap: SKILL_SORT_FIELD_MAP, defaultSortField: SkillOrderField.NAME }) if (Object.keys(sortQuery).length > 0) { query.sort(sortQuery) } if (validatedArgs.skip !== undefined) { query.skip(validatedArgs.skip) } if (validatedArgs.limit !== undefined) { query.limit(validatedArgs.limit) } return query.lean() } @Query(() => Skill2024, { nullable: true, description: 'Gets a single skill by index.' }) async skill(@Arg('index', () => String) indexInput: string): Promise { const { index } = SkillIndexArgsSchema.parse({ index: indexInput }) return SkillModel.findOne({ index }).lean() } @FieldResolver(() => AbilityScore2024) async ability_score(@Root() skill: Skill2024): Promise { return resolveSingleReference(skill.ability_score, AbilityScoreModel) } } ================================================ FILE: src/graphql/2024/resolvers/species/args.ts ================================================ import { ArgsType, Field, InputType, registerEnumType } from 'type-graphql' import { z } from 'zod' import { BaseFilterArgs, BaseFilterArgsSchema, BaseIndexArgsSchema, BaseOrderInterface } from '@/graphql/common/args' import { OrderByDirection } from '@/graphql/common/enums' export enum SpeciesOrderField { NAME = 'name' } export const SPECIES_SORT_FIELD_MAP: Record = { [SpeciesOrderField.NAME]: 'name' } registerEnumType(SpeciesOrderField, { name: 'Species2024OrderField', description: 'Fields to sort Species by' }) @InputType() export class SpeciesOrder implements BaseOrderInterface { @Field(() => SpeciesOrderField) by!: SpeciesOrderField @Field(() => OrderByDirection) direction!: OrderByDirection @Field(() => SpeciesOrder, { nullable: true }) then_by?: SpeciesOrder } export const SpeciesOrderSchema: z.ZodType = z.lazy(() => z.object({ by: z.nativeEnum(SpeciesOrderField), direction: z.nativeEnum(OrderByDirection), then_by: SpeciesOrderSchema.optional() }) ) export const SpeciesArgsSchema = z.object({ ...BaseFilterArgsSchema.shape, order: SpeciesOrderSchema.optional() }) export const SpeciesIndexArgsSchema = BaseIndexArgsSchema @ArgsType() export class SpeciesArgs extends BaseFilterArgs { @Field(() => SpeciesOrder, { nullable: true, description: 'Specify sorting order for species.' }) order?: SpeciesOrder } ================================================ FILE: src/graphql/2024/resolvers/species/resolver.ts ================================================ import { Arg, Args, FieldResolver, Query, Resolver, Root } from 'type-graphql' import { buildSortPipeline } from '@/graphql/common/args' import { resolveMultipleReferences } from '@/graphql/utils/resolvers' import Species2024Model, { Species2024 } from '@/models/2024/species' import Subspecies2024Model, { Subspecies2024 } from '@/models/2024/subspecies' import Trait2024Model, { Trait2024 } from '@/models/2024/trait' import { escapeRegExp } from '@/util' import { SPECIES_SORT_FIELD_MAP, SpeciesArgs, SpeciesArgsSchema, SpeciesIndexArgsSchema, SpeciesOrderField } from './args' @Resolver(Species2024) export class SpeciesResolver { @Query(() => [Species2024], { description: 'Gets all species, optionally filtered by name and sorted by name.' }) async species2024(@Args(() => SpeciesArgs) args: SpeciesArgs): Promise { const validatedArgs = SpeciesArgsSchema.parse(args) const query = Species2024Model.find() if (validatedArgs.name != null && validatedArgs.name !== '') { query.where({ name: { $regex: new RegExp(escapeRegExp(validatedArgs.name), 'i') } }) } const sortQuery = buildSortPipeline({ order: validatedArgs.order, sortFieldMap: SPECIES_SORT_FIELD_MAP, defaultSortField: SpeciesOrderField.NAME }) if (Object.keys(sortQuery).length > 0) { query.sort(sortQuery) } if (validatedArgs.skip) { query.skip(validatedArgs.skip) } if (validatedArgs.limit) { query.limit(validatedArgs.limit) } return query.lean() } @Query(() => Species2024, { nullable: true, description: 'Gets a single species by index.' }) async species2024ByIndex( @Arg('index', () => String) indexInput: string ): Promise { const { index } = SpeciesIndexArgsSchema.parse({ index: indexInput }) return Species2024Model.findOne({ index }).lean() } @FieldResolver(() => [Subspecies2024], { description: 'The subspecies available for this species.' }) async subspecies(@Root() species: Species2024): Promise { return resolveMultipleReferences(species.subspecies, Subspecies2024Model) } @FieldResolver(() => [Trait2024], { description: 'The traits granted by this species.' }) async traits(@Root() species: Species2024): Promise { return resolveMultipleReferences(species.traits, Trait2024Model) } } ================================================ FILE: src/graphql/2024/resolvers/subclass/args.ts ================================================ import { ArgsType, Field, InputType, registerEnumType } from 'type-graphql' import { z } from 'zod' import { BaseFilterArgs, BaseFilterArgsSchema, BaseIndexArgsSchema, BaseOrderInterface } from '@/graphql/common/args' import { OrderByDirection } from '@/graphql/common/enums' export enum SubclassOrderField { NAME = 'name' } export const SUBCLASS_SORT_FIELD_MAP: Record = { [SubclassOrderField.NAME]: 'name' } registerEnumType(SubclassOrderField, { name: 'SubclassOrderField', description: 'Fields to sort Subclasses by' }) @InputType() export class SubclassOrder implements BaseOrderInterface { @Field(() => SubclassOrderField) by!: SubclassOrderField @Field(() => OrderByDirection) direction!: OrderByDirection @Field(() => SubclassOrder, { nullable: true }) then_by?: SubclassOrder } export const SubclassOrderSchema: z.ZodType = z.lazy(() => z.object({ by: z.nativeEnum(SubclassOrderField), direction: z.nativeEnum(OrderByDirection), then_by: SubclassOrderSchema.optional() }) ) export const SubclassArgsSchema = z.object({ ...BaseFilterArgsSchema.shape, order: SubclassOrderSchema.optional() }) export const SubclassIndexArgsSchema = BaseIndexArgsSchema @ArgsType() export class SubclassArgs extends BaseFilterArgs { @Field(() => SubclassOrder, { nullable: true, description: 'Specify sorting order for subclasses.' }) order?: SubclassOrder } ================================================ FILE: src/graphql/2024/resolvers/subclass/resolver.ts ================================================ import { Arg, Args, Query, Resolver } from 'type-graphql' import { buildSortPipeline } from '@/graphql/common/args' import SubclassModel, { Subclass2024 } from '@/models/2024/subclass' import { escapeRegExp } from '@/util' import { SUBCLASS_SORT_FIELD_MAP, SubclassArgs, SubclassArgsSchema, SubclassIndexArgsSchema, SubclassOrderField } from './args' @Resolver(Subclass2024) export class SubclassResolver { @Query(() => [Subclass2024], { description: 'Gets all subclasses, optionally filtered by name.' }) async subclasses(@Args(() => SubclassArgs) args: SubclassArgs): Promise { const validatedArgs = SubclassArgsSchema.parse(args) const query = SubclassModel.find() if (validatedArgs.name != null && validatedArgs.name !== '') { query.where({ name: { $regex: new RegExp(escapeRegExp(validatedArgs.name), 'i') } }) } const sortQuery = buildSortPipeline({ order: validatedArgs.order, sortFieldMap: SUBCLASS_SORT_FIELD_MAP, defaultSortField: SubclassOrderField.NAME }) if (Object.keys(sortQuery).length > 0) { query.sort(sortQuery) } if (validatedArgs.skip) { query.skip(validatedArgs.skip) } if (validatedArgs.limit) { query.limit(validatedArgs.limit) } return query.lean() } @Query(() => Subclass2024, { nullable: true, description: 'Gets a single subclass by index.' }) async subclass(@Arg('index', () => String) indexInput: string): Promise { const { index } = SubclassIndexArgsSchema.parse({ index: indexInput }) return SubclassModel.findOne({ index }).lean() } } ================================================ FILE: src/graphql/2024/resolvers/subspecies/args.ts ================================================ import { ArgsType, Field, InputType, registerEnumType } from 'type-graphql' import { z } from 'zod' import { BaseFilterArgs, BaseFilterArgsSchema, BaseIndexArgsSchema, BaseOrderInterface } from '@/graphql/common/args' import { OrderByDirection } from '@/graphql/common/enums' export enum SubspeciesOrderField { NAME = 'name' } export const SUBSPECIES_SORT_FIELD_MAP: Record = { [SubspeciesOrderField.NAME]: 'name' } registerEnumType(SubspeciesOrderField, { name: 'Subspecies2024OrderField', description: 'Fields to sort Subspecies by' }) @InputType() export class SubspeciesOrder implements BaseOrderInterface { @Field(() => SubspeciesOrderField) by!: SubspeciesOrderField @Field(() => OrderByDirection) direction!: OrderByDirection @Field(() => SubspeciesOrder, { nullable: true }) then_by?: SubspeciesOrder } export const SubspeciesOrderSchema: z.ZodType = z.lazy(() => z.object({ by: z.nativeEnum(SubspeciesOrderField), direction: z.nativeEnum(OrderByDirection), then_by: SubspeciesOrderSchema.optional() }) ) export const SubspeciesArgsSchema = z.object({ ...BaseFilterArgsSchema.shape, order: SubspeciesOrderSchema.optional() }) export const SubspeciesIndexArgsSchema = BaseIndexArgsSchema @ArgsType() export class SubspeciesArgs extends BaseFilterArgs { @Field(() => SubspeciesOrder, { nullable: true, description: 'Specify sorting order for subspecies.' }) order?: SubspeciesOrder } ================================================ FILE: src/graphql/2024/resolvers/subspecies/resolver.ts ================================================ import { Arg, Args, FieldResolver, Query, Resolver, Root } from 'type-graphql' import { buildSortPipeline } from '@/graphql/common/args' import { resolveMultipleReferences, resolveSingleReference } from '@/graphql/utils/resolvers' import DamageType2024Model, { DamageType2024 } from '@/models/2024/damageType' import Species2024Model, { Species2024 } from '@/models/2024/species' import Subspecies2024Model, { Subspecies2024 } from '@/models/2024/subspecies' import Trait2024Model, { Trait2024 } from '@/models/2024/trait' import { escapeRegExp } from '@/util' import { SUBSPECIES_SORT_FIELD_MAP, SubspeciesArgs, SubspeciesArgsSchema, SubspeciesIndexArgsSchema, SubspeciesOrderField } from './args' @Resolver(Subspecies2024) export class SubspeciesResolver { @Query(() => [Subspecies2024], { description: 'Gets all subspecies, optionally filtered by name and sorted by name.' }) async subspecies2024( @Args(() => SubspeciesArgs) args: SubspeciesArgs ): Promise { const validatedArgs = SubspeciesArgsSchema.parse(args) const query = Subspecies2024Model.find() if (validatedArgs.name != null && validatedArgs.name !== '') { query.where({ name: { $regex: new RegExp(escapeRegExp(validatedArgs.name), 'i') } }) } const sortQuery = buildSortPipeline({ order: validatedArgs.order, sortFieldMap: SUBSPECIES_SORT_FIELD_MAP, defaultSortField: SubspeciesOrderField.NAME }) if (Object.keys(sortQuery).length > 0) { query.sort(sortQuery) } if (validatedArgs.skip) { query.skip(validatedArgs.skip) } if (validatedArgs.limit) { query.limit(validatedArgs.limit) } return query.lean() } @Query(() => Subspecies2024, { nullable: true, description: 'Gets a single subspecies by index.' }) async subspecies2024ByIndex( @Arg('index', () => String) indexInput: string ): Promise { const { index } = SubspeciesIndexArgsSchema.parse({ index: indexInput }) return Subspecies2024Model.findOne({ index }).lean() } @FieldResolver(() => Species2024, { description: 'The parent species of this subspecies.' }) async species(@Root() subspecies: Subspecies2024): Promise { return resolveSingleReference(subspecies.species, Species2024Model) } @FieldResolver(() => [Trait2024], { description: 'The traits associated with this subspecies.' }) async traits(@Root() subspecies: Subspecies2024): Promise { return resolveMultipleReferences(subspecies.traits, Trait2024Model) } @FieldResolver(() => DamageType2024, { nullable: true, description: 'The damage type associated with this subspecies (Dragonborn only).' }) async damage_type(@Root() subspecies: Subspecies2024): Promise { return resolveSingleReference(subspecies.damage_type, DamageType2024Model) } } ================================================ FILE: src/graphql/2024/resolvers/trait/args.ts ================================================ import { ArgsType, Field, InputType, registerEnumType } from 'type-graphql' import { z } from 'zod' import { BaseFilterArgs, BaseFilterArgsSchema, BaseIndexArgsSchema, BaseOrderInterface } from '@/graphql/common/args' import { OrderByDirection } from '@/graphql/common/enums' export enum TraitOrderField { NAME = 'name' } export const TRAIT_SORT_FIELD_MAP: Record = { [TraitOrderField.NAME]: 'name' } registerEnumType(TraitOrderField, { name: 'Trait2024OrderField', description: 'Fields to sort Traits by' }) @InputType() export class TraitOrder implements BaseOrderInterface { @Field(() => TraitOrderField) by!: TraitOrderField @Field(() => OrderByDirection) direction!: OrderByDirection @Field(() => TraitOrder, { nullable: true }) then_by?: TraitOrder } export const TraitOrderSchema: z.ZodType = z.lazy(() => z.object({ by: z.nativeEnum(TraitOrderField), direction: z.nativeEnum(OrderByDirection), then_by: TraitOrderSchema.optional() }) ) export const TraitArgsSchema = z.object({ ...BaseFilterArgsSchema.shape, order: TraitOrderSchema.optional() }) export const TraitIndexArgsSchema = BaseIndexArgsSchema @ArgsType() export class TraitArgs extends BaseFilterArgs { @Field(() => TraitOrder, { nullable: true, description: 'Specify sorting order for traits.' }) order?: TraitOrder } ================================================ FILE: src/graphql/2024/resolvers/trait/resolver.ts ================================================ import { Arg, Args, FieldResolver, Query, Resolver, Root } from 'type-graphql' import { buildSortPipeline } from '@/graphql/common/args' import { resolveMultipleReferences } from '@/graphql/utils/resolvers' import Species2024Model, { Species2024 } from '@/models/2024/species' import Subspecies2024Model, { Subspecies2024 } from '@/models/2024/subspecies' import Trait2024Model, { Trait2024 } from '@/models/2024/trait' import { escapeRegExp } from '@/util' import { TRAIT_SORT_FIELD_MAP, TraitArgs, TraitArgsSchema, TraitIndexArgsSchema, TraitOrderField } from './args' @Resolver(Trait2024) export class TraitResolver { @Query(() => [Trait2024], { description: 'Gets all traits, optionally filtered by name and sorted by name.' }) async traits2024(@Args(() => TraitArgs) args: TraitArgs): Promise { const validatedArgs = TraitArgsSchema.parse(args) const query = Trait2024Model.find() if (validatedArgs.name != null && validatedArgs.name !== '') { query.where({ name: { $regex: new RegExp(escapeRegExp(validatedArgs.name), 'i') } }) } const sortQuery = buildSortPipeline({ order: validatedArgs.order, sortFieldMap: TRAIT_SORT_FIELD_MAP, defaultSortField: TraitOrderField.NAME }) if (Object.keys(sortQuery).length > 0) { query.sort(sortQuery) } if (validatedArgs.skip) { query.skip(validatedArgs.skip) } if (validatedArgs.limit) { query.limit(validatedArgs.limit) } return query.lean() } @Query(() => Trait2024, { nullable: true, description: 'Gets a single trait by index.' }) async trait2024(@Arg('index', () => String) indexInput: string): Promise { const { index } = TraitIndexArgsSchema.parse({ index: indexInput }) return Trait2024Model.findOne({ index }).lean() } @FieldResolver(() => [Species2024], { description: 'The species that grant this trait.' }) async species(@Root() trait: Trait2024): Promise { return resolveMultipleReferences(trait.species, Species2024Model) } @FieldResolver(() => [Subspecies2024], { description: 'The subspecies that grant this trait.' }) async subspecies(@Root() trait: Trait2024): Promise { return resolveMultipleReferences(trait.subspecies, Subspecies2024Model) } } ================================================ FILE: src/graphql/2024/resolvers/weaponMasteryProperty/args.ts ================================================ import { ArgsType, Field, InputType, registerEnumType } from 'type-graphql' import { z } from 'zod' import { BaseFilterArgs, BaseFilterArgsSchema, BaseIndexArgsSchema, BaseOrderInterface } from '@/graphql/common/args' import { OrderByDirection } from '@/graphql/common/enums' export enum WeaponMasteryPropertyOrderField { NAME = 'name' } export const WEAPON_MASTERY_PROPERTY_SORT_FIELD_MAP: Record< WeaponMasteryPropertyOrderField, string > = { [WeaponMasteryPropertyOrderField.NAME]: 'name' } registerEnumType(WeaponMasteryPropertyOrderField, { name: 'WeaponMasteryPropertyOrderField', description: 'Fields to sort Weapon Mastery Properties by' }) @InputType() export class WeaponMasteryPropertyOrder implements BaseOrderInterface { @Field(() => WeaponMasteryPropertyOrderField) by!: WeaponMasteryPropertyOrderField @Field(() => OrderByDirection) direction!: OrderByDirection @Field(() => WeaponMasteryPropertyOrder, { nullable: true }) then_by?: WeaponMasteryPropertyOrder } export const WeaponMasteryPropertyOrderSchema: z.ZodType = z.lazy(() => z.object({ by: z.nativeEnum(WeaponMasteryPropertyOrderField), direction: z.nativeEnum(OrderByDirection), then_by: WeaponMasteryPropertyOrderSchema.optional() }) ) export const WeaponMasteryPropertyArgsSchema = z.object({ ...BaseFilterArgsSchema.shape, order: WeaponMasteryPropertyOrderSchema.optional() }) export const WeaponMasteryPropertyIndexArgsSchema = BaseIndexArgsSchema @ArgsType() export class WeaponMasteryPropertyArgs extends BaseFilterArgs { @Field(() => WeaponMasteryPropertyOrder, { nullable: true, description: 'Specify sorting order for weapon mastery properties.' }) order?: WeaponMasteryPropertyOrder } ================================================ FILE: src/graphql/2024/resolvers/weaponMasteryProperty/resolver.ts ================================================ import { Arg, Args, Query, Resolver } from 'type-graphql' import { buildSortPipeline } from '@/graphql/common/args' import WeaponMasteryPropertyModel, { WeaponMasteryProperty2024 } from '@/models/2024/weaponMasteryProperty' import { escapeRegExp } from '@/util' import { WEAPON_MASTERY_PROPERTY_SORT_FIELD_MAP, WeaponMasteryPropertyArgs, WeaponMasteryPropertyArgsSchema, WeaponMasteryPropertyIndexArgsSchema, WeaponMasteryPropertyOrderField } from './args' @Resolver(WeaponMasteryProperty2024) export class WeaponMasteryPropertyResolver { @Query(() => [WeaponMasteryProperty2024], { description: 'Gets all weapon mastery properties, optionally filtered by name and sorted by name.' }) async weaponMasteryProperties( @Args(() => WeaponMasteryPropertyArgs) args: WeaponMasteryPropertyArgs ): Promise { const validatedArgs = WeaponMasteryPropertyArgsSchema.parse(args) const query = WeaponMasteryPropertyModel.find() if (validatedArgs.name != null && validatedArgs.name !== '') { query.where({ name: { $regex: new RegExp(escapeRegExp(validatedArgs.name), 'i') } }) } const sortQuery = buildSortPipeline({ order: validatedArgs.order, sortFieldMap: WEAPON_MASTERY_PROPERTY_SORT_FIELD_MAP, defaultSortField: WeaponMasteryPropertyOrderField.NAME }) if (Object.keys(sortQuery).length > 0) { query.sort(sortQuery) } if (validatedArgs.skip) { query.skip(validatedArgs.skip) } if (validatedArgs.limit) { query.limit(validatedArgs.limit) } return query.lean() } @Query(() => WeaponMasteryProperty2024, { nullable: true, description: 'Gets a single weapon mastery property by index.' }) async weaponMasteryProperty( @Arg('index', () => String) indexInput: string ): Promise { const { index } = WeaponMasteryPropertyIndexArgsSchema.parse({ index: indexInput }) return WeaponMasteryPropertyModel.findOne({ index }).lean() } } ================================================ FILE: src/graphql/2024/resolvers/weaponProperty/args.ts ================================================ import { ArgsType, Field, InputType, registerEnumType } from 'type-graphql' import { z } from 'zod' import { BaseFilterArgs, BaseFilterArgsSchema, BaseIndexArgsSchema, BaseOrderInterface } from '@/graphql/common/args' import { OrderByDirection } from '@/graphql/common/enums' export enum WeaponPropertyOrderField { NAME = 'name' } export const WEAPON_PROPERTY_SORT_FIELD_MAP: Record = { [WeaponPropertyOrderField.NAME]: 'name' } registerEnumType(WeaponPropertyOrderField, { name: 'WeaponPropertyOrderField', description: 'Fields to sort Weapon Properties by' }) @InputType() export class WeaponPropertyOrder implements BaseOrderInterface { @Field(() => WeaponPropertyOrderField) by!: WeaponPropertyOrderField @Field(() => OrderByDirection) direction!: OrderByDirection @Field(() => WeaponPropertyOrder, { nullable: true }) then_by?: WeaponPropertyOrder } export const WeaponPropertyOrderSchema: z.ZodType = z.lazy(() => z.object({ by: z.nativeEnum(WeaponPropertyOrderField), direction: z.nativeEnum(OrderByDirection), then_by: WeaponPropertyOrderSchema.optional() }) ) export const WeaponPropertyArgsSchema = z.object({ ...BaseFilterArgsSchema.shape, order: WeaponPropertyOrderSchema.optional() }) export const WeaponPropertyIndexArgsSchema = BaseIndexArgsSchema @ArgsType() export class WeaponPropertyArgs extends BaseFilterArgs { @Field(() => WeaponPropertyOrder, { nullable: true, description: 'Specify sorting order for weapon properties.' }) order?: WeaponPropertyOrder } ================================================ FILE: src/graphql/2024/resolvers/weaponProperty/resolver.ts ================================================ import { Arg, Args, Query, Resolver } from 'type-graphql' import { buildSortPipeline } from '@/graphql/common/args' import WeaponPropertyModel, { WeaponProperty2024 } from '@/models/2024/weaponProperty' import { escapeRegExp } from '@/util' import { WEAPON_PROPERTY_SORT_FIELD_MAP, WeaponPropertyArgs, WeaponPropertyArgsSchema, WeaponPropertyIndexArgsSchema, WeaponPropertyOrderField } from './args' @Resolver(WeaponProperty2024) export class WeaponPropertyResolver { @Query(() => [WeaponProperty2024], { description: 'Gets all weapon properties, optionally filtered by name and sorted by name.' }) async weaponProperties( @Args(() => WeaponPropertyArgs) args: WeaponPropertyArgs ): Promise { const validatedArgs = WeaponPropertyArgsSchema.parse(args) const query = WeaponPropertyModel.find() if (validatedArgs.name != null && validatedArgs.name !== '') { query.where({ name: { $regex: new RegExp(escapeRegExp(validatedArgs.name), 'i') } }) } const sortQuery = buildSortPipeline({ order: validatedArgs.order, sortFieldMap: WEAPON_PROPERTY_SORT_FIELD_MAP, defaultSortField: WeaponPropertyOrderField.NAME }) if (Object.keys(sortQuery).length > 0) { query.sort(sortQuery) } if (validatedArgs.skip) { query.skip(validatedArgs.skip) } if (validatedArgs.limit) { query.limit(validatedArgs.limit) } return query.lean() } @Query(() => WeaponProperty2024, { nullable: true, description: 'Gets a single weapon property by index.' }) async weaponProperty( @Arg('index', () => String) indexInput: string ): Promise { const { index } = WeaponPropertyIndexArgsSchema.parse({ index: indexInput }) return WeaponPropertyModel.findOne({ index }).lean() } } ================================================ FILE: src/graphql/2024/types/backgroundEquipment/choice.ts ================================================ import { Field, Int, ObjectType } from 'type-graphql' import { EquipmentOptionSet2024 } from './optionSet' @ObjectType({ description: 'A top-level equipment choice for a 2024 background.' }) export class BackgroundEquipmentChoice2024 { @Field(() => String, { nullable: true, description: 'Description of the choice.' }) desc?: string @Field(() => Int, { description: 'Number to choose.' }) choose!: number @Field(() => String, { description: "The type of choice, e.g., 'equipment'." }) type!: string @Field(() => EquipmentOptionSet2024, { description: 'The set of equipment options.' }) from!: EquipmentOptionSet2024 } ================================================ FILE: src/graphql/2024/types/backgroundEquipment/common.ts ================================================ import { createUnionType, Field, Int, ObjectType } from 'type-graphql' import { EquipmentCategory2024 } from '@/models/2024/equipmentCategory' import { AnyEquipment } from '../../common/unions' // --- Money Option --- @ObjectType({ description: 'A money option within an equipment choice.' }) export class MoneyOption2024 { @Field(() => String, { description: "The type of this option, e.g., 'money'." }) option_type!: string @Field(() => Int, { description: 'Amount of money.' }) count!: number @Field(() => String, { description: "Currency unit (e.g., 'gp')." }) unit!: string } // --- Counted Reference Option --- @ObjectType({ description: 'A counted reference to a specific piece of equipment.' }) export class CountedReferenceOption2024 { @Field(() => String, { description: "The type of this option, e.g., 'counted_reference'." }) option_type!: string @Field(() => Int, { description: 'Quantity of the referenced item.' }) count!: number @Field(() => AnyEquipment, { description: 'The referenced equipment item.' }) of!: typeof AnyEquipment } // --- Equipment Category Set (used inside ChoiceItemOption2024) --- @ObjectType({ description: 'A set of equipment choices derived from an equipment category.' }) export class EquipmentCategorySet2024 { @Field(() => String, { description: "Indicates the type of option set, e.g., 'equipment_category'." }) option_set_type!: string @Field(() => EquipmentCategory2024, { description: 'The equipment category to choose from.' }) equipment_category!: EquipmentCategory2024 } @ObjectType({ description: 'Details of a nested choice limited to an equipment category.' }) export class EquipmentCategoryChoice2024 { @Field(() => Int, { description: 'Number of items to choose from the category.' }) choose!: number @Field(() => String, { description: "Type of choice, e.g., 'equipment'." }) type!: string @Field(() => EquipmentCategorySet2024, { description: 'The equipment category to choose from.' }) from!: EquipmentCategorySet2024 } @ObjectType({ description: 'An option representing a choice from a single equipment category.' }) export class ChoiceItemOption2024 { @Field(() => String, { description: "The type of this option, e.g., 'choice'." }) option_type!: string @Field(() => EquipmentCategoryChoice2024, { description: 'The nested equipment category choice.' }) choice!: EquipmentCategoryChoice2024 } // --- Multiple Items Option --- export const MultipleItemUnion2024 = createUnionType({ name: 'MultipleItemUnion2024', description: 'An item within a multiple-items equipment option.', types: () => [CountedReferenceOption2024, MoneyOption2024, ChoiceItemOption2024] as const, resolveType(value) { if (value.option_type === 'counted_reference') return CountedReferenceOption2024 if (value.option_type === 'money') return MoneyOption2024 if (value.option_type === 'choice') return ChoiceItemOption2024 return undefined } }) @ObjectType({ description: 'A bundle of multiple equipment items.' }) export class MultipleItemsOption2024 { @Field(() => String, { description: "The type of this option, e.g., 'multiple'." }) option_type!: string @Field(() => [MultipleItemUnion2024], { description: 'The items included in this bundle.' }) items!: Array } ================================================ FILE: src/graphql/2024/types/backgroundEquipment/index.ts ================================================ export * from './choice' export * from './common' export * from './optionSet' ================================================ FILE: src/graphql/2024/types/backgroundEquipment/optionSet.ts ================================================ import { createUnionType, Field, ObjectType } from 'type-graphql' import { CountedReferenceOption2024, MoneyOption2024, MultipleItemsOption2024 } from './common' @ObjectType({ description: 'A set of explicitly listed equipment options.' }) export class EquipmentOptionSet2024 { @Field(() => String, { description: "Indicates the type of option set, e.g., 'options_array'." }) option_set_type!: string @Field(() => [EquipmentOptionUnion2024], { description: 'A list of specific equipment options.' }) options!: Array } export const EquipmentOptionUnion2024 = createUnionType({ name: 'EquipmentOptionUnion2024', description: 'An option within a background equipment choice.', types: () => [CountedReferenceOption2024, MultipleItemsOption2024, MoneyOption2024] as const, resolveType(value) { if (value.option_type === 'counted_reference') return CountedReferenceOption2024 if (value.option_type === 'multiple') return MultipleItemsOption2024 if (value.option_type === 'money') return MoneyOption2024 return undefined } }) ================================================ FILE: src/graphql/2024/utils/backgroundEquipmentResolver.ts ================================================ import EquipmentModel from '@/models/2024/equipment' import EquipmentCategoryModel, { EquipmentCategory2024 } from '@/models/2024/equipmentCategory' import { Choice, ChoiceOption, CountedReferenceOption, EquipmentCategoryOptionSet, MoneyOption, MultipleOption, OptionsArrayOptionSet } from '@/models/common/choice' import { BackgroundEquipmentChoice2024 } from '../types/backgroundEquipment/choice' import { ChoiceItemOption2024, CountedReferenceOption2024, EquipmentCategoryChoice2024, EquipmentCategorySet2024, MoneyOption2024, MultipleItemsOption2024 } from '../types/backgroundEquipment/common' import { EquipmentOptionSet2024 } from '../types/backgroundEquipment/optionSet' // --- Main entry point --- export async function resolveBackgroundEquipmentChoices( choices: Choice[] | undefined ): Promise { if (!choices) return [] const resolved: BackgroundEquipmentChoice2024[] = [] for (const choice of choices) { const resolvedChoice = await resolveBackgroundEquipmentChoice(choice) if (resolvedChoice) resolved.push(resolvedChoice) } return resolved } // --- Single choice --- async function resolveBackgroundEquipmentChoice( choice: Choice ): Promise { const resolvedFrom = await resolveEquipmentOptionSet(choice.from as OptionsArrayOptionSet) if (!resolvedFrom) return null return { desc: choice.desc, choose: choice.choose, type: choice.type, from: resolvedFrom } } // --- Option set --- async function resolveEquipmentOptionSet( optionSet: OptionsArrayOptionSet ): Promise { const resolvedOptions: Array< CountedReferenceOption2024 | MultipleItemsOption2024 | MoneyOption2024 > = [] for (const option of optionSet.options) { const resolved = await resolveEquipmentOptionUnion( option as CountedReferenceOption | MultipleOption | MoneyOption ) if (resolved) resolvedOptions.push(resolved) } return { option_set_type: optionSet.option_set_type, options: resolvedOptions } } // --- Top-level option union --- async function resolveEquipmentOptionUnion( option: CountedReferenceOption | MultipleOption | MoneyOption ): Promise { switch (option.option_type) { case 'counted_reference': return resolveCountedReferenceOption(option as CountedReferenceOption) case 'multiple': return resolveMultipleItemsOption(option as MultipleOption) case 'money': return resolveMoneyOption(option as MoneyOption) default: console.warn(`Unknown top-level option_type: ${option.option_type}`) return null } } // --- Counted reference --- async function resolveCountedReferenceOption( option: CountedReferenceOption ): Promise { if (!option.of?.index) return null const equipment = await EquipmentModel.findOne({ index: option.of.index }).lean() if (!equipment) return null return { option_type: option.option_type, count: option.count, of: equipment } as CountedReferenceOption2024 } // --- Multiple items --- async function resolveMultipleItemsOption( option: MultipleOption ): Promise { if (!option.items?.length) return null const resolvedItems: Array = [] for (const item of option.items) { let resolved: CountedReferenceOption2024 | MoneyOption2024 | ChoiceItemOption2024 | null = null if (item.option_type === 'counted_reference') { resolved = await resolveCountedReferenceOption(item as CountedReferenceOption) } else if (item.option_type === 'money') { resolved = resolveMoneyOption(item as MoneyOption) } else if (item.option_type === 'choice') { resolved = await resolveChoiceItemOption(item as ChoiceOption) } else { console.warn(`Unknown option_type within MultipleOption items: ${item.option_type}`) } if (resolved) resolvedItems.push(resolved) } return { option_type: option.option_type, items: resolvedItems } } // --- Choice item (equipment category) --- async function resolveChoiceItemOption(option: ChoiceOption): Promise { const nestedChoice = option.choice const nestedFrom = nestedChoice.from as EquipmentCategoryOptionSet if ( nestedFrom.option_set_type !== 'equipment_category' || !nestedFrom.equipment_category?.index ) { console.warn('ChoiceOption.choice.from is not a valid EquipmentCategoryOptionSet:', nestedFrom) return null } const equipmentCategory = await EquipmentCategoryModel.findOne({ index: nestedFrom.equipment_category.index }).lean() if (!equipmentCategory) { console.warn(`Equipment category not found: ${nestedFrom.equipment_category.index}`) return null } const categorySet: EquipmentCategorySet2024 = { option_set_type: nestedFrom.option_set_type, equipment_category: equipmentCategory as EquipmentCategory2024 } const equipmentCategoryChoice: EquipmentCategoryChoice2024 = { choose: nestedChoice.choose, type: nestedChoice.type, from: categorySet } return { option_type: option.option_type, choice: equipmentCategoryChoice } } // --- Money --- function resolveMoneyOption(option: MoneyOption): MoneyOption2024 { return { option_type: option.option_type, count: option.count, unit: option.unit } } ================================================ FILE: src/graphql/2024/utils/choiceResolvers.ts ================================================ import { Proficiency2024Choice, Proficiency2024ChoiceOption, Proficiency2024ChoiceOptionSet, ScorePrerequisiteChoice2024, ScorePrerequisiteOption2024, ScorePrerequisiteOptionSet2024 } from '@/graphql/2024/common/choiceTypes' import { resolveSingleReference } from '@/graphql/utils/resolvers' import AbilityScoreModel, { AbilityScore2024 } from '@/models/2024/abilityScore' import ProficiencyModel, { Proficiency2024 } from '@/models/2024/proficiency' import { Choice, OptionsArrayOptionSet, ReferenceOption, ScorePrerequisiteOption } from '@/models/common/choice' export async function resolveScorePrerequisiteChoice( choiceData: Choice | undefined ): Promise { if (!choiceData) return null const optionsArraySet = choiceData.from as OptionsArrayOptionSet const resolvedOptions: ScorePrerequisiteOption2024[] = [] for (const dbOption of optionsArraySet.options) { const dbScoreOpt = dbOption as ScorePrerequisiteOption const abilityScore = await resolveSingleReference(dbScoreOpt.ability_score, AbilityScoreModel) if (abilityScore !== null) { resolvedOptions.push({ option_type: dbScoreOpt.option_type, ability_score: abilityScore as AbilityScore2024, minimum_score: dbScoreOpt.minimum_score }) } } return { desc: choiceData.desc, choose: choiceData.choose, type: choiceData.type, from: { option_set_type: optionsArraySet.option_set_type, options: resolvedOptions } as ScorePrerequisiteOptionSet2024 } } export async function resolveProficiency2024Choice( choiceData: Choice | undefined ): Promise { if (!choiceData) return null const optionsArraySet = choiceData.from as OptionsArrayOptionSet const resolvedOptions: Proficiency2024ChoiceOption[] = [] for (const dbOption of optionsArraySet.options) { const dbRefOpt = dbOption as ReferenceOption const proficiency = await resolveSingleReference(dbRefOpt.item, ProficiencyModel) if (proficiency !== null) { resolvedOptions.push({ option_type: dbRefOpt.option_type, item: proficiency as Proficiency2024 }) } } return { desc: choiceData.desc, choose: choiceData.choose, type: choiceData.type, from: { option_set_type: optionsArraySet.option_set_type, options: resolvedOptions } as Proficiency2024ChoiceOptionSet } } export async function resolveProficiency2024ChoiceArray( choices: Choice[] | undefined ): Promise { if (!choices || !Array.isArray(choices)) return [] const resolved: Proficiency2024Choice[] = [] for (const choice of choices) { const resolvedChoice = await resolveProficiency2024Choice(choice) if (resolvedChoice) resolved.push(resolvedChoice) } return resolved } ================================================ FILE: src/graphql/2024/utils/resolvers.ts ================================================ ================================================ FILE: src/graphql/common/args.ts ================================================ import { ArgsType, Field, Int } from 'type-graphql' import { z } from 'zod' import { OrderByDirection } from '@/graphql/common/enums' // --- Pagination --- export const BasePaginationArgsSchema = z.object({ skip: z.number().int().min(0).optional().default(0), limit: z.number().int().min(1).optional().default(100) // Default 50, Max 100 }) @ArgsType() export class BasePaginationArgs { @Field(() => Int, { nullable: true, description: 'Number of results to skip for pagination. Default: 0.' }) skip?: number @Field(() => Int, { nullable: true, description: 'Maximum number of results to return for pagination. Default: 50, Max: 100.' }) limit?: number } // --- Filtering & Sorting by Name (includes Pagination) --- export const BaseFilterArgsSchema = z.object({ ...BasePaginationArgsSchema.shape, name: z.string().optional() }) @ArgsType() export class BaseFilterArgs extends BasePaginationArgs { @Field(() => String, { nullable: true, description: 'Filter by name (case-insensitive, partial match).' }) name?: string } // --- Index Argument --- export const BaseIndexArgsSchema = z.object({ index: z.string().min(1, { message: 'Index must be a non-empty string' }) }) // --- Generic Sorting Logic --- export interface BaseOrderInterface { by: TOrderFieldValue direction: OrderByDirection then_by?: BaseOrderInterface } interface BuildSortPipelineArgs { order?: BaseOrderInterface sortFieldMap: Record defaultSortField?: TOrderFieldValue defaultSortDirection?: OrderByDirection } export function buildSortPipeline({ order, sortFieldMap, defaultSortField, defaultSortDirection = OrderByDirection.ASC }: BuildSortPipelineArgs): Record { const sortPipeline: Record = {} function populateSortPipelineRecursive( currentOrder: BaseOrderInterface | undefined ) { if (!currentOrder) { return } const fieldKey = currentOrder.by const mappedDbField = sortFieldMap[fieldKey] if (mappedDbField) { const direction = currentOrder.direction === OrderByDirection.ASC ? 1 : -1 sortPipeline[mappedDbField] = direction } else { console.warn( `Sort field for key "${fieldKey}" not found in sortFieldMap. Skipping this criterion.` ) } if (currentOrder.then_by) { populateSortPipelineRecursive(currentOrder.then_by) } } populateSortPipelineRecursive(order) if (Object.keys(sortPipeline).length === 0 && defaultSortField != null) { const defaultFieldKey = String(defaultSortField) const defaultMappedField = sortFieldMap[defaultFieldKey] if (defaultMappedField) { sortPipeline[defaultMappedField] = defaultSortDirection === OrderByDirection.ASC ? 1 : -1 } else { console.warn( `Default sort field for key "${defaultFieldKey}" not found in sortFieldMap. No default sort will be applied.` ) } } return sortPipeline } ================================================ FILE: src/graphql/common/choiceTypes.ts ================================================ import { Field, Int, ObjectType } from 'type-graphql' // --- Generic String Choice Types --- @ObjectType({ description: 'Represents a single string option within a choice (e.g., a flaw, a bond).' }) export class StringChoiceOption { @Field(() => String, { description: 'The text content of the string option.' }) string!: string @Field(() => String, { description: 'The type of the string option.' }) option_type!: string } @ObjectType({ description: 'Represents a set of string options.' }) export class StringChoiceOptionSet { @Field(() => String, { description: 'The type of the string option set.' }) option_set_type!: string @Field(() => [StringChoiceOption], { description: 'The list of string options available.' }) options!: StringChoiceOption[] } @ObjectType({ description: 'Represents a choice from a list of string options.' }) export class StringChoice { @Field(() => Int, { description: 'The number of options to choose from this list.' }) choose!: number @Field(() => String, { description: 'The type or category of the choice.' }) type!: string @Field(() => StringChoiceOptionSet, { description: 'The set of string options available.' }) from!: StringChoiceOptionSet } ================================================ FILE: src/graphql/common/enums.ts ================================================ import { registerEnumType } from 'type-graphql' export enum OrderByDirection { ASC = 'ASC', DESC = 'DESC' } registerEnumType(OrderByDirection, { name: 'OrderByDirection', description: 'Specifies the direction for ordering results.' }) ================================================ FILE: src/graphql/common/inputs.ts ================================================ import { Field, InputType, Int } from 'type-graphql' import { z } from 'zod' // Zod schema for NumberRangeFilterInput export const NumberRangeFilterInputSchema = z.object({ lt: z.number().int().optional(), lte: z.number().int().optional(), gt: z.number().int().optional(), gte: z.number().int().optional() }) @InputType({ description: 'Input for filtering integer fields, allowing exact match, a list of matches, or a range.' }) export class NumberRangeFilterInput { @Field(() => Int, { nullable: true, description: 'Matches values less than.' }) lt?: number @Field(() => Int, { nullable: true, description: 'Matches values less than or equal to.' }) lte?: number @Field(() => Int, { nullable: true, description: 'Matches values greater than.' }) gt?: number @Field(() => Int, { nullable: true, description: 'Matches values greater than or equal to.' }) gte?: number } // Zod schema for NumberFilterInput export const NumberFilterInputSchema = z.object({ eq: z.number().int().optional(), in: z.array(z.number().int()).optional(), nin: z.array(z.number().int()).optional(), range: NumberRangeFilterInputSchema.optional() }) @InputType({ description: 'Input for filtering by an integer, an array of integers, or a range of integers.' }) export class NumberFilterInput { @Field(() => Int, { nullable: true, description: 'Matches an exact integer value.' }) eq?: number; @Field(() => [Int], { nullable: true, description: 'Matches any integer value in the provided list.' }) in?: number[] @Field(() => [Int], { nullable: true, description: 'Matches no integer value in the provided list.' }) nin?: number[] @Field(() => NumberRangeFilterInput, { nullable: true, description: 'Matches integer values within a specified range.' }) range?: NumberRangeFilterInput } /** * Builds a MongoDB query object from a NumberFilterInput. * @param filterInput The NumberFilterInput object. * @returns A MongoDB query object for the number field, or null if the input is empty or invalid. */ export function buildMongoQueryFromNumberFilter( filterInput?: NumberFilterInput ): Record | null { if (!filterInput) { return null } const queryPortion: any = {} if (typeof filterInput.eq === 'number') { queryPortion.$eq = filterInput.eq } if (Array.isArray(filterInput.in) && filterInput.in.length > 0) { queryPortion.$in = filterInput.in } if (Array.isArray(filterInput.nin) && filterInput.nin.length > 0) { queryPortion.$nin = filterInput.nin } if (filterInput.range) { if (typeof filterInput.range.lt === 'number') queryPortion.$lt = filterInput.range.lt if (typeof filterInput.range.lte === 'number') queryPortion.$lte = filterInput.range.lte if (typeof filterInput.range.gt === 'number') queryPortion.$gt = filterInput.range.gt if (typeof filterInput.range.gte === 'number') queryPortion.$gte = filterInput.range.gte } if (Object.keys(queryPortion).length === 0) { return null } return queryPortion } ================================================ FILE: src/graphql/common/types.ts ================================================ import { Field, Int, ObjectType } from 'type-graphql' @ObjectType({ description: 'A key-value pair representing a value at a specific level.' }) export class LevelValue { @Field(() => Int, { description: 'The level.' }) level!: number @Field(() => String, { description: 'The value associated with the level.' }) value!: string } @ObjectType({ description: 'Represents a count of spell slots for a specific level.' }) export class SpellSlotCount { @Field(() => Int, { description: 'The spell slot level.' }) slot_level!: number @Field(() => Int, { description: 'The number of spell slots available for this level.' }) count!: number } ================================================ FILE: src/graphql/utils/resolvers.ts ================================================ import { ReturnModelType } from '@typegoose/typegoose' import { AnyParamConstructor } from '@typegoose/typegoose/lib/types' import { StringChoice, StringChoiceOption, StringChoiceOptionSet } from '@/graphql/common/choiceTypes' import { APIReference } from '@/models/common/apiReference' import { Choice, OptionsArrayOptionSet, ReferenceOption, StringOption } from '@/models/common/choice' export async function resolveSingleReference( reference: APIReference | null | undefined, TargetModel: ReturnModelType> ): Promise { if (reference == null || reference.index == null || reference.index === '') { return null } return TargetModel.findOne({ index: reference.index }).lean() as any } export async function resolveMultipleReferences( references: APIReference[] | null | undefined, TargetModel: ReturnModelType> ): Promise { if (!references || references.length === 0) { return [] } const indices = references.map((ref) => ref.index) return TargetModel.find({ index: { $in: indices } }).lean() as any } export async function resolveReferenceOptionArray< TItem, TGqlItemChoiceOption extends { option_type: string; item: TItem } >( optionsArraySet: OptionsArrayOptionSet, ItemModel: ReturnModelType>, createGqlOption: (item: TItem, optionType: string) => TGqlItemChoiceOption ): Promise { const resolvedEmbeddedOptions: TGqlItemChoiceOption[] = [] for (const dbOption of optionsArraySet.options) { const dbRefOpt = dbOption as ReferenceOption const resolvedItem = await resolveSingleReference(dbRefOpt.item, ItemModel) if (resolvedItem !== null) { resolvedEmbeddedOptions.push(createGqlOption(resolvedItem as TItem, dbRefOpt.option_type)) } } return resolvedEmbeddedOptions } export function resolveStringChoice(choiceData: Choice): StringChoice { const dbOptionSet = choiceData.from as OptionsArrayOptionSet const gqlChoiceOptions: StringChoiceOption[] = [] for (const dbOption of dbOptionSet.options) { const dbStringOpt = dbOption as StringOption gqlChoiceOptions.push({ string: dbStringOpt.string, option_type: dbStringOpt.option_type || 'string' // Fallback for option_type }) } const gqlOptionSet: StringChoiceOptionSet = { option_set_type: dbOptionSet.option_set_type, options: gqlChoiceOptions } return { choose: choiceData.choose, type: choiceData.type, from: gqlOptionSet } } ================================================ FILE: src/middleware/apolloServer.ts ================================================ import { ApolloServer } from '@apollo/server' import { ApolloServerPluginCacheControl } from '@apollo/server/plugin/cacheControl' import depthLimit from 'graphql-depth-limit' const createApolloMiddleware = async (schema: any) => { const server = new ApolloServer({ schema, plugins: [ ApolloServerPluginCacheControl({ // Cache everything for 1 second by default. defaultMaxAge: 3600, // Don't send the `cache-control` response header. calculateHttpHeaders: false }) ], introspection: true, validationRules: [depthLimit(20)] }) return server } export { createApolloMiddleware } ================================================ FILE: src/middleware/bugsnag.ts ================================================ import bugsnag from '@bugsnag/js' import bugsnagExpress from '@bugsnag/plugin-express' import { bugsnagApiKey } from '@/util' const createBugsnagMiddleware = () => { if (bugsnagApiKey == null || bugsnagApiKey === '') { return null } const bugsnagClient = bugsnag.start({ apiKey: bugsnagApiKey, plugins: [bugsnagExpress] }) return bugsnagClient.getPlugin('express') } const bugsnagMiddleware = createBugsnagMiddleware() export default bugsnagMiddleware ================================================ FILE: src/middleware/errorHandler.ts ================================================ import { NextFunction, Request, Response } from 'express' /* eslint-disable @typescript-eslint/no-unused-vars */ const errorHandler = (err: any, req: Request, res: Response, next: NextFunction): void => { console.error(err.stack) const statusCode = typeof err.status === 'number' ? err.status : 404 res.status(statusCode).json({ message: err.message }) } export default errorHandler ================================================ FILE: src/models/2014/abilityScore.ts ================================================ import { getModelForClass, prop } from '@typegoose/typegoose' import { DocumentType } from '@typegoose/typegoose/lib/types' import { Field, ObjectType } from 'type-graphql' import { APIReference } from '@/models/common/apiReference' import { srdModelOptions } from '@/util/modelOptions' import { Skill } from './skill' @ObjectType({ description: 'An ability score representing a fundamental character attribute (e.g., Strength, Dexterity).' }) @srdModelOptions('2014-ability-scores') export class AbilityScore { @Field(() => [String], { description: 'A description of the ability score and its applications.' }) @prop({ required: true, index: true, type: () => [String] }) public desc!: string[] @Field(() => String, { description: 'The full name of the ability score (e.g., Strength).' }) @prop({ required: true, index: true, type: () => String }) public full_name!: string @Field(() => String, { description: 'The unique identifier for this ability score (e.g., str).' }) @prop({ required: true, index: true, type: () => String }) public index!: string @Field(() => String, { description: 'The abbreviated name of the ability score (e.g., STR).' }) @prop({ required: true, index: true, type: () => String }) public name!: string @Field(() => [Skill], { description: 'Skills associated with this ability score.' }) @prop({ type: () => [APIReference] }) public skills!: APIReference[] @prop({ required: true, index: true, type: () => String }) public url!: string @Field(() => String, { description: 'Timestamp of the last update.' }) @prop({ required: true, index: true, type: () => String }) public updated_at!: string } export type AbilityScoreDocument = DocumentType const AbilityScoreModel = getModelForClass(AbilityScore) export default AbilityScoreModel ================================================ FILE: src/models/2014/alignment.ts ================================================ import { getModelForClass, prop } from '@typegoose/typegoose' import { DocumentType } from '@typegoose/typegoose/lib/types' import { Field, ObjectType } from 'type-graphql' import { srdModelOptions } from '@/util/modelOptions' @ObjectType({ description: "Represents a creature's moral and ethical outlook." }) @srdModelOptions('2014-alignments') export class Alignment { @Field(() => String, { description: 'A brief description of the alignment.' }) @prop({ required: true, index: true, type: () => String }) public desc!: string @Field(() => String, { description: 'A shortened representation of the alignment (e.g., LG, CE).' }) @prop({ required: true, index: true, type: () => String }) public abbreviation!: string @Field(() => String, { description: 'The unique identifier for this alignment (e.g., lawful-good).' }) @prop({ required: true, index: true, type: () => String }) public index!: string @Field(() => String, { description: 'The name of the alignment (e.g., Lawful Good, Chaotic Evil).' }) @prop({ required: true, index: true, type: () => String }) public name!: string @prop({ required: true, index: true, type: () => String }) public url!: string @Field(() => String, { description: 'Timestamp of the last update.' }) @prop({ required: true, index: true, type: () => String }) public updated_at!: string } export type AlignmentDocument = DocumentType const AlignmentModel = getModelForClass(Alignment) export default AlignmentModel ================================================ FILE: src/models/2014/background.ts ================================================ import { getModelForClass, prop } from '@typegoose/typegoose' import { DocumentType } from '@typegoose/typegoose/lib/types' import { Field, Int, ObjectType } from 'type-graphql' import { APIReference } from '@/models/common/apiReference' import { Choice } from '@/models/common/choice' import { srdModelOptions } from '@/util/modelOptions' import { Equipment } from './equipment' import { Proficiency } from './proficiency' @ObjectType({ description: 'Reference to a piece of equipment with a quantity.' }) export class EquipmentRef { @Field(() => Equipment, { description: 'The specific equipment referenced.' }) @prop({ type: () => APIReference }) public equipment!: APIReference @Field(() => Int, { description: 'The quantity of the referenced equipment.' }) @prop({ required: true, index: true, type: () => Number }) public quantity!: number } @ObjectType({ description: 'A special feature granted by the background.' }) class BackgroundFeature { @Field(() => String, { description: 'The name of the background feature.' }) @prop({ required: true, index: true, type: () => String }) public name!: string @Field(() => [String], { description: 'The description of the background feature.' }) @prop({ required: true, index: true, type: () => [String] }) public desc!: string[] } @ObjectType({ description: 'Represents a character background providing flavor, proficiencies, and features.' }) @srdModelOptions('2014-backgrounds') export class Background { @Field(() => String, { description: 'The unique identifier for this background (e.g., acolyte).' }) @prop({ required: true, index: true, type: () => String }) public index!: string @Field(() => String, { description: 'The name of the background (e.g., Acolyte).' }) @prop({ required: true, index: true, type: () => String }) public name!: string @Field(() => [Proficiency], { description: 'Proficiencies granted by this background at start.' }) @prop({ type: () => [APIReference] }) public starting_proficiencies!: APIReference[] // Handled by BackgroundResolver @prop({ type: () => Choice }) public language_options!: Choice @prop({ required: true, index: true, type: () => String }) public url!: string @Field(() => [EquipmentRef], { description: 'Equipment received when choosing this background.' }) @prop({ type: () => [EquipmentRef] }) public starting_equipment!: EquipmentRef[] // Handled by BackgroundResolver @prop({ type: () => [Choice], index: true }) public starting_equipment_options!: Choice[] @Field(() => BackgroundFeature, { description: 'The feature associated with this background.' }) @prop({ type: () => BackgroundFeature }) public feature!: BackgroundFeature // Handled by BackgroundResolver @prop({ type: () => Choice }) public personality_traits!: Choice // Handled by BackgroundResolver @prop({ type: () => Choice }) public ideals!: Choice // Handled by BackgroundResolver @prop({ type: () => Choice }) public bonds!: Choice // Handled by BackgroundResolver @prop({ type: () => Choice }) public flaws!: Choice @Field(() => String, { description: 'Timestamp of the last update.' }) @prop({ required: true, index: true, type: () => String }) public updated_at!: string } export type BackgroundDocument = DocumentType const BackgroundModel = getModelForClass(Background) export default BackgroundModel ================================================ FILE: src/models/2014/class.ts ================================================ import { getModelForClass, prop } from '@typegoose/typegoose' import { DocumentType } from '@typegoose/typegoose/lib/types' import { Field, Int, ObjectType } from 'type-graphql' import { APIReference } from '@/models/common/apiReference' import { Choice } from '@/models/common/choice' import { srdModelOptions } from '@/util/modelOptions' import { AbilityScore } from './abilityScore' import { Level } from './level' import { Proficiency } from './proficiency' import { Spell } from './spell' import { Subclass } from './subclass' @ObjectType({ description: 'Starting equipment item for a class' }) export class ClassEquipment { // Handled by ClassEquipmentResolver @prop({ type: () => APIReference }) public equipment!: APIReference @Field(() => Int, { description: 'Quantity of the equipment item.' }) @prop({ required: true, index: true, type: () => Number }) public quantity!: number } @ObjectType({ description: "Information about a class's spellcasting ability" }) export class SpellcastingInfo { @Field(() => [String], { description: 'Description of the spellcasting ability.' }) @prop({ required: true, index: true, type: () => [String] }) public desc!: string[] @Field(() => String, { description: 'Name of the spellcasting ability.' }) @prop({ required: true, index: true, type: () => String }) public name!: string } @ObjectType({ description: 'Spellcasting details for a class' }) export class Spellcasting { @Field(() => [SpellcastingInfo], { description: 'Spellcasting details for the class.' }) @prop({ type: () => [SpellcastingInfo] }) public info!: SpellcastingInfo[] @Field(() => Int, { description: 'Level of the spellcasting ability.' }) @prop({ required: true, index: true, type: () => Number }) public level!: number @Field(() => AbilityScore, { description: 'Ability score used for spellcasting.' }) @prop({ type: () => APIReference }) public spellcasting_ability!: APIReference } @ObjectType({ description: 'Prerequisite for multi-classing' }) export class MultiClassingPrereq { @Field(() => AbilityScore, { nullable: true, description: 'The ability score required.' }) @prop({ type: () => APIReference }) public ability_score!: APIReference @Field(() => Int, { description: 'The minimum score required.' }) @prop({ required: true, index: true, type: () => Number }) public minimum_score!: number } @ObjectType({ description: 'Multi-classing requirements and features for a class' }) export class MultiClassing { @Field(() => [MultiClassingPrereq], { nullable: true, description: 'Ability score prerequisites for multi-classing.' }) @prop({ type: () => [MultiClassingPrereq], default: undefined }) public prerequisites?: MultiClassingPrereq[] // Handled by MultiClassingResolver @prop({ type: () => Choice, default: undefined }) public prerequisite_options?: Choice @Field(() => [Proficiency], { nullable: true, description: 'Proficiencies gained when multi-classing into this class.' }) @prop({ type: () => [APIReference], default: undefined }) public proficiencies?: APIReference[] // Handled by MultiClassingResolver @prop({ type: () => [Choice], default: undefined }) public proficiency_choices?: Choice[] } @ObjectType({ description: 'Represents a character class (e.g., Barbarian, Wizard)' }) @srdModelOptions('2014-classes') export class Class { @Field(() => [Level], { description: 'All levels for this class, detailing features and abilities gained.' }) @prop({ required: true, index: true, type: () => String }) public class_levels!: string @Field(() => MultiClassing, { nullable: true, description: 'Multi-classing requirements and features for this class.' }) @prop({ type: () => MultiClassing }) public multi_classing!: MultiClassing @Field(() => Int, { description: 'Hit die size for the class (e.g., 6, 8, 10, 12)' }) @prop({ required: true, index: true, type: () => Number }) public hit_die!: number @Field(() => String, { description: 'Unique identifier for the class' }) @prop({ required: true, index: true, type: () => String }) public index!: string @Field(() => String, { description: 'Name of the class' }) @prop({ required: true, index: true, type: () => String }) public name!: string @Field(() => [Proficiency], { nullable: true, description: 'Base proficiencies granted by this class.' }) @prop({ type: () => [APIReference] }) public proficiencies!: APIReference[] // Handled by ClassResolver @prop({ type: () => [Choice] }) public proficiency_choices!: Choice[] @Field(() => [AbilityScore], { nullable: true, description: 'Saving throw proficiencies granted by this class.' }) @prop({ type: () => [APIReference] }) public saving_throws!: APIReference[] @Field(() => Spellcasting, { nullable: true, description: 'Spellcasting details for the class.' }) @prop({ type: () => Spellcasting }) public spellcasting?: Spellcasting @Field(() => [Spell], { description: 'Spells available to this class.' }) @prop({ required: true, index: true, type: () => String }) public spells!: string @Field(() => [ClassEquipment], { nullable: true, description: 'Starting equipment for the class.' }) @prop({ type: () => [ClassEquipment] }) public starting_equipment!: ClassEquipment[] // Handled by ClassResolver @prop({ type: () => [Choice] }) public starting_equipment_options!: Choice[] @Field(() => [Subclass], { nullable: true, description: 'Available subclasses for this class.' }) @prop({ type: () => [APIReference] }) public subclasses!: APIReference[] @prop({ required: true, index: true, type: () => String }) public url!: string @Field(() => String, { description: 'Timestamp of the last update' }) @prop({ required: true, index: true, type: () => String }) public updated_at!: string } export type ClassDocument = DocumentType const ClassModel = getModelForClass(Class) export default ClassModel ================================================ FILE: src/models/2014/collection.ts ================================================ import { getModelForClass, prop } from '@typegoose/typegoose' import { DocumentType } from '@typegoose/typegoose/lib/types' import { srdModelOptions } from '@/util/modelOptions' @srdModelOptions('2014-collections') export class Collection { @prop({ required: true, index: true, type: () => String }) public index!: string } export type CollectionDocument = DocumentType const CollectionModel = getModelForClass(Collection) export default CollectionModel ================================================ FILE: src/models/2014/condition.ts ================================================ import { getModelForClass, prop } from '@typegoose/typegoose' import { DocumentType } from '@typegoose/typegoose/lib/types' import { Field, ObjectType } from 'type-graphql' import { srdModelOptions } from '@/util/modelOptions' @ObjectType({ description: 'A state that can affect a creature, such as Blinded or Prone.' }) @srdModelOptions('2014-conditions') export class Condition { @Field(() => String, { description: 'The unique identifier for this condition (e.g., blinded).' }) @prop({ required: true, index: true, type: () => String }) public index!: string @Field(() => String, { description: 'The name of the condition (e.g., Blinded).' }) @prop({ required: true, index: true, type: () => String }) public name!: string @Field(() => [String], { description: 'A description of the effects of the condition.' }) @prop({ required: true, type: () => [String] }) public desc!: string[] @prop({ required: true, index: true, type: () => String }) public url!: string @Field(() => String, { description: 'Timestamp of the last update.' }) @prop({ required: true, index: true, type: () => String }) public updated_at!: string } export type ConditionDocument = DocumentType const ConditionModel = getModelForClass(Condition) export default ConditionModel ================================================ FILE: src/models/2014/damageType.ts ================================================ import { getModelForClass, prop } from '@typegoose/typegoose' import { DocumentType } from '@typegoose/typegoose/lib/types' import { Field, ObjectType } from 'type-graphql' import { srdModelOptions } from '@/util/modelOptions' @ObjectType({ description: 'Represents a type of damage (e.g., Acid, Bludgeoning, Fire).' }) @srdModelOptions('2014-damage-types') export class DamageType { @Field(() => String, { description: 'The unique identifier for this damage type (e.g., acid).' }) @prop({ required: true, index: true, type: () => String }) public index!: string @Field(() => String, { description: 'The name of the damage type (e.g., Acid).' }) @prop({ required: true, index: true, type: () => String }) public name!: string @Field(() => [String], { description: 'A description of the damage type.' }) @prop({ required: true, type: () => [String] }) public desc!: string[] @prop({ required: true, index: true, type: () => String }) public url!: string @Field(() => String, { description: 'Timestamp of the last update.' }) @prop({ required: true, index: true, type: () => String }) public updated_at!: string } export type DamageTypeDocument = DocumentType const DamageTypeModel = getModelForClass(DamageType) export default DamageTypeModel ================================================ FILE: src/models/2014/equipment.ts ================================================ import { getModelForClass, prop } from '@typegoose/typegoose' import { DocumentType } from '@typegoose/typegoose/lib/types' import { Field, Float, Int, ObjectType } from 'type-graphql' import { IEquipment } from '@/graphql/2014/common/interfaces' import { EquipmentCategory } from '@/models/2014/equipmentCategory' import { APIReference } from '@/models/common/apiReference' import { Damage } from '@/models/common/damage' import { srdModelOptions } from '@/util/modelOptions' @ObjectType({ description: 'Details about armor class.' }) export class ArmorClass { @Field(() => Int, { description: 'Base armor class value.' }) @prop({ required: true, index: true, type: () => Number }) public base!: number @Field(() => Boolean, { description: 'Indicates if Dexterity bonus applies.' }) @prop({ required: true, index: true, type: () => Boolean }) public dex_bonus!: boolean @Field(() => Int, { nullable: true, description: 'Maximum Dexterity bonus allowed.' }) @prop({ index: true, type: () => Number }) public max_bonus?: number } @ObjectType({ description: 'An item and its quantity within a container or bundle.' }) export class Content { // Handled by ContentFieldResolver @prop({ type: () => APIReference }) public item!: APIReference @Field(() => Int, { description: 'The quantity of the item.' }) @prop({ required: true, index: true, type: () => Number }) public quantity!: number } @ObjectType({ description: 'Cost of an item in coinage.' }) export class Cost { @Field(() => Int, { description: 'The quantity of coins.' }) @prop({ required: true, index: true, type: () => Number }) public quantity!: number @Field(() => String, { description: 'The unit of coinage (e.g., gp, sp, cp).' }) @prop({ required: true, index: true, type: () => String }) public unit!: string } @ObjectType({ description: 'Range of a weapon (normal and long).' }) export class Range { @Field(() => Int, { nullable: true, description: 'The long range of the weapon.' }) @prop({ index: true, type: () => Number }) public long?: number @Field(() => Int, { description: 'The normal range of the weapon.' }) @prop({ required: true, index: true, type: () => Number }) public normal!: number } @ObjectType({ description: 'Speed of a mount or vehicle.' }) export class Speed { @Field(() => Float, { description: 'The speed quantity.' }) @prop({ required: true, index: true, type: () => Number }) public quantity!: number @Field(() => String, { description: 'The unit of speed (e.g., ft./round).' }) @prop({ required: true, index: true, type: () => String }) public unit!: string } @ObjectType({ description: 'Range for a thrown weapon.' }) export class ThrowRange { @Field(() => Int, { description: 'The long range when thrown.' }) @prop({ required: true, index: true, type: () => Number }) public long!: number @Field(() => Int, { description: 'The normal range when thrown.' }) @prop({ required: true, index: true, type: () => Number }) public normal!: number } @ObjectType({ description: 'Base Equipment class for common fields, potentially used in Unions.' }) @srdModelOptions('2014-equipment') export class Equipment implements IEquipment { // General fields @Field(() => String, { description: 'The unique identifier for this equipment.' }) @prop({ required: true, index: true, type: () => String }) public index!: string @Field(() => String, { description: 'The name of the equipment.' }) @prop({ required: true, index: true, type: () => String }) public name!: string @Field(() => [String], { nullable: true, description: 'Description of the equipment.' }) @prop({ required: true, index: true, type: () => [String] }) public desc?: string[] @Field(() => EquipmentCategory, { description: 'The category this equipment belongs to.' }) @prop({ type: () => APIReference }) public equipment_category!: APIReference @Field(() => EquipmentCategory, { nullable: true, description: 'Category if the equipment is gear.' }) @prop({ type: () => APIReference }) public gear_category?: APIReference @Field(() => Cost, { description: 'Cost of the equipment in coinage.' }) @prop({ type: () => Cost }) public cost!: Cost @Field(() => Float, { nullable: true, description: 'Weight of the equipment in pounds.' }) @prop({ index: true, type: () => Number }) public weight?: number // Specific fields @prop({ index: true, type: () => String }) public armor_category?: string @prop({ type: () => ArmorClass }) public armor_class?: ArmorClass @prop({ index: true, type: () => String }) public capacity?: string @prop({ index: true, type: () => String }) public category_range?: string @prop({ type: () => [Content] }) public contents?: Content[] @prop({ type: () => Damage }) public damage?: Damage @prop({ index: true, type: () => String }) public image?: string @prop({ type: () => [APIReference] }) public properties?: APIReference[] @prop({ index: true, type: () => Number }) public quantity?: number @prop({ type: () => Range }) public range?: Range @prop({ index: true, type: () => [String] }) public special?: string[] @prop({ type: () => Speed }) public speed?: Speed @prop({ index: true, type: () => Boolean }) public stealth_disadvantage?: boolean @prop({ index: true, type: () => Number }) public str_minimum?: number @prop({ type: () => ThrowRange }) public throw_range?: ThrowRange @prop({ index: true, type: () => String }) public tool_category?: string @prop({ type: () => Damage }) public two_handed_damage?: Damage @prop({ required: true, index: true, type: () => String }) public url!: string @prop({ index: true, type: () => String }) public vehicle_category?: string @prop({ index: true, type: () => String }) public weapon_category?: string @prop({ index: true, type: () => String }) public weapon_range?: string @Field(() => String, { description: 'Timestamp of the last update.' }) @prop({ required: true, index: true, type: () => String }) public updated_at!: string } export type EquipmentDocument = DocumentType const EquipmentModel = getModelForClass(Equipment) export default EquipmentModel ================================================ FILE: src/models/2014/equipmentCategory.ts ================================================ import { getModelForClass, prop } from '@typegoose/typegoose' import { DocumentType } from '@typegoose/typegoose/lib/types' import { Field, ObjectType } from 'type-graphql' import { APIReference } from '@/models/common/apiReference' import { srdModelOptions } from '@/util/modelOptions' @ObjectType({ description: 'A category for grouping equipment (e.g., Weapon, Armor, Adventuring Gear).' }) @srdModelOptions('2014-equipment-categories') export class EquipmentCategory { // Handled by EquipmentCategoryResolver @prop({ type: () => [APIReference], index: true }) public equipment!: APIReference[] @Field(() => String, { description: 'The unique identifier for this category (e.g., weapon).' }) @prop({ required: true, index: true, type: () => String }) public index!: string @Field(() => String, { description: 'The name of the category (e.g., Weapon).' }) @prop({ required: true, index: true, type: () => String }) public name!: string @prop({ required: true, index: true, type: () => String }) public url!: string @Field(() => String, { description: 'Timestamp of the last update.' }) @prop({ required: true, index: true, type: () => String }) public updated_at!: string } export type EquipmentCategoryDocument = DocumentType const EquipmentCategoryModel = getModelForClass(EquipmentCategory) export default EquipmentCategoryModel ================================================ FILE: src/models/2014/feat.ts ================================================ import { getModelForClass, prop } from '@typegoose/typegoose' import { DocumentType } from '@typegoose/typegoose/lib/types' import { Field, Int, ObjectType } from 'type-graphql' import { APIReference } from '@/models/common/apiReference' import { srdModelOptions } from '@/util/modelOptions' import { AbilityScore } from './abilityScore' @ObjectType({ description: 'A prerequisite for taking a feat, usually a minimum ability score.' }) export class Prerequisite { @Field(() => AbilityScore, { nullable: true, description: 'The ability score required for this prerequisite.' }) @prop({ type: () => APIReference }) public ability_score!: APIReference @Field(() => Int, { description: 'The minimum score required in the referenced ability score.' }) @prop({ required: true, index: true, type: () => Number }) public minimum_score!: number } @ObjectType({ description: 'A feat representing a special talent or expertise giving unique capabilities.' }) @srdModelOptions('2014-feats') export class Feat { @Field(() => String, { description: 'The unique identifier for this feat (e.g., grappler).' }) @prop({ required: true, index: true, type: () => String }) public index!: string @Field(() => String, { description: 'The name of the feat (e.g., Grappler).' }) @prop({ required: true, index: true, type: () => String }) public name!: string @Field(() => [Prerequisite], { description: 'Prerequisites that must be met to take the feat.' }) @prop({ type: () => [Prerequisite] }) public prerequisites!: Prerequisite[] @Field(() => [String], { description: 'A description of the benefits conferred by the feat.' }) @prop({ required: true, index: true, type: () => [String] }) public desc!: string[] @prop({ required: true, index: true, type: () => String }) public url!: string @Field(() => String, { description: 'Timestamp of the last update.' }) @prop({ required: true, index: true, type: () => String }) public updated_at!: string } export type FeatDocument = DocumentType const FeatModel = getModelForClass(Feat) export default FeatModel ================================================ FILE: src/models/2014/feature.ts ================================================ import { getModelForClass, prop } from '@typegoose/typegoose' import { DocumentType } from '@typegoose/typegoose/lib/types' import { Field, Int, ObjectType } from 'type-graphql' import { APIReference } from '@/models/common/apiReference' import { Choice } from '@/models/common/choice' import { srdModelOptions } from '@/util/modelOptions' import { Class } from './class' import { Spell } from './spell' import { Subclass } from './subclass' // Export nested classes @ObjectType({ description: 'Prerequisite based on character level' }) export class LevelPrerequisite { @Field(() => String, { description: 'Type indicator for this prerequisite.' }) @prop({ required: true, index: true, type: () => String }) public type!: string @Field(() => Int, { description: 'The character level required.' }) @prop({ required: true, index: true, type: () => Number }) public level!: number } @ObjectType({ description: 'Prerequisite based on having another feature' }) export class FeaturePrerequisite { @Field(() => String, { description: 'Type indicator for this prerequisite.' }) @prop({ required: true, index: true, type: () => String }) public type!: string @Field(() => Feature, { description: 'The specific feature required.' }) @prop({ required: true, index: true, type: () => String }) public feature!: string } @ObjectType({ description: 'Prerequisite based on knowing a specific spell' }) export class SpellPrerequisite { @Field(() => String, { description: 'Type indicator for this prerequisite.' }) @prop({ required: true, index: true, type: () => String }) public type!: string @Field(() => Spell, { description: 'The specific spell required.' }) @prop({ required: true, index: true, type: () => String }) public spell!: string } export type Prerequisite = LevelPrerequisite | FeaturePrerequisite | SpellPrerequisite @ObjectType({ description: 'Specific details related to a feature' }) export class FeatureSpecific { @prop({ type: () => Choice }) public subfeature_options?: Choice @prop({ type: () => Choice }) public expertise_options?: Choice @prop({ type: () => Choice }) public terrain_type_options?: Choice @prop({ type: () => Choice }) public enemy_type_options?: Choice @Field(() => [Feature], { nullable: true, description: 'Invocations related to this feature.' }) @prop({ type: () => [APIReference] }) public invocations?: APIReference[] } @ObjectType({ description: 'Represents a class or subclass feature.' }) @srdModelOptions('2014-features') export class Feature { @Field(() => Class, { nullable: true, description: 'The class that gains this feature.' }) @prop({ type: () => APIReference }) public class!: APIReference @Field(() => [String], { description: 'Description of the feature.' }) @prop({ required: true, index: true, type: () => [String] }) public desc!: string[] @Field(() => Feature, { nullable: true, description: 'A parent feature, if applicable.' }) @prop({ type: () => APIReference }) public parent?: APIReference @Field(() => String, { description: 'Unique identifier for this feature.' }) @prop({ required: true, index: true, type: () => String }) public index!: string @Field(() => Int, { description: 'Level at which the feature is gained.' }) @prop({ required: true, index: true, type: () => Number }) public level!: number @Field(() => String, { description: 'Name of the feature.' }) @prop({ required: true, index: true, type: () => String }) public name!: string // Handled by FeatureResolver @prop({ type: () => [Object] }) public prerequisites?: Prerequisite[] @Field(() => String, { nullable: true, description: 'Reference information (e.g., book and page number).' }) @prop({ index: true, type: () => String }) public reference?: string @Field(() => Subclass, { nullable: true, description: 'The subclass that gains this feature, if applicable.' }) @prop({ type: () => APIReference }) public subclass?: APIReference @Field(() => FeatureSpecific, { nullable: true, description: 'Specific details for this feature, if applicable.' }) @prop({ type: () => FeatureSpecific }) public feature_specific?: FeatureSpecific @prop({ required: true, index: true, type: () => String }) public url!: string @Field(() => String, { description: 'Timestamp of the last update.' }) @prop({ required: true, index: true, type: () => String }) public updated_at!: string } export type FeatureDocument = DocumentType const FeatureModel = getModelForClass(Feature) export default FeatureModel ================================================ FILE: src/models/2014/language.ts ================================================ import { getModelForClass, prop } from '@typegoose/typegoose' import { DocumentType } from '@typegoose/typegoose/lib/types' import { Field, ObjectType } from 'type-graphql' import { srdModelOptions } from '@/util/modelOptions' @ObjectType({ description: 'Represents a language spoken in the D&D world.' }) @srdModelOptions('2014-languages') export class Language { @Field(() => String, { nullable: true, description: 'A brief description of the language.' }) @prop({ index: true, type: () => String }) public desc?: string @Field(() => String, { description: 'The unique identifier for this language (e.g., common).' }) @prop({ required: true, index: true, type: () => String }) public index!: string @Field(() => String, { description: 'The name of the language (e.g., Common).' }) @prop({ required: true, index: true, type: () => String }) public name!: string @Field(() => String, { nullable: true, description: 'The script used to write the language (e.g., Common, Elvish).' }) @prop({ index: true, type: () => String }) public script?: string @Field(() => String, { description: 'The type of language (e.g., Standard, Exotic).' }) @prop({ required: true, index: true, type: () => String }) public type!: string @Field(() => [String], { description: 'Typical speakers of the language.' }) @prop({ type: () => [String], index: true }) public typical_speakers!: string[] @prop({ required: true, index: true, type: () => String }) public url!: string @Field(() => String, { description: 'Timestamp of the last update.' }) @prop({ required: true, index: true, type: () => String }) public updated_at!: string } export type LanguageDocument = DocumentType const LanguageModel = getModelForClass(Language) export default LanguageModel ================================================ FILE: src/models/2014/level.ts ================================================ import { getModelForClass, prop } from '@typegoose/typegoose' import { DocumentType } from '@typegoose/typegoose/lib/types' import { Field, Float, Int, ObjectType } from 'type-graphql' import { APIReference } from '@/models/common/apiReference' import { srdModelOptions } from '@/util/modelOptions' import { Class } from './class' import { Feature } from './feature' import { Subclass } from './subclass' // Export nested classes @ObjectType({ description: 'Spell slot creation details for Sorcerer levels' }) export class ClassSpecificCreatingSpellSlot { @Field(() => Int, { description: 'Cost in sorcery points.' }) @prop({ required: true, index: true, type: () => Number }) public sorcery_point_cost!: number @Field(() => Int, { description: 'Level of the spell slot created.' }) @prop({ required: true, index: true, type: () => Number }) public spell_slot_level!: number } @ObjectType({ description: 'Martial arts details for Monk levels' }) export class ClassSpecificMartialArt { @Field(() => Int, { description: 'Number of dice for martial arts damage.' }) @prop({ required: true, index: true, type: () => Number }) public dice_count!: number @Field(() => Int, { description: 'Value of the dice used (e.g., 4 for d4).' }) @prop({ required: true, index: true, type: () => Number }) public dice_value!: number } @ObjectType({ description: 'Sneak attack details for Rogue levels' }) export class ClassSpecificSneakAttack { @Field(() => Int, { description: 'Number of dice for sneak attack damage.' }) @prop({ required: true, index: true, type: () => Number }) public dice_count!: number @Field(() => Int, { description: 'Value of the dice used (e.g., 6 for d6).' }) @prop({ required: true, index: true, type: () => Number }) public dice_value!: number } @ObjectType({ description: 'Class-specific features and values gained at a level' }) export class ClassSpecific { @Field(() => Int, { nullable: true, description: 'Number of Action Surges available.' }) @prop({ index: true, type: () => Number }) public action_surges?: number @Field(() => Int, { nullable: true, description: 'Maximum spell level recoverable via Arcane Recovery.' }) @prop({ index: true, type: () => Number }) public arcane_recovery_levels?: number @Field(() => Int, { nullable: true, description: 'Range of Paladin auras in feet.' }) @prop({ index: true, type: () => Number }) public aura_range?: number @Field(() => Int, { nullable: true, description: 'Die size for Bardic Inspiration (e.g., 6 for d6).' }) @prop({ index: true, type: () => Number }) public bardic_inspiration_die?: number @Field(() => Int, { nullable: true, description: "Number of extra damage dice for Barbarian's Brutal Critical." }) @prop({ index: true, type: () => Number }) public brutal_critical_dice?: number @Field(() => Int, { nullable: true, description: 'Number of uses for Channel Divinity.' }) @prop({ index: true, type: () => Number }) public channel_divinity_charges?: number @Field(() => [ClassSpecificCreatingSpellSlot], { nullable: true, description: 'Sorcerer spell slot creation options.' }) @prop({ type: () => [ClassSpecificCreatingSpellSlot], default: undefined }) public creating_spell_slots?: ClassSpecificCreatingSpellSlot[] @Field(() => Float, { nullable: true, description: 'Maximum Challenge Rating of undead that can be destroyed by Channel Divinity.' }) @prop({ index: true, type: () => Number }) public destroy_undead_cr?: number @Field(() => Int, { nullable: true, description: 'Number of extra attacks granted.' }) @prop({ index: true, type: () => Number }) public extra_attacks?: number @Field(() => Int, { nullable: true, description: 'Number of favored enemies known by Ranger.' }) @prop({ index: true, type: () => Number }) public favored_enemies?: number @Field(() => Int, { nullable: true, description: 'Number of favored terrains known by Ranger.' }) @prop({ index: true, type: () => Number }) public favored_terrain?: number @Field(() => Int, { nullable: true, description: "Number of uses for Fighter's Indomitable feature." }) @prop({ index: true, type: () => Number }) public indomitable_uses?: number @Field(() => Int, { nullable: true, description: 'Number of Warlock invocations known.' }) @prop({ index: true, type: () => Number }) public invocations_known?: number @Field(() => Int, { nullable: true, description: 'Number of Monk ki points.' }) @prop({ index: true, type: () => Number }) public ki_points?: number @Field(() => Int, { nullable: true, description: "Maximum level of spells gained via Bard's Magical Secrets (up to level 5)." }) @prop({ index: true, type: () => Number }) public magical_secrets_max_5?: number @Field(() => Int, { nullable: true, description: "Maximum level of spells gained via Bard's Magical Secrets (up to level 7)." }) @prop({ index: true, type: () => Number }) public magical_secrets_max_7?: number @Field(() => Int, { nullable: true, description: "Maximum level of spells gained via Bard's Magical Secrets (up to level 9)." }) @prop({ index: true, type: () => Number }) public magical_secrets_max_9?: number @Field(() => ClassSpecificMartialArt, { nullable: true, description: 'Monk martial arts damage progression.' }) @prop({ type: () => ClassSpecificMartialArt }) public martial_arts?: ClassSpecificMartialArt @Field(() => Int, { nullable: true, description: 'Number of Sorcerer metamagic options known.' }) @prop({ index: true, type: () => Number }) public metamagic_known?: number @Field(() => Int, { nullable: true, description: 'Indicates if Warlock gained level 6 Mystic Arcanum (1 = yes).' }) @prop({ index: true, type: () => Number }) public mystic_arcanum_level_6?: number @Field(() => Int, { nullable: true, description: 'Indicates if Warlock gained level 7 Mystic Arcanum (1 = yes).' }) @prop({ index: true, type: () => Number }) public mystic_arcanum_level_7?: number @Field(() => Int, { nullable: true, description: 'Indicates if Warlock gained level 8 Mystic Arcanum (1 = yes).' }) @prop({ index: true, type: () => Number }) public mystic_arcanum_level_8?: number @Field(() => Int, { nullable: true, description: 'Indicates if Warlock gained level 9 Mystic Arcanum (1 = yes).' }) @prop({ index: true, type: () => Number }) public mystic_arcanum_level_9?: number @Field(() => Int, { nullable: true, description: 'Number of Barbarian rages per long rest.' }) @prop({ index: true, type: () => Number }) public rage_count?: number @Field(() => Int, { nullable: true, description: 'Damage bonus added to Barbarian rage attacks.' }) @prop({ index: true, type: () => Number }) public rage_damage_bonus?: number @Field(() => ClassSpecificSneakAttack, { nullable: true, description: 'Rogue sneak attack damage progression.' }) @prop({ type: () => ClassSpecificSneakAttack }) public sneak_attack?: ClassSpecificSneakAttack @Field(() => Int, { nullable: true, description: "Die size for Bard's Song of Rest (e.g., 6 for d6)." }) @prop({ index: true, type: () => Number }) public song_of_rest_die?: number @Field(() => Int, { nullable: true, description: 'Number of Sorcerer sorcery points.' }) @prop({ index: true, type: () => Number }) public sorcery_points?: number @Field(() => Int, { nullable: true, description: "Bonus speed for Monk's Unarmored Movement in feet." }) @prop({ index: true, type: () => Number }) public unarmored_movement?: number @Field(() => Boolean, { nullable: true, description: "Indicates if Druid's Wild Shape allows flying." }) @prop({ index: true, type: () => Boolean }) public wild_shape_fly?: boolean @Field(() => Float, { nullable: true, description: "Maximum Challenge Rating for Druid's Wild Shape form." }) @prop({ index: true, type: () => Number }) public wild_shape_max_cr?: number @Field(() => Boolean, { nullable: true, description: "Indicates if Druid's Wild Shape allows swimming." }) @prop({ index: true, type: () => Boolean }) public wild_shape_swim?: boolean } @ObjectType({ description: 'Spellcasting details for a class at a specific level' }) export class LevelSpellcasting { @Field(() => Int, { nullable: true, description: 'Number of cantrips known.' }) @prop({ index: true, type: () => Number }) public cantrips_known?: number @Field(() => Int, { description: 'Number of level 1 spell slots.' }) @prop({ required: true, index: true, type: () => Number }) public spell_slots_level_1!: number @Field(() => Int, { description: 'Number of level 2 spell slots.' }) @prop({ required: true, index: true, type: () => Number }) public spell_slots_level_2!: number @Field(() => Int, { description: 'Number of level 3 spell slots.' }) @prop({ required: true, index: true, type: () => Number }) public spell_slots_level_3!: number @Field(() => Int, { description: 'Number of level 4 spell slots.' }) @prop({ required: true, index: true, type: () => Number }) public spell_slots_level_4!: number @Field(() => Int, { description: 'Number of level 5 spell slots.' }) @prop({ required: true, index: true, type: () => Number }) public spell_slots_level_5!: number @Field(() => Int, { nullable: true, description: 'Number of level 6 spell slots.' }) @prop({ index: true, type: () => Number }) public spell_slots_level_6?: number @Field(() => Int, { nullable: true, description: 'Number of level 7 spell slots.' }) @prop({ index: true, type: () => Number }) public spell_slots_level_7?: number @Field(() => Int, { nullable: true, description: 'Number of level 8 spell slots.' }) @prop({ index: true, type: () => Number }) public spell_slots_level_8?: number @Field(() => Int, { nullable: true, description: 'Number of level 9 spell slots.' }) @prop({ index: true, type: () => Number }) public spell_slots_level_9?: number @Field(() => Int, { nullable: true, description: 'Total number of spells known (for certain classes like Sorcerer).' }) @prop({ index: true, type: () => Number }) public spells_known?: number } @ObjectType({ description: 'Subclass-specific features and values gained at a level' }) export class SubclassSpecific { @Field(() => Int, { nullable: true, description: "Maximum level of spells gained via Bard's Additional Magical Secrets." }) @prop({ index: true, type: () => Number }) public additional_magical_secrets_max_lvl?: number @Field(() => Int, { nullable: true, description: 'Range of subclass-specific auras (e.g., Paladin) in feet.' }) @prop({ index: true, type: () => Number }) public aura_range?: number } @ObjectType({ description: 'Represents the features and abilities gained at a specific class level' }) @srdModelOptions('2014-levels') export class Level { @Field(() => Int, { nullable: true, description: 'Number of ability score bonuses gained at this level' }) @prop({ index: true, type: () => Number }) public ability_score_bonuses?: number @Field(() => Class, { nullable: true, description: 'The class this level belongs to.' }) @prop({ type: () => APIReference }) public class!: APIReference @Field(() => ClassSpecific, { nullable: true, description: 'Class-specific details for this level.' }) @prop({ type: () => ClassSpecific }) public class_specific?: ClassSpecific @Field(() => [Feature], { nullable: true, description: 'Features gained at this level.' }) @prop({ type: () => [APIReference] }) public features?: APIReference[] @Field(() => String, { description: 'Unique identifier for this level (e.g., barbarian-1, rogue-20)' }) @prop({ required: true, index: true, type: () => String }) public index!: string @Field(() => Int, { description: 'The class level (1-20)' }) @prop({ required: true, index: true, type: () => Number }) public level!: number @Field(() => Int, { nullable: true, description: 'Proficiency bonus gained at this level' }) @prop({ index: true, type: () => Number }) public prof_bonus?: number @Field(() => LevelSpellcasting, { nullable: true, description: 'Spellcasting progression details for this level.' }) @prop({ type: () => LevelSpellcasting }) public spellcasting?: LevelSpellcasting @Field(() => Subclass, { nullable: true, description: 'The subclass this level relates to, if applicable.' }) @prop({ type: () => APIReference }) public subclass?: APIReference @Field(() => SubclassSpecific, { nullable: true, description: 'Subclass-specific details for this level.' }) @prop({ type: () => SubclassSpecific }) public subclass_specific?: SubclassSpecific // url field is not exposed via GraphQL @prop({ required: true, index: true, type: () => String }) public url!: string @Field(() => String, { description: 'Timestamp of the last update' }) @prop({ required: true, index: true, type: () => String }) public updated_at!: string } export type LevelDocument = DocumentType const LevelModel = getModelForClass(Level) export default LevelModel ================================================ FILE: src/models/2014/magicItem.ts ================================================ import { getModelForClass, prop } from '@typegoose/typegoose' import { DocumentType } from '@typegoose/typegoose/lib/types' import { Field, ObjectType } from 'type-graphql' import { APIReference } from '@/models/common/apiReference' import { srdModelOptions } from '@/util/modelOptions' import { EquipmentCategory } from './equipmentCategory' @ObjectType({ description: 'Rarity level of a magic item.' }) export class Rarity { @Field(() => String, { description: 'The name of the rarity level (e.g., Common, Uncommon, Rare).' }) @prop({ required: true, index: true, type: () => String }) public name!: string } @ObjectType({ description: 'An item imbued with magical properties.' }) @srdModelOptions('2014-magic-items') export class MagicItem { @Field(() => [String], { description: 'A description of the magic item, including its effects and usage.' }) @prop({ type: () => [String], index: true }) public desc!: string[] @Field(() => EquipmentCategory, { description: 'The category of equipment this magic item belongs to.' }) @prop({ type: () => APIReference, index: true }) public equipment_category!: APIReference @Field(() => String, { nullable: true, description: 'URL of an image for the magic item, if available.' }) @prop({ type: () => String, index: true }) public image?: string @Field(() => String, { description: 'The unique identifier for this magic item (e.g., adamantite-armor).' }) @prop({ required: true, index: true, type: () => String }) public index!: string @Field(() => String, { description: 'The name of the magic item (e.g., Adamantite Armor).' }) @prop({ required: true, index: true, type: () => String }) public name!: string @Field(() => Rarity, { description: 'The rarity of the magic item.' }) @prop({ required: true, index: true, type: () => Rarity }) public rarity!: Rarity @prop({ required: true, index: true, type: () => String }) public url!: string @Field(() => [MagicItem], { nullable: true, description: 'Other magic items that are variants of this item.' }) @prop({ type: () => [APIReference], index: true }) public variants!: APIReference[] @Field(() => Boolean, { description: 'Indicates if this magic item is a variant of another item.' }) @prop({ required: true, index: true, type: () => Boolean }) public variant!: boolean @Field(() => String, { description: 'Timestamp of the last update.' }) @prop({ required: true, index: true, type: () => String }) public updated_at!: string } export type MagicItemDocument = DocumentType const MagicItemModel = getModelForClass(MagicItem) export default MagicItemModel ================================================ FILE: src/models/2014/magicSchool.ts ================================================ import { getModelForClass, prop } from '@typegoose/typegoose' import { DocumentType } from '@typegoose/typegoose/lib/types' import { Field, ObjectType } from 'type-graphql' import { srdModelOptions } from '@/util/modelOptions' @ObjectType({ description: 'A school of magic, representing a particular tradition like Evocation or Illusion.' }) @srdModelOptions('2014-magic-schools') export class MagicSchool { @Field(() => String, { description: 'A brief description of the school of magic.' }) @prop({ type: () => String, index: true }) public desc!: string @Field(() => String, { description: 'The unique identifier for this school (e.g., evocation).' }) @prop({ required: true, index: true, type: () => String }) public index!: string @Field(() => String, { description: 'The name of the school (e.g., Evocation).' }) @prop({ required: true, index: true, type: () => String }) public name!: string @prop({ required: true, index: true, type: () => String }) public url!: string @Field(() => String, { description: 'Timestamp of the last update.' }) @prop({ required: true, index: true, type: () => String }) public updated_at!: string } export type MagicSchoolDocument = DocumentType const MagicSchoolModel = getModelForClass(MagicSchool) export default MagicSchoolModel ================================================ FILE: src/models/2014/monster.ts ================================================ import { getModelForClass, modelOptions, prop, Severity } from '@typegoose/typegoose' import { DocumentType } from '@typegoose/typegoose/lib/types' import { Field, Float, Int, ObjectType } from 'type-graphql' import { APIReference } from '@/models/common/apiReference' import { Choice } from '@/models/common/choice' import { Damage } from '@/models/common/damage' import { DifficultyClass } from '@/models/common/difficultyClass' import { srdModelOptions } from '@/util/modelOptions' import { AbilityScore } from './abilityScore' import { Condition } from './condition' import { Proficiency } from './proficiency' import { Spell } from './spell' // Export all nested classes/types @ObjectType({ description: 'Option within a monster action' }) export class ActionOption { @Field(() => String, { description: 'The name of the action.' }) @prop({ required: true, index: true, type: () => String }) public action_name!: string @Field(() => String, { description: 'Number of times the action can be used.' }) @prop({ required: true, index: true, type: () => String }) public count!: number | string @Field(() => String, { description: 'The type of action.' }) @prop({ required: true, index: true, type: () => String }) public type!: 'melee' | 'ranged' | 'ability' | 'magic' } @ObjectType({ description: 'Usage details for a monster action or ability' }) export class ActionUsage { @Field(() => String, { description: 'The type of action usage.' }) @prop({ required: true, index: true, type: () => String }) public type!: string @Field(() => String, { nullable: true, description: 'The dice roll for the action usage.' }) @prop({ index: true, type: () => String }) public dice?: string @Field(() => Int, { nullable: true, description: 'The minimum value for the action usage.' }) @prop({ index: true, type: () => Number }) public min_value?: number } @ObjectType({ description: 'An action a monster can perform' }) export class MonsterAction { @Field(() => String, { description: 'The name of the action.' }) @prop({ required: true, index: true, type: () => String }) public name!: string @Field(() => String, { description: 'The description of the action.' }) @prop({ required: true, index: true, type: () => String }) public desc!: string @Field(() => Int, { nullable: true, description: 'The attack bonus for the action.' }) @prop({ index: true, type: () => Number }) public attack_bonus?: number // Handled by MonsterActionResolver @prop({ type: () => [Object] }) public damage?: (Damage | Choice)[] @Field(() => DifficultyClass, { nullable: true, description: 'The difficulty class for the action.' }) @prop({ type: () => DifficultyClass }) public dc?: DifficultyClass // Handled by MonsterActionResolver @prop({ type: () => Choice }) public options?: Choice @Field(() => ActionUsage, { nullable: true, description: 'The usage for the action.' }) @prop({ type: () => ActionUsage }) public usage?: ActionUsage @Field(() => String, { nullable: true, description: 'The type of multiattack for the action.' }) @prop({ required: true, index: true, type: () => String }) public multiattack_type?: 'actions' | 'action_options' @Field(() => [ActionOption], { nullable: true, description: 'The actions for the action.' }) @prop({ type: () => [ActionOption] }) public actions?: ActionOption[] // Handled by MonsterActionResolver @prop({ type: () => Choice }) public action_options?: Choice } @ObjectType({ description: 'Monster Armor Class component: Dexterity based' }) export class ArmorClassDex { @Field(() => String, { description: "Type of AC component: 'dex'" }) @prop({ required: true, index: true, type: () => String }) public type!: 'dex' @Field(() => Int, { description: 'AC value from dexterity.' }) @prop({ required: true, index: true, type: () => Number }) public value!: number @Field(() => String, { nullable: true, description: 'Optional description for this AC component.' }) @prop({ index: true, type: () => String }) public desc?: string } @ObjectType({ description: 'Monster Armor Class component: Natural armor' }) export class ArmorClassNatural { @Field(() => String, { description: "Type of AC component: 'natural'" }) @prop({ required: true, index: true, type: () => String }) public type!: 'natural' @Field(() => Int, { description: 'AC value from natural armor.' }) @prop({ required: true, index: true, type: () => Number }) public value!: number @Field(() => String, { nullable: true, description: 'Optional description for this AC component.' }) @prop({ index: true, type: () => String }) public desc?: string } @ObjectType({ description: 'Monster Armor Class component: Armor worn' }) export class ArmorClassArmor { @Field(() => String, { description: "Type of AC component: 'armor'" }) @prop({ required: true, index: true, type: () => String }) public type!: 'armor' @Field(() => Int, { description: 'AC value from worn armor.' }) @prop({ required: true, index: true, type: () => Number }) public value!: number // Handled by MonsterArmorClassResolver @prop({ type: () => [APIReference] }) public armor?: APIReference[] @Field(() => String, { nullable: true, description: 'Optional description for this AC component.' }) @prop({ index: true, type: () => String }) public desc?: string } @ObjectType({ description: 'Monster Armor Class component: Spell effect' }) export class ArmorClassSpell { @Field(() => String, { description: "Type of AC component: 'spell'" }) @prop({ required: true, index: true, type: () => String }) public type!: 'spell' @Field(() => Int, { description: 'AC value from spell effect.' }) @prop({ required: true, index: true, type: () => Number }) public value!: number @Field(() => Spell, { description: 'The spell providing the AC bonus. Resolved via resolver.' }) @prop({ type: () => APIReference }) public spell!: APIReference @Field(() => String, { nullable: true, description: 'Optional description for this AC component.' }) @prop({ index: true, type: () => String }) public desc?: string } @ObjectType({ description: 'Monster Armor Class component: Condition effect' }) export class ArmorClassCondition { @Field(() => String, { description: "Type of AC component: 'condition'" }) @prop({ required: true, index: true, type: () => String }) public type!: 'condition' @Field(() => Int, { description: 'AC value from condition effect.' }) @prop({ required: true, index: true, type: () => Number }) public value!: number @Field(() => Condition, { description: 'The condition providing the AC bonus. Resolved via resolver.' }) @prop({ type: () => APIReference }) public condition!: APIReference @Field(() => String, { nullable: true, description: 'Optional description for this AC component.' }) @prop({ index: true, type: () => String }) public desc?: string } @ObjectType({ description: 'A legendary action a monster can perform' }) export class LegendaryAction { @Field(() => String, { description: 'The name of the legendary action.' }) @prop({ required: true, index: true, type: () => String }) public name!: string @Field(() => String, { description: 'The description of the legendary action.' }) @prop({ required: true, index: true, type: () => String }) public desc!: string @Field(() => Int, { nullable: true, description: 'The attack bonus for the legendary action.' }) @prop({ index: true, type: () => Number }) public attack_bonus?: number @Field(() => [Damage], { nullable: true, description: 'The damage for the legendary action.' }) @prop({ type: () => [Damage] }) public damage?: Damage[] @Field(() => DifficultyClass, { nullable: true, description: 'The difficulty class for the legendary action.' }) @prop({ type: () => DifficultyClass }) public dc?: DifficultyClass } @ObjectType({ description: "A monster's specific proficiency and its bonus value." }) export class MonsterProficiency { @Field(() => Proficiency, { description: 'The specific proficiency (e.g., Saving Throw: STR, Skill: Athletics).' }) @prop({ type: () => APIReference }) public proficiency!: APIReference @Field(() => Int, { description: 'The proficiency bonus value for this monster.' }) @prop({ required: true, index: true, type: () => Number }) public value!: number } @ObjectType({ description: 'A reaction a monster can perform' }) export class Reaction { @Field(() => String, { description: 'The name of the reaction.' }) @prop({ required: true, index: true, type: () => String }) public name!: string @Field(() => String, { description: 'The description of the reaction.' }) @prop({ required: true, index: true, type: () => String }) public desc!: string @Field(() => DifficultyClass, { nullable: true, description: 'The difficulty class for the reaction.' }) @prop({ type: () => DifficultyClass }) public dc?: DifficultyClass } @ObjectType({ description: 'Monster senses details' }) export class Sense { @Field(() => String, { nullable: true }) @prop({ index: true, type: () => String }) public blindsight?: string @Field(() => String, { nullable: true }) @prop({ index: true, type: () => String }) public darkvision?: string @Field(() => Int) @prop({ required: true, index: true, type: () => Number }) public passive_perception!: number @Field(() => String, { nullable: true }) @prop({ index: true, type: () => String }) public tremorsense?: string @Field(() => String, { nullable: true }) @prop({ index: true, type: () => String }) public truesight?: string } @ObjectType({ description: 'Usage details for a special ability' }) export class SpecialAbilityUsage { @Field(() => String, { description: 'The type of usage for the special ability.' }) @prop({ required: true, index: true, type: () => String }) public type!: string @Field(() => Int, { nullable: true, description: 'The number of times the special ability can be used.' }) @prop({ index: true, type: () => Number }) public times?: number @Field(() => [String], { nullable: true, description: 'The types of rest the special ability can be used on.' }) @prop({ type: () => [String] }) public rest_types?: string[] } @ObjectType({ description: "A spell within a monster's special ability spellcasting" }) export class SpecialAbilitySpell { @prop({ required: true, index: true, type: () => String }) public name!: string @Field(() => Int, { description: 'The level of the spell.' }) @prop({ required: true, index: true, type: () => Number }) public level!: number @prop({ required: true, index: true, type: () => String }) public url!: string @Field(() => String, { nullable: true, description: 'The notes for the spell.' }) @prop({ index: true, type: () => String }) public notes?: string @Field(() => SpecialAbilityUsage, { nullable: true, description: 'The usage for the spell.' }) @prop({ type: () => SpecialAbilityUsage }) public usage?: SpecialAbilityUsage } @ObjectType({ description: 'Spellcasting details for a monster special ability' }) @modelOptions({ options: { allowMixed: Severity.ALLOW } }) export class SpecialAbilitySpellcasting { @Field(() => Int, { nullable: true, description: 'The level of the spellcasting.' }) @prop({ index: true, type: () => Number }) public level?: number @Field(() => AbilityScore, { description: 'The ability for the spellcasting.' }) @prop({ type: () => APIReference }) public ability!: APIReference @Field(() => Int, { nullable: true, description: 'The difficulty class for the spellcasting.' }) @prop({ index: true, type: () => Number }) public dc?: number @Field(() => Int, { nullable: true, description: 'The modifier for the spellcasting.' }) @prop({ index: true, type: () => Number }) public modifier?: number @Field(() => [String], { description: 'The components required for the spellcasting.' }) @prop({ type: () => [String] }) public components_required!: string[] @Field(() => String, { nullable: true, description: 'The school of the spellcasting.' }) @prop({ index: true, type: () => String }) public school?: string // Handled by MonsterSpellcastingResolver @prop({ type: () => Object, default: undefined }) public slots?: Record @Field(() => [SpecialAbilitySpell], { description: 'The spells for the spellcasting.' }) @prop({ type: () => [SpecialAbilitySpell] }) public spells!: SpecialAbilitySpell[] } @ObjectType({ description: 'A special ability of the monster' }) export class SpecialAbility { @Field(() => String, { description: 'The name of the special ability.' }) @prop({ required: true, index: true, type: () => String }) public name!: string @Field(() => String, { description: 'The description of the special ability.' }) @prop({ required: true, index: true, type: () => String }) public desc!: string @Field(() => Int, { nullable: true, description: 'The attack bonus for the special ability.' }) @prop({ index: true, type: () => Number }) public attack_bonus?: number @Field(() => [Damage], { nullable: true, description: 'The damage for the special ability.' }) @prop({ type: () => [Damage] }) public damage?: Damage[] @Field(() => DifficultyClass, { nullable: true, description: 'The difficulty class for the special ability.' }) @prop({ type: () => DifficultyClass }) public dc?: DifficultyClass @Field(() => SpecialAbilitySpellcasting, { nullable: true, description: 'The spellcasting for the special ability.' }) @prop({ type: () => SpecialAbilitySpellcasting }) public spellcasting?: SpecialAbilitySpellcasting @Field(() => SpecialAbilityUsage, { nullable: true, description: 'The usage for the special ability.' }) @prop({ type: () => SpecialAbilityUsage }) public usage?: SpecialAbilityUsage } @ObjectType({ description: 'Monster movement speeds' }) export class MonsterSpeed { @Field(() => String, { nullable: true }) @prop({ index: true, type: () => String }) public burrow?: string @Field(() => String, { nullable: true }) @prop({ index: true, type: () => String }) public climb?: string @Field(() => String, { nullable: true }) @prop({ index: true, type: () => String }) public fly?: string @Field(() => Boolean, { nullable: true }) @prop({ index: true, type: () => Boolean }) public hover?: boolean @Field(() => String, { nullable: true }) @prop({ index: true, type: () => String }) public swim?: string @Field(() => String, { nullable: true }) @prop({ index: true, type: () => String }) public walk?: string } @ObjectType({ description: 'A D&D monster.' }) @srdModelOptions('2014-monsters') export class Monster { @Field(() => [MonsterAction], { nullable: true, description: 'The actions for the monster.' }) @prop({ type: () => [MonsterAction] }) public actions?: MonsterAction[] @Field(() => String) @prop({ required: true, index: true, type: () => String }) public alignment!: string // Handled by MonsterArmorClassResolver @prop({ type: () => Array< ArmorClassDex | ArmorClassNatural | ArmorClassArmor | ArmorClassSpell | ArmorClassCondition >, required: true }) public armor_class!: Array< ArmorClassDex | ArmorClassNatural | ArmorClassArmor | ArmorClassSpell | ArmorClassCondition > @Field(() => Float) @prop({ required: true, index: true, type: () => Number }) public challenge_rating!: number @Field(() => Int) @prop({ required: true, index: true, type: () => Number }) public charisma!: number @Field(() => [Condition], { nullable: true, description: 'Conditions the monster is immune to.' }) @prop({ type: () => [APIReference] }) public condition_immunities!: APIReference[] @Field(() => Int) @prop({ required: true, index: true, type: () => Number }) public constitution!: number @Field(() => [String]) @prop({ type: () => [String] }) public damage_immunities!: string[] @Field(() => [String]) @prop({ type: () => [String] }) public damage_resistances!: string[] @Field(() => [String]) @prop({ type: () => [String] }) public damage_vulnerabilities!: string[] @Field(() => Int) @prop({ required: true, index: true, type: () => Number }) public dexterity!: number @Field(() => [Monster], { nullable: true, description: 'Other forms the monster can assume.' }) @prop({ type: () => [APIReference] }) public forms?: APIReference[] @Field(() => String) @prop({ required: true, index: true, type: () => String }) public hit_dice!: string @Field(() => Int) @prop({ required: true, index: true, type: () => Number }) public hit_points!: number @Field(() => String) @prop({ required: true, index: true, type: () => String }) public hit_points_roll!: string @Field(() => String, { nullable: true }) @prop({ index: true, type: () => String }) public image?: string @Field(() => String) @prop({ required: true, index: true, type: () => String }) public index!: string @Field(() => Int) @prop({ required: true, index: true, type: () => Number }) public intelligence!: number @Field(() => String) @prop({ required: true, index: true, type: () => String }) public languages!: string @Field(() => [LegendaryAction], { nullable: true, description: 'The legendary actions for the monster.' }) @prop({ type: () => [LegendaryAction] }) public legendary_actions?: LegendaryAction[] @Field(() => String) @prop({ required: true, index: true, type: () => String }) public name!: string @Field(() => [MonsterProficiency], { nullable: true, description: 'The proficiencies for the monster.' }) @prop({ type: () => [MonsterProficiency] }) public proficiencies!: MonsterProficiency[] @Field(() => [Reaction], { nullable: true, description: 'The reactions for the monster.' }) @prop({ type: () => [Reaction] }) public reactions?: Reaction[] @Field(() => Sense) @prop({ type: () => Sense }) public senses!: Sense @Field(() => String) @prop({ required: true, index: true, type: () => String }) public size!: string @Field(() => [SpecialAbility], { nullable: true, description: 'The special abilities for the monster.' }) @prop({ type: () => [SpecialAbility] }) public special_abilities?: SpecialAbility[] @Field(() => MonsterSpeed) @prop({ type: () => MonsterSpeed }) public speed!: MonsterSpeed @Field(() => Int) @prop({ required: true, index: true, type: () => Number }) public strength!: number @Field(() => String, { nullable: true }) @prop({ index: true, type: () => String }) public subtype?: string @Field(() => String) @prop({ required: true, index: true, type: () => String }) public type!: string @prop({ required: true, index: true, type: () => String }) public url!: string @Field(() => Int) @prop({ required: true, index: true, type: () => Number }) public wisdom!: number @Field(() => Int) @prop({ required: true, index: true, type: () => Number }) public xp!: number @Field(() => String) @prop({ required: true, index: true, type: () => String }) public updated_at!: string } export type MonsterDocument = DocumentType const MonsterModel = getModelForClass(Monster) export default MonsterModel ================================================ FILE: src/models/2014/proficiency.ts ================================================ import { getModelForClass, prop } from '@typegoose/typegoose' import { DocumentType } from '@typegoose/typegoose/lib/types' import { Field, ObjectType } from 'type-graphql' import { APIReference } from '@/models/common/apiReference' import { srdModelOptions } from '@/util/modelOptions' import { Class } from './class' import { Race } from './race' @ObjectType({ description: 'Represents a skill, tool, weapon, armor, or saving throw proficiency.' }) @srdModelOptions('2014-proficiencies') export class Proficiency { @Field(() => [Class], { nullable: true, description: 'Classes that grant this proficiency.' }) @prop({ type: () => [APIReference] }) public classes?: APIReference[] @Field(() => String, { description: 'Unique identifier for this proficiency.' }) @prop({ required: true, index: true, type: () => String }) public index!: string @Field(() => String, { description: 'Name of the proficiency.' }) @prop({ required: true, index: true, type: () => String }) public name!: string @Field(() => [Race], { nullable: true, description: 'Races that grant this proficiency.' }) @prop({ type: () => [APIReference] }) public races?: APIReference[] @prop({ type: () => APIReference }) public reference!: APIReference @Field(() => String, { description: 'Category of proficiency (e.g., Armor, Weapons, Saving Throws, Skills).' }) @prop({ required: true, index: true, type: () => String }) public type!: string @prop({ required: true, index: true, type: () => String }) public url!: string @Field(() => String, { description: 'Timestamp of the last update.' }) @prop({ required: true, index: true, type: () => String }) public updated_at!: string } export type ProficiencyDocument = DocumentType const ProficiencyModel = getModelForClass(Proficiency) export default ProficiencyModel ================================================ FILE: src/models/2014/race.ts ================================================ import { getModelForClass, prop } from '@typegoose/typegoose' import { DocumentType } from '@typegoose/typegoose/lib/types' import { Field, Int, ObjectType } from 'type-graphql' import { APIReference } from '@/models/common/apiReference' import { Choice } from '@/models/common/choice' import { srdModelOptions } from '@/util/modelOptions' import { AbilityScore } from './abilityScore' import { Language } from './language' import { Subrace } from './subrace' import { Trait } from './trait' @ObjectType({ description: 'Ability score bonus provided by a race' }) export class RaceAbilityBonus { @Field(() => AbilityScore, { nullable: true, description: 'The ability score that receives the bonus.' }) @prop({ type: () => APIReference, required: true }) public ability_score!: APIReference @Field(() => Int, { description: 'The bonus value for the ability score' }) @prop({ required: true, index: true, type: () => Number }) public bonus!: number } @ObjectType({ description: 'Represents a playable race in D&D' }) @srdModelOptions('2014-races') export class Race { @Field(() => String, { description: 'The index of the race.' }) @prop({ required: true, index: true, type: () => String }) public index!: string // Handled by RaceResolver @prop({ type: () => Choice, required: false, index: true }) public ability_bonus_options?: Choice @Field(() => [RaceAbilityBonus], { description: 'Ability score bonuses granted by this race.' }) @prop({ type: () => [RaceAbilityBonus], required: true }) public ability_bonuses!: RaceAbilityBonus[] @Field(() => String, { description: 'Typical age range and lifespan for the race' }) @prop({ required: true, index: true, type: () => String }) public age!: string @Field(() => String, { description: 'Typical alignment tendencies for the race' }) @prop({ required: true, index: true, type: () => String }) public alignment!: string @Field(() => String, { description: 'Description of languages typically spoken by the race' }) @prop({ required: true, index: true, type: () => String }) public language_desc!: string // Handled by RaceResolver @prop({ type: () => Choice }) public language_options?: Choice @Field(() => [Language], { nullable: true, description: 'Languages typically spoken by this race.' }) @prop({ type: () => [APIReference], required: true }) public languages!: APIReference[] @Field(() => String, { description: 'The name of the race.' }) @prop({ required: true, index: true, type: () => String }) public name!: string @Field(() => String, { description: 'Size category (e.g., Medium, Small)' }) @prop({ required: true, index: true, type: () => String }) public size!: string @Field(() => String, { description: "Description of the race's size" }) @prop({ required: true, index: true, type: () => String }) public size_description!: string @Field(() => Int, { description: 'Base walking speed in feet' }) @prop({ required: true, index: true, type: () => Number }) public speed!: number @Field(() => [Subrace], { nullable: true, description: 'Subraces available for this race.' }) @prop({ type: () => [APIReference] }) public subraces?: APIReference[] @Field(() => [Trait], { nullable: true, description: 'Traits common to this race.' }) @prop({ type: () => [APIReference] }) public traits?: APIReference[] @prop({ required: true, index: true, type: () => String }) public url!: string @Field(() => String, { description: 'Timestamp of the last update' }) @prop({ required: true, index: true, type: () => String }) public updated_at!: string } export type RaceDocument = DocumentType const RaceModel = getModelForClass(Race) export default RaceModel ================================================ FILE: src/models/2014/rule.ts ================================================ import { getModelForClass, prop } from '@typegoose/typegoose' import { DocumentType } from '@typegoose/typegoose/lib/types' import { Field, ObjectType } from 'type-graphql' import { APIReference } from '@/models/common/apiReference' import { srdModelOptions } from '@/util/modelOptions' import { RuleSection } from './ruleSection' @ObjectType({ description: 'A specific rule from the SRD.' }) @srdModelOptions('2014-rules') export class Rule { @Field(() => String, { description: 'A description of the rule.' }) @prop({ required: true, index: true, type: () => String }) public desc!: string @Field(() => String, { description: 'The unique identifier for this rule (e.g., adventuring).' }) @prop({ required: true, index: true, type: () => String }) public index!: string @Field(() => String, { description: 'The name of the rule (e.g., Adventuring).' }) @prop({ required: true, index: true, type: () => String }) public name!: string @Field(() => [RuleSection], { description: 'Subsections clarifying or detailing this rule.' }) @prop({ type: () => [APIReference], index: true }) public subsections!: APIReference[] @prop({ required: true, index: true, type: () => String }) public url!: string @Field(() => String, { description: 'Timestamp of the last update.' }) @prop({ required: true, index: true, type: () => String }) public updated_at!: string } export type RuleDocument = DocumentType const RuleModel = getModelForClass(Rule) export default RuleModel ================================================ FILE: src/models/2014/ruleSection.ts ================================================ import { getModelForClass, prop } from '@typegoose/typegoose' import { DocumentType } from '@typegoose/typegoose/lib/types' import { Field, ObjectType } from 'type-graphql' import { srdModelOptions } from '@/util/modelOptions' @ObjectType({ description: 'Represents a named section of the SRD rules document.' }) @srdModelOptions('2014-rule-sections') export class RuleSection { @Field(() => String, { description: 'A description of the rule section.' }) @prop({ required: true, index: true, type: () => String }) public desc!: string @Field(() => String, { description: 'The unique identifier for this rule section (e.g., ability-checks).' }) @prop({ required: true, index: true, type: () => String }) public index!: string @Field(() => String, { description: 'The name of the rule section (e.g., Ability Checks).' }) @prop({ required: true, index: true, type: () => String }) public name!: string @prop({ required: true, index: true, type: () => String }) public url!: string @Field(() => String, { description: 'Timestamp of the last update.' }) @prop({ required: true, index: true, type: () => String }) public updated_at!: string } export type RuleSectionDocument = DocumentType const RuleSectionModel = getModelForClass(RuleSection) export default RuleSectionModel ================================================ FILE: src/models/2014/skill.ts ================================================ import { getModelForClass, prop } from '@typegoose/typegoose' import { DocumentType } from '@typegoose/typegoose/lib/types' import { Field, ObjectType } from 'type-graphql' import { APIReference } from '@/models/common/apiReference' import { srdModelOptions } from '@/util/modelOptions' import { AbilityScore } from './abilityScore' @ObjectType({ description: 'A skill representing proficiency in a specific task (e.g., Athletics, Stealth).' }) @srdModelOptions('2014-skills') export class Skill { @Field(() => AbilityScore, { description: 'The ability score associated with this skill.' }) @prop({ type: () => APIReference, required: true }) public ability_score!: APIReference @Field(() => [String], { description: 'A description of the skill.' }) @prop({ required: true, index: true, type: () => [String] }) public desc!: string[] @Field(() => String, { description: 'The unique identifier for this skill (e.g., athletics).' }) @prop({ required: true, index: true, type: () => String }) public index!: string @Field(() => String, { description: 'The name of the skill (e.g., Athletics).' }) @prop({ required: true, index: true, type: () => String }) public name!: string // url is intentionally not decorated with @Field @prop({ required: true, index: true, type: () => String }) public url!: string @Field(() => String, { description: 'Timestamp of the last update.' }) @prop({ required: true, index: true, type: () => String }) public updated_at!: string } export type SkillDocument = DocumentType const SkillModel = getModelForClass(Skill) export default SkillModel ================================================ FILE: src/models/2014/spell.ts ================================================ import { getModelForClass, modelOptions, prop, Severity } from '@typegoose/typegoose' import { DocumentType } from '@typegoose/typegoose/lib/types' import { Field, Int, ObjectType } from 'type-graphql' import { APIReference } from '@/models/common/apiReference' import { AreaOfEffect } from '@/models/common/areaOfEffect' import { srdModelOptions } from '@/util/modelOptions' import { AbilityScore } from './abilityScore' import { Class } from './class' import { DamageType } from './damageType' import { MagicSchool } from './magicSchool' import { Subclass } from './subclass' @ObjectType({ description: 'Details about spell damage' }) @modelOptions({ options: { allowMixed: Severity.ALLOW } }) export class SpellDamage { @Field(() => DamageType, { nullable: true, description: 'Type of damage dealt.' }) @prop({ type: () => APIReference }) public damage_type?: APIReference // Handled by SpellDamageResolver @prop({ mapProp: true, type: () => Object, default: undefined }) public damage_at_slot_level?: Record // Handled by SpellDamageResolver @prop({ mapProp: true, type: () => Object, default: undefined }) public damage_at_character_level?: Record } @ObjectType({ description: "Details about a spell's saving throw" }) export class SpellDC { @Field(() => AbilityScore, { description: 'The ability score used for the saving throw.' }) @prop({ type: () => APIReference, required: true }) public dc_type!: APIReference @Field(() => String, { description: "The result of a successful save (e.g., 'half', 'none')." }) @prop({ required: true, index: true, type: () => String }) public dc_success!: string @Field(() => String, { nullable: true, description: 'Additional description for the saving throw.' }) @prop({ index: true, type: () => String }) public desc?: string } @ObjectType({ description: 'Represents a spell in D&D' }) @srdModelOptions('2014-spells') export class Spell { @Field(() => AreaOfEffect, { nullable: true, description: 'Area of effect details, if applicable.' }) @prop({ type: () => AreaOfEffect }) public area_of_effect?: AreaOfEffect @Field(() => String, { nullable: true, description: 'Type of attack associated with the spell (e.g., Melee, Ranged)' }) @prop({ index: true, type: () => String }) public attack_type?: string @Field(() => String, { description: 'Time required to cast the spell' }) @prop({ required: true, index: true, type: () => String }) public casting_time!: string @Field(() => [Class], { nullable: true, description: 'Classes that can cast this spell.' }) @prop({ type: () => [APIReference], required: true }) public classes!: APIReference[] @Field(() => [String], { description: 'Components required for the spell (V, S, M)' }) @prop({ type: () => [String], required: true }) public components!: string[] @Field(() => Boolean, { description: 'Indicates if the spell requires concentration' }) @prop({ index: true, type: () => Boolean }) public concentration!: boolean @Field(() => SpellDamage, { nullable: true, description: 'Damage details, if applicable.' }) @prop({ type: () => SpellDamage }) public damage?: SpellDamage @Field(() => SpellDC, { nullable: true, description: 'Saving throw details, if applicable.' }) @prop({ type: () => SpellDC }) public dc?: SpellDC @Field(() => [String], { description: "Description of the spell's effects" }) @prop({ required: true, index: true, type: () => [String] }) public desc!: string[] @Field(() => String, { description: 'Duration of the spell' }) @prop({ required: true, index: true, type: () => String }) public duration!: string // Handled by SpellResolver @prop({ type: () => Object }) public heal_at_slot_level?: Record @Field(() => [String], { nullable: true, description: 'Description of effects when cast at higher levels' }) @prop({ type: () => [String] }) public higher_level?: string[] @Field(() => String, { description: 'Unique identifier for this spell' }) @prop({ required: true, index: true, type: () => String }) public index!: string @Field(() => Int, { description: 'Level of the spell (0 for cantrips)' }) @prop({ required: true, index: true, type: () => Number }) public level!: number @Field(() => String, { nullable: true, description: 'Material components required, if any' }) @prop({ index: true, type: () => String }) public material?: string @Field(() => String, { description: 'Name of the spell' }) @prop({ required: true, index: true, type: () => String }) public name!: string @Field(() => String, { description: 'Range of the spell' }) @prop({ required: true, index: true, type: () => String }) public range!: string @Field(() => Boolean, { description: 'Indicates if the spell can be cast as a ritual' }) @prop({ required: true, index: true, type: () => Boolean }) public ritual!: boolean @Field(() => MagicSchool, { nullable: true, description: 'The school of magic this spell belongs to.' }) @prop({ type: () => APIReference, required: true }) public school!: APIReference @Field(() => [Subclass], { nullable: true, description: 'Subclasses that can cast this spell.' }) @prop({ type: () => [APIReference], required: true }) public subclasses?: APIReference[] @prop({ required: true, index: true, type: () => String }) public url!: string @Field(() => String, { description: 'Timestamp of the last update' }) @prop({ required: true, index: true, type: () => String }) public updated_at!: string } export type SpellDocument = DocumentType const SpellModel = getModelForClass(Spell) export default SpellModel ================================================ FILE: src/models/2014/subclass.ts ================================================ import { getModelForClass, prop } from '@typegoose/typegoose' import { DocumentType } from '@typegoose/typegoose/lib/types' import { Field, ObjectType } from 'type-graphql' import { APIReference } from '@/models/common/apiReference' import { srdModelOptions } from '@/util/modelOptions' import { Class } from './class' import { Level } from './level' import { Spell } from './spell' @ObjectType({ description: 'Prerequisite for a subclass spell' }) export class Prerequisite { @prop({ required: true, index: true, type: () => String }) public index!: string @prop({ required: true, type: () => String }) public name!: string @prop({ required: true, type: () => String }) public type!: string @prop({ required: true, type: () => String }) public url!: string } @ObjectType({ description: 'Spell gained by a subclass' }) export class SubclassSpell { // Handled by SubclassSpellResolver @prop({ type: () => [Prerequisite], required: true }) public prerequisites!: Prerequisite[] @Field(() => Spell, { description: 'The spell gained.' }) @prop({ type: () => APIReference, required: true }) public spell!: APIReference } @ObjectType({ description: 'Represents a subclass (e.g., Path of the Berserker, School of Evocation)' }) @srdModelOptions('2014-subclasses') export class Subclass { @Field(() => Class, { nullable: true, description: 'The parent class for this subclass.' }) @prop({ type: () => APIReference, required: true }) public class!: APIReference @Field(() => [String], { description: 'Description of the subclass' }) @prop({ required: true, index: true, type: () => [String] }) public desc!: string[] @Field(() => String, { description: 'Unique identifier for the subclass' }) @prop({ required: true, index: true, type: () => String }) public index!: string @Field(() => String, { description: 'Name of the subclass' }) @prop({ required: true, index: true, type: () => String }) public name!: string @Field(() => [SubclassSpell], { nullable: true, description: 'Spells specific to this subclass.' }) @prop({ type: () => [SubclassSpell] }) public spells?: SubclassSpell[] @Field(() => String, { description: 'Flavor text describing the subclass' }) @prop({ required: true, index: true, type: () => String }) public subclass_flavor!: string @Field(() => [Level], { nullable: true, description: 'Features and abilities gained by level for this subclass.' }) @prop({ required: true, index: true, type: () => String }) public subclass_levels!: string @prop({ required: true, index: true, type: () => String }) public url!: string @Field(() => String, { description: 'Timestamp of the last update' }) @prop({ required: true, index: true, type: () => String }) public updated_at!: string } export type SubclassDocument = DocumentType const SubclassModel = getModelForClass(Subclass) export default SubclassModel ================================================ FILE: src/models/2014/subrace.ts ================================================ import { getModelForClass, prop } from '@typegoose/typegoose' import { DocumentType } from '@typegoose/typegoose/lib/types' import { Field, Int, ObjectType } from 'type-graphql' import { APIReference } from '@/models/common/apiReference' import { srdModelOptions } from '@/util/modelOptions' import { AbilityScore } from './abilityScore' import { Race } from './race' import { Trait } from './trait' @ObjectType({ description: 'Bonus to an ability score provided by a subrace.' }) export class SubraceAbilityBonus { @Field(() => AbilityScore, { nullable: true, description: 'The ability score receiving the bonus.' }) @prop({ type: () => APIReference, required: true }) public ability_score!: APIReference @Field(() => Int, { description: 'The bonus value to the ability score.' }) @prop({ required: true, index: true, type: () => Number }) public bonus!: number } @ObjectType({ description: 'A subrace representing a specific heritage within a larger race.' }) @srdModelOptions('2014-subraces') export class Subrace { @Field(() => [SubraceAbilityBonus], { description: 'Ability score bonuses granted by this subrace.' }) @prop({ type: () => [SubraceAbilityBonus], required: true }) public ability_bonuses!: SubraceAbilityBonus[] @Field(() => String, { description: 'A description of the subrace.' }) @prop({ required: true, index: true, type: () => String }) public desc!: string @Field(() => String, { description: 'The unique identifier for this subrace (e.g., high-elf).' }) @prop({ required: true, index: true, type: () => String }) public index!: string @Field(() => String, { description: 'The name of the subrace (e.g., High Elf).' }) @prop({ required: true, index: true, type: () => String }) public name!: string @Field(() => Race, { nullable: true, description: 'The parent race for this subrace.' }) @prop({ type: () => APIReference, required: true }) public race!: APIReference @Field(() => [Trait], { nullable: true, description: 'Racial traits associated with this subrace.' }) @prop({ type: () => [APIReference] }) public racial_traits!: APIReference[] @prop({ required: true, index: true, type: () => String }) public url!: string @Field(() => String, { description: 'Timestamp of the last update.' }) @prop({ required: true, index: true, type: () => String }) public updated_at!: string } export type SubraceDocument = DocumentType const SubraceModel = getModelForClass(Subrace) export default SubraceModel ================================================ FILE: src/models/2014/trait.ts ================================================ import { getModelForClass, modelOptions, prop, Severity } from '@typegoose/typegoose' import { DocumentType } from '@typegoose/typegoose/lib/types' import { Field, Int, ObjectType } from 'type-graphql' import { APIReference } from '@/models/common/apiReference' import { AreaOfEffect } from '@/models/common/areaOfEffect' import { srdModelOptions } from '@/util/modelOptions' import { AbilityScore } from './abilityScore' import { DamageType } from './damageType' import { Proficiency } from './proficiency' import { Race } from './race' import { Subrace } from './subrace' import { Choice } from '../common/choice' @ObjectType({ description: 'Damage details for an action' }) @modelOptions({ options: { allowMixed: Severity.ALLOW } }) export class ActionDamage { @Field(() => DamageType, { nullable: true, description: 'The type of damage dealt.' }) @prop({ type: () => APIReference }) public damage_type!: APIReference // Handled by ActionDamageResolver @prop({ type: () => Object }) public damage_at_character_level?: Record } @ObjectType({ description: 'Usage limit details for an action' }) export class Usage { @Field(() => String, { description: "Type of usage limit (e.g., 'per day')." }) @prop({ required: true, index: true, type: () => String }) public type!: string @Field(() => Int, { description: 'Number of times the action can be used.' }) @prop({ required: true, index: true, type: () => Number }) public times!: number } @ObjectType({ description: 'DC details for a trait action (lacks dc_value).' }) export class TraitActionDC { @Field(() => AbilityScore, { description: 'The ability score associated with this DC.' }) @prop({ type: () => APIReference, required: true }) public dc_type!: APIReference @Field(() => String, { description: 'The result of a successful save against this DC.' }) @prop({ type: () => String, required: true }) public success_type!: 'none' | 'half' | 'other' } @ObjectType({ description: 'Represents an action associated with a trait (like a breath weapon).' }) export class Action { @Field(() => String, { description: 'The name of the action.' }) @prop({ required: true, index: true, type: () => String }) public name!: string @Field(() => String, { description: 'Description of the action.' }) @prop({ required: true, index: true, type: () => String }) public desc!: string @Field(() => Usage, { nullable: true, description: 'Usage limitations for the action.' }) @prop({ type: () => Usage }) public usage?: Usage @Field(() => TraitActionDC, { nullable: true, description: 'The Difficulty Class (DC) associated with the action (value may not be applicable).' }) @prop({ type: () => TraitActionDC }) public dc?: TraitActionDC @Field(() => [ActionDamage], { nullable: true, description: 'Damage dealt by the action.' }) @prop({ type: () => [ActionDamage] }) public damage?: ActionDamage[] @Field(() => AreaOfEffect, { nullable: true, description: 'The area of effect for the action.' }) @prop({ type: () => AreaOfEffect }) public area_of_effect?: AreaOfEffect } @ObjectType({ description: 'Details specific to certain traits.' }) export class TraitSpecific { // Handled by TraitSpecificResolver @prop({ type: () => Choice }) public subtrait_options?: Choice // Handled by TraitSpecificResolver @prop({ type: () => Choice }) public spell_options?: Choice // Handled by TraitSpecificResolver @prop({ type: () => APIReference }) public damage_type?: APIReference @Field(() => Action, { nullable: true, description: 'Breath weapon action details, if applicable.' }) @prop({ type: () => Action }) public breath_weapon?: Action } @ObjectType({ description: 'A racial or subracial trait providing specific benefits or abilities.' }) @srdModelOptions('2014-traits') export class Trait { @Field(() => [String], { description: 'A description of the trait.' }) @prop({ required: true, index: true, type: () => [String] }) public desc!: string[] @Field(() => String, { description: 'The unique identifier for this trait (e.g., darkvision).' }) @prop({ required: true, index: true, type: () => String }) public index!: string @Field(() => String, { description: 'The name of the trait (e.g., Darkvision).' }) @prop({ required: true, index: true, type: () => String }) public name!: string @Field(() => [Proficiency], { nullable: true, description: 'Proficiencies granted by this trait.' }) @prop({ type: () => [APIReference] }) public proficiencies?: APIReference[] // Handled by TraitResolver @prop({ type: () => Choice }) public proficiency_choices?: Choice // Handled by TraitResolver @prop({ type: () => Choice }) public language_options?: Choice @Field(() => [Race], { nullable: true, description: 'Races that possess this trait.' }) @prop({ type: () => [APIReference], required: true }) public races!: APIReference[] @Field(() => [Subrace], { nullable: true, description: 'Subraces that possess this trait.' }) @prop({ type: () => [APIReference], required: true }) public subraces!: APIReference[] @Field(() => Trait, { nullable: true, description: 'A parent trait, if this is a sub-trait.' }) @prop({ type: () => APIReference }) public parent?: APIReference @Field(() => TraitSpecific, { nullable: true, description: 'Specific details for this trait, if applicable.' }) @prop({ type: () => TraitSpecific }) public trait_specific?: TraitSpecific @prop({ required: true, index: true, type: () => String }) public url!: string @Field(() => String, { description: 'Timestamp of the last update.' }) @prop({ required: true, index: true, type: () => String }) public updated_at!: string } export type TraitDocument = DocumentType const TraitModel = getModelForClass(Trait) export default TraitModel ================================================ FILE: src/models/2014/weaponProperty.ts ================================================ import { getModelForClass, prop } from '@typegoose/typegoose' import { DocumentType } from '@typegoose/typegoose/lib/types' import { Field, ObjectType } from 'type-graphql' import { srdModelOptions } from '@/util/modelOptions' @ObjectType({ description: 'A property that can be applied to a weapon, modifying its use or characteristics.' }) @srdModelOptions('2014-weapon-properties') export class WeaponProperty { @Field(() => [String], { description: 'A description of the weapon property.' }) @prop({ required: true, index: true, type: () => [String] }) public desc!: string[] @Field(() => String, { description: 'The unique identifier for this property (e.g., versatile).' }) @prop({ required: true, index: true, type: () => String }) public index!: string @Field(() => String, { description: 'The name of the property (e.g., Versatile).' }) @prop({ required: true, index: true, type: () => String }) public name!: string @prop({ required: true, index: true, type: () => String }) public url!: string @Field(() => String, { description: 'Timestamp of the last update.' }) @prop({ required: true, index: true, type: () => String }) public updated_at!: string } export type WeaponPropertyDocument = DocumentType const WeaponPropertyModel = getModelForClass(WeaponProperty) export default WeaponPropertyModel ================================================ FILE: src/models/2024/abilityScore.ts ================================================ import { getModelForClass, prop } from '@typegoose/typegoose' import { DocumentType } from '@typegoose/typegoose/lib/types' import { Field, ObjectType } from 'type-graphql' import { Skill2024 } from '@/models/2024/skill' import { APIReference } from '@/models/common/apiReference' import { srdModelOptions } from '@/util/modelOptions' @ObjectType({ description: 'An ability score representing a fundamental character attribute (e.g., Strength, Dexterity).' }) @srdModelOptions('2024-ability-scores') export class AbilityScore2024 { @Field(() => String, { description: 'A description of the ability score and its applications.' }) @prop({ required: true, index: true, type: () => String }) public description!: string @Field(() => String, { description: 'The full name of the ability score (e.g., Strength).' }) @prop({ required: true, index: true, type: () => String }) public full_name!: string @Field(() => String, { description: 'The unique identifier for this ability score (e.g., str).' }) @prop({ required: true, index: true, type: () => String }) public index!: string @Field(() => String, { description: 'The abbreviated name of the ability score (e.g., STR).' }) @prop({ required: true, index: true, type: () => String }) public name!: string @Field(() => [Skill2024], { description: 'Skills associated with this ability score.' }) @prop({ type: () => [APIReference] }) public skills!: APIReference[] @prop({ required: true, index: true, type: () => String }) public url!: string @Field(() => String, { description: 'Timestamp of the last update.' }) @prop({ required: true, index: true, type: () => String }) public updated_at!: string } export type AbilityScoreDocument = DocumentType const AbilityScoreModel = getModelForClass(AbilityScore2024) export default AbilityScoreModel ================================================ FILE: src/models/2024/alignment.ts ================================================ import { getModelForClass, prop } from '@typegoose/typegoose' import { DocumentType } from '@typegoose/typegoose/lib/types' import { Field, ObjectType } from 'type-graphql' import { srdModelOptions } from '@/util/modelOptions' @ObjectType({ description: "An alignment representing a character's moral and ethical beliefs." }) @srdModelOptions('2024-alignments') export class Alignment2024 { @Field(() => String, { description: 'A description of the alignment.' }) @prop({ required: true, index: true, type: () => String }) public description!: string @Field(() => String, { description: 'A shortened representation of the alignment (e.g., LG, CE).' }) @prop({ required: true, index: true, type: () => String }) public abbreviation!: string @Field(() => String, { description: 'The unique identifier for this alignment (e.g., lawful-good).' }) @prop({ required: true, index: true, type: () => String }) public index!: string @Field(() => String, { description: 'The name of the alignment (e.g., Lawful Good, Chaotic Evil).' }) @prop({ required: true, index: true, type: () => String }) public name!: string @prop({ required: true, index: true, type: () => String }) public url!: string @Field(() => String, { description: 'Timestamp of the last update.' }) @prop({ required: true, index: true, type: () => String }) public updated_at!: string } export type AlignmentDocument = DocumentType const AlignmentModel = getModelForClass(Alignment2024) export default AlignmentModel ================================================ FILE: src/models/2024/background.ts ================================================ import { getModelForClass, prop } from '@typegoose/typegoose' import { DocumentType } from '@typegoose/typegoose/lib/types' import { Field, ObjectType } from 'type-graphql' import { APIReference } from '@/models/common/apiReference' import { Choice } from '@/models/common/choice' import { srdModelOptions } from '@/util/modelOptions' @ObjectType({ description: 'A reference to a feat with an optional note.' }) export class BackgroundFeatReference { @Field(() => String) @prop({ required: true, type: () => String }) public index!: string @Field(() => String) @prop({ required: true, type: () => String }) public name!: string @Field(() => String) @prop({ required: true, type: () => String }) public url!: string @Field(() => String, { nullable: true }) @prop({ type: () => String }) public note?: string } @ObjectType({ description: 'A 2024 character background.' }) @srdModelOptions('2024-backgrounds') export class Background2024 { @Field(() => String, { description: 'The unique identifier for this background.' }) @prop({ required: true, index: true, type: () => String }) public index!: string @Field(() => String, { description: 'The name of this background.' }) @prop({ required: true, index: true, type: () => String }) public name!: string @prop({ type: () => [APIReference], required: true }) public ability_scores!: APIReference[] @prop({ type: () => BackgroundFeatReference, required: true }) public feat!: BackgroundFeatReference @prop({ type: () => [APIReference], required: true }) public proficiencies!: APIReference[] @prop({ type: () => [Choice] }) public proficiency_choices?: Choice[] @prop({ type: () => [Choice] }) public equipment_options?: Choice[] @prop({ required: true, index: true, type: () => String }) public url!: string @Field(() => String, { description: 'Timestamp of the last update.' }) @prop({ required: true, index: true, type: () => String }) public updated_at!: string } export type BackgroundDocument = DocumentType const BackgroundModel = getModelForClass(Background2024) export default BackgroundModel ================================================ FILE: src/models/2024/collection.ts ================================================ import { getModelForClass, prop } from '@typegoose/typegoose' import { DocumentType } from '@typegoose/typegoose/lib/types' import { srdModelOptions } from '@/util/modelOptions' @srdModelOptions('2024-collections') export class Collection2024 { @prop({ required: true, index: true, type: () => String }) public index!: string } export type CollectionDocument = DocumentType const CollectionModel = getModelForClass(Collection2024) export default CollectionModel ================================================ FILE: src/models/2024/condition.ts ================================================ import { getModelForClass, prop } from '@typegoose/typegoose' import { DocumentType } from '@typegoose/typegoose/lib/types' import { Field, ObjectType } from 'type-graphql' import { srdModelOptions } from '@/util/modelOptions' @ObjectType({ description: 'A state that can affect a creature, such as Blinded or Prone.' }) @srdModelOptions('2024-conditions') export class Condition2024 { @Field(() => String, { description: 'The unique identifier for this condition (e.g., blinded).' }) @prop({ required: true, index: true, type: () => String }) public index!: string @Field(() => String, { description: 'The name of the condition (e.g., Blinded).' }) @prop({ required: true, index: true, type: () => String }) public name!: string @Field(() => String, { description: 'A description of the effects of the condition.' }) @prop({ required: true, type: () => String }) public description!: string @prop({ required: true, index: true, type: () => String }) public url!: string @Field(() => String, { description: 'Timestamp of the last update.' }) @prop({ required: true, index: true, type: () => String }) public updated_at!: string } export type ConditionDocument = DocumentType const ConditionModel = getModelForClass(Condition2024) export default ConditionModel ================================================ FILE: src/models/2024/damageType.ts ================================================ import { getModelForClass, prop } from '@typegoose/typegoose' import { DocumentType } from '@typegoose/typegoose/lib/types' import { Field, ObjectType } from 'type-graphql' import { srdModelOptions } from '@/util/modelOptions' @ObjectType({ description: 'Represents a type of damage (e.g., Acid, Bludgeoning, Fire).' }) @srdModelOptions('2024-damage-types') export class DamageType2024 { @Field(() => String, { description: 'The unique identifier for this damage type (e.g., acid).' }) @prop({ required: true, index: true, type: () => String }) public index!: string @Field(() => String, { description: 'The name of the damage type (e.g., Acid).' }) @prop({ required: true, index: true, type: () => String }) public name!: string @Field(() => String, { description: 'A description of the damage type.' }) @prop({ required: true, type: () => String }) public description!: string @prop({ required: true, index: true, type: () => String }) public url!: string @Field(() => String, { description: 'Timestamp of the last update.' }) @prop({ required: true, index: true, type: () => String }) public updated_at!: string } export type DamageTypeDocument = DocumentType const DamageTypeModel = getModelForClass(DamageType2024) export default DamageTypeModel ================================================ FILE: src/models/2024/equipment.ts ================================================ import { getModelForClass, prop } from '@typegoose/typegoose' import { DocumentType } from '@typegoose/typegoose/lib/types' import { Field, Float, Int, ObjectType } from 'type-graphql' import { EquipmentCategory2024 } from '@/models/2024/equipmentCategory' import { APIReference } from '@/models/common/apiReference' import { Damage } from '@/models/common/damage' import { DifficultyClass } from '@/models/common/difficultyClass' import { srdModelOptions } from '@/util/modelOptions' @ObjectType({ description: 'Details about armor class.' }) export class ArmorClass { @Field(() => Int, { description: 'Base armor class value.' }) @prop({ required: true, index: true, type: () => Number }) public base!: number @Field(() => Boolean, { description: 'Indicates if Dexterity bonus applies.' }) @prop({ required: true, index: true, type: () => Boolean }) public dex_bonus!: boolean @Field(() => Int, { nullable: true, description: 'Maximum Dexterity bonus allowed.' }) @prop({ index: true, type: () => Number }) public max_bonus?: number } @ObjectType({ description: 'An item and its quantity within a container or bundle.' }) export class Content { // Handled by ContentFieldResolver @prop({ type: () => APIReference }) public item!: APIReference @Field(() => Int, { description: 'The quantity of the item.' }) @prop({ required: true, index: true, type: () => Number }) public quantity!: number } @ObjectType({ description: 'Cost of an item in coinage.' }) export class Cost { @Field(() => Int, { description: 'The quantity of coins.' }) @prop({ required: true, index: true, type: () => Number }) public quantity!: number @Field(() => String, { description: 'The unit of coinage (e.g., gp, sp, cp).' }) @prop({ required: true, index: true, type: () => String }) public unit!: string } @ObjectType({ description: 'Range of a weapon (normal and long).' }) export class Range { @Field(() => Int, { nullable: true, description: 'The long range of the weapon.' }) @prop({ index: true, type: () => Number }) public long?: number @Field(() => Int, { description: 'The normal range of the weapon.' }) @prop({ required: true, index: true, type: () => Number }) public normal!: number } @ObjectType({ description: 'Range for a thrown weapon.' }) export class ThrowRange { @Field(() => Int, { description: 'The long range when thrown.' }) @prop({ required: true, index: true, type: () => Number }) public long!: number @Field(() => Int, { description: 'The normal range when thrown.' }) @prop({ required: true, index: true, type: () => Number }) public normal!: number } @ObjectType({ description: 'How to utilize a tool.' }) export class Utilize { @Field(() => String, { description: 'The name of the action.' }) @prop({ required: true, index: true, type: () => String }) public name!: string @Field(() => DifficultyClass, { description: 'The DC of the action.' }) @prop({ type: () => DifficultyClass }) public dc!: DifficultyClass } @ObjectType({ description: 'Base Equipment class for common fields, potentially used in Unions.' }) @srdModelOptions('2024-equipment') export class Equipment2024 { // General fields @Field(() => String, { description: 'The unique identifier for this equipment.' }) @prop({ required: true, index: true, type: () => String }) public index!: string @Field(() => String, { description: 'The name of the equipment.' }) @prop({ required: true, index: true, type: () => String }) public name!: string @Field(() => [String], { nullable: true, description: 'Description of the equipment.' }) @prop({ required: true, index: true, type: () => [String] }) public description?: string[] @Field(() => [EquipmentCategory2024], { description: 'The categories this equipment belongs to.' }) @prop({ type: () => [APIReference] }) public equipment_categories!: APIReference[] @prop({ index: true, type: () => APIReference }) public ammunition?: APIReference @prop({ type: () => ArmorClass }) public armor_class?: ArmorClass @prop({ type: () => [Content] }) public contents?: Content[] @Field(() => Cost, { description: 'Cost of the equipment in coinage.' }) @prop({ type: () => Cost }) public cost!: Cost @Field(() => Float, { nullable: true, description: 'Weight of the equipment in pounds.' }) @prop({ index: true, type: () => Number }) public weight?: number @prop({ index: true, type: () => APIReference }) public ability?: APIReference @prop({ index: true, type: () => [APIReference] }) public craft?: APIReference[] @prop({ type: () => Damage }) public damage?: Damage @prop({ index: true, type: () => String }) public doff_time?: string @prop({ index: true, type: () => String }) public don_time?: string @prop({ index: true, type: () => String }) public image?: string @prop({ index: true, type: () => APIReference }) public mastery?: APIReference @prop({ index: true, type: () => [String] }) public notes?: string[] @prop({ type: () => [APIReference] }) public properties?: APIReference[] @prop({ index: true, type: () => Number }) public quantity?: number @prop({ type: () => Range }) public range?: Range @prop({ index: true, type: () => Boolean }) public stealth_disadvantage?: boolean @prop({ index: true, type: () => Number }) public str_minimum?: number @prop({ type: () => ThrowRange }) public throw_range?: ThrowRange @prop({ type: () => Damage }) public two_handed_damage?: Damage @prop({ index: true, type: () => [Utilize] }) public utilize?: Utilize[] @prop({ required: true, index: true, type: () => String }) public url!: string @Field(() => String, { description: 'Timestamp of the last update.' }) @prop({ required: true, index: true, type: () => String }) public updated_at!: string } export type EquipmentDocument = DocumentType const EquipmentModel = getModelForClass(Equipment2024) export default EquipmentModel ================================================ FILE: src/models/2024/equipmentCategory.ts ================================================ import { getModelForClass, prop } from '@typegoose/typegoose' import { DocumentType } from '@typegoose/typegoose/lib/types' import { Field, ObjectType } from 'type-graphql' import { APIReference } from '@/models/common/apiReference' import { srdModelOptions } from '@/util/modelOptions' @ObjectType({ description: 'A category for grouping equipment (e.g., Weapon, Armor, Adventuring Gear).' }) @srdModelOptions('2024-equipment-categories') export class EquipmentCategory2024 { // Handled by EquipmentCategoryResolver @prop({ type: () => [APIReference], index: true }) public equipment!: APIReference[] @Field(() => String, { description: 'The unique identifier for this category (e.g., weapon).' }) @prop({ required: true, index: true, type: () => String }) public index!: string @Field(() => String, { description: 'The name of the category (e.g., Weapon).' }) @prop({ required: true, index: true, type: () => String }) public name!: string @prop({ required: true, index: true, type: () => String }) public url!: string @Field(() => String, { description: 'Timestamp of the last update.' }) @prop({ required: true, index: true, type: () => String }) public updated_at!: string } export type EquipmentCategoryDocument = DocumentType const EquipmentCategoryModel = getModelForClass(EquipmentCategory2024) export default EquipmentCategoryModel ================================================ FILE: src/models/2024/feat.ts ================================================ import { getModelForClass, prop } from '@typegoose/typegoose' import { DocumentType } from '@typegoose/typegoose/lib/types' import { Field, Int, ObjectType } from 'type-graphql' import { srdModelOptions } from '@/util/modelOptions' import { Choice } from '../common/choice' @ObjectType({ description: 'Prerequisites for a 2024 feat.' }) export class FeatPrerequisites2024 { @Field(() => Int, { nullable: true, description: 'Minimum character level required.' }) @prop({ index: true, type: () => Number }) public minimum_level?: number @Field(() => String, { nullable: true, description: 'Name of a feature (e.g. Spellcasting) required.' }) @prop({ index: true, type: () => String }) public feature_named?: string } @ObjectType({ description: 'A 2024 feat.' }) @srdModelOptions('2024-feats') export class Feat2024 { @Field(() => String, { description: 'The unique identifier for this feat.' }) @prop({ required: true, index: true, type: () => String }) public index!: string @Field(() => String, { description: 'The name of this feat.' }) @prop({ required: true, index: true, type: () => String }) public name!: string @Field(() => String, { description: 'Description of the feat.' }) @prop({ required: true, type: () => String }) public description!: string @Field(() => String, { description: 'The type of feat (origin, general, fighting-style, epic-boon).' }) @prop({ required: true, index: true, type: () => String }) public type!: string @Field(() => String, { nullable: true, description: 'Repeatability note, if applicable.' }) @prop({ index: true, type: () => String }) public repeatable?: string @Field(() => FeatPrerequisites2024, { nullable: true, description: 'Static prerequisites.' }) @prop({ type: () => FeatPrerequisites2024 }) public prerequisites?: FeatPrerequisites2024 @prop({ type: () => Choice }) public prerequisite_options?: Choice @prop({ required: true, index: true, type: () => String }) public url!: string @Field(() => String, { description: 'Timestamp of the last update.' }) @prop({ required: true, index: true, type: () => String }) public updated_at!: string } export type FeatDocument = DocumentType const FeatModel = getModelForClass(Feat2024) export default FeatModel ================================================ FILE: src/models/2024/language.ts ================================================ import { getModelForClass, prop } from '@typegoose/typegoose' import { DocumentType } from '@typegoose/typegoose/lib/types' import { Field, ObjectType } from 'type-graphql' import { srdModelOptions } from '@/util/modelOptions' @ObjectType({ description: 'Represents a language spoken in the D&D world.' }) @srdModelOptions('2024-languages') export class Language2024 { @Field(() => String, { description: 'The unique identifier for this language (e.g., draconic).' }) @prop({ required: true, index: true, type: () => String }) public index!: string @Field(() => String, { description: 'The name of the language (e.g., Draconic).' }) @prop({ required: true, index: true, type: () => String }) public name!: string @Field(() => Boolean, { description: 'Whether the language is rare.' }) @prop({ required: true, index: true, type: () => Boolean }) public is_rare!: boolean @Field(() => String, { description: 'A note about the language.' }) @prop({ required: true, index: true, type: () => String }) public note!: string @prop({ required: true, index: true, type: () => String }) public url!: string @Field(() => String, { description: 'Timestamp of the last update.' }) @prop({ required: true, index: true, type: () => String }) public updated_at!: string } export type LanguageDocument = DocumentType const LanguageModel = getModelForClass(Language2024) export default LanguageModel ================================================ FILE: src/models/2024/magicItem.ts ================================================ import { getModelForClass, prop } from '@typegoose/typegoose' import { DocumentType } from '@typegoose/typegoose/lib/types' import { Field, ObjectType } from 'type-graphql' import { APIReference } from '@/models/common/apiReference' import { srdModelOptions } from '@/util/modelOptions' import { EquipmentCategory2024 } from './equipmentCategory' @ObjectType({ description: 'The rarity level of a 2024 magic item.' }) export class Rarity2024 { @Field(() => String, { description: 'The name of the rarity level (e.g., Common, Uncommon, Rare).' }) @prop({ required: true, index: true, type: () => String }) public name!: string } @ObjectType({ description: 'An item imbued with magical properties in D&D 5e 2024.' }) @srdModelOptions('2024-magic-items') export class MagicItem2024 { @Field(() => String, { description: 'The unique identifier for this magic item (e.g., bag-of-holding).' }) @prop({ required: true, index: true, type: () => String }) public index!: string @Field(() => String, { description: 'The name of the magic item.' }) @prop({ required: true, index: true, type: () => String }) public name!: string @Field(() => String, { description: 'A description of the magic item.' }) @prop({ required: true, type: () => String }) public desc!: string @Field(() => String, { nullable: true, description: 'URL of an image for the magic item.' }) @prop({ type: () => String, index: true }) public image?: string @Field(() => EquipmentCategory2024, { description: 'The category of equipment this magic item belongs to.' }) @prop({ type: () => APIReference, index: true }) public equipment_category!: APIReference @Field(() => Boolean, { description: 'Whether this magic item requires attunement.' }) @prop({ required: true, index: true, type: () => Boolean }) public attunement!: boolean @Field(() => Boolean, { description: 'Indicates if this magic item is a variant of another item.' }) @prop({ required: true, index: true, type: () => Boolean }) public variant!: boolean @Field(() => [MagicItem2024], { nullable: true, description: 'Other magic items that are variants of this item.' }) @prop({ type: () => [APIReference], index: true }) public variants!: APIReference[] @Field(() => Rarity2024, { description: 'The rarity of the magic item.' }) @prop({ required: true, index: true, type: () => Rarity2024 }) public rarity!: Rarity2024 @Field(() => String, { nullable: true, description: 'Class restriction for attunement (e.g., "by a wizard").' }) @prop({ type: () => String, index: true }) public limited_to?: string @prop({ required: true, index: true, type: () => String }) public url!: string @Field(() => String, { description: 'Timestamp of the last update.' }) @prop({ required: true, index: true, type: () => String }) public updated_at!: string } export type MagicItemDocument = DocumentType const MagicItemModel = getModelForClass(MagicItem2024) export default MagicItemModel ================================================ FILE: src/models/2024/magicSchool.ts ================================================ import { getModelForClass, prop } from '@typegoose/typegoose' import { DocumentType } from '@typegoose/typegoose/lib/types' import { Field, ObjectType } from 'type-graphql' import { srdModelOptions } from '@/util/modelOptions' @ObjectType({ description: 'A school of magic, representing a particular tradition like Evocation or Illusion.' }) @srdModelOptions('2024-magic-schools') export class MagicSchool2024 { @Field(() => String, { description: 'A brief description of the school of magic.' }) @prop({ type: () => String, index: true }) public description!: string @Field(() => String, { description: 'The unique identifier for this school (e.g., evocation).' }) @prop({ required: true, index: true, type: () => String }) public index!: string @Field(() => String, { description: 'The name of the school (e.g., Evocation).' }) @prop({ required: true, index: true, type: () => String }) public name!: string @prop({ required: true, index: true, type: () => String }) public url!: string @Field(() => String, { description: 'Timestamp of the last update.' }) @prop({ required: true, index: true, type: () => String }) public updated_at!: string } export type MagicSchoolDocument = DocumentType const MagicSchoolModel = getModelForClass(MagicSchool2024) export default MagicSchoolModel ================================================ FILE: src/models/2024/proficiency.ts ================================================ import { getModelForClass, prop } from '@typegoose/typegoose' import { DocumentType } from '@typegoose/typegoose/lib/types' import { Field, ObjectType } from 'type-graphql' import { APIReference } from '@/models/common/apiReference' import { srdModelOptions } from '@/util/modelOptions' @ObjectType({ description: 'A 2024 proficiency.' }) @srdModelOptions('2024-proficiencies') export class Proficiency2024 { @Field(() => String, { description: 'The unique identifier for this proficiency.' }) @prop({ required: true, index: true, type: () => String }) public index!: string @Field(() => String, { description: 'The name of this proficiency.' }) @prop({ required: true, index: true, type: () => String }) public name!: string @Field(() => String, { description: 'The type of proficiency (e.g., Skills, Tools).' }) @prop({ required: true, index: true, type: () => String }) public type!: string @prop({ type: () => [APIReference], required: true }) public backgrounds!: APIReference[] @prop({ type: () => [APIReference], required: true }) public classes!: APIReference[] @Field(() => APIReference, { nullable: true, description: 'The referenced skill or tool.' }) @prop({ type: () => APIReference }) public reference?: APIReference @prop({ required: true, index: true, type: () => String }) public url!: string @Field(() => String, { description: 'Timestamp of the last update.' }) @prop({ required: true, index: true, type: () => String }) public updated_at!: string } export type ProficiencyDocument = DocumentType const ProficiencyModel = getModelForClass(Proficiency2024) export default ProficiencyModel ================================================ FILE: src/models/2024/skill.ts ================================================ import { getModelForClass, prop } from '@typegoose/typegoose' import { DocumentType } from '@typegoose/typegoose/lib/types' import { Field, ObjectType } from 'type-graphql' import { APIReference } from '@/models/common/apiReference' import { srdModelOptions } from '@/util/modelOptions' import { AbilityScore2024 } from './abilityScore' @ObjectType({ description: 'A skill representing proficiency in a specific task (e.g., Athletics, Stealth).' }) @srdModelOptions('2024-skills') export class Skill2024 { @Field(() => AbilityScore2024, { description: 'The ability score associated with this skill.' }) @prop({ type: () => APIReference, required: true }) public ability_score!: APIReference @Field(() => String, { description: 'A description of the skill.' }) @prop({ required: true, index: true, type: () => String }) public description!: string @Field(() => String, { description: 'The unique identifier for this skill (e.g., athletics).' }) @prop({ required: true, index: true, type: () => String }) public index!: string @Field(() => String, { description: 'The name of the skill (e.g., Athletics).' }) @prop({ required: true, index: true, type: () => String }) public name!: string @prop({ required: true, index: true, type: () => String }) public url!: string @Field(() => String, { description: 'Timestamp of the last update.' }) @prop({ required: true, index: true, type: () => String }) public updated_at!: string } export type SkillDocument = DocumentType const SkillModel = getModelForClass(Skill2024) export default SkillModel ================================================ FILE: src/models/2024/species.ts ================================================ import { getModelForClass, prop } from '@typegoose/typegoose' import { DocumentType } from '@typegoose/typegoose/lib/types' import { Field, ObjectType } from 'type-graphql' import { APIReference } from '@/models/common/apiReference' import { Choice } from '@/models/common/choice' import { srdModelOptions } from '@/util/modelOptions' @ObjectType({ description: 'A species representing a playable species in D&D 5e 2024.' }) @srdModelOptions('2024-species') export class Species2024 { @Field(() => String, { description: 'The unique identifier for this species (e.g., elf).' }) @prop({ required: true, index: true, type: () => String }) public index!: string @Field(() => String, { description: 'The name of the species (e.g., Elf).' }) @prop({ required: true, index: true, type: () => String }) public name!: string @Field(() => String, { description: 'The URL of the API resource.' }) @prop({ required: true, type: () => String }) public url!: string @Field(() => String, { description: 'The creature type of this species (e.g., Humanoid).' }) @prop({ required: true, type: () => String }) public type!: string @Field(() => String, { nullable: true, description: 'The size of this species.' }) @prop({ type: () => String }) public size?: string @prop({ type: () => Choice }) public size_options?: Choice @Field(() => Number, { description: 'The base walking speed of this species in feet.' }) @prop({ required: true, type: () => Number }) public speed!: number @Field(() => [APIReference], { nullable: true, description: 'Traits granted by this species.' }) @prop({ type: () => [APIReference] }) public traits?: APIReference[] @Field(() => [APIReference], { nullable: true, description: 'Subspecies available for this species.' }) @prop({ type: () => [APIReference] }) public subspecies?: APIReference[] @prop({ required: true, type: () => String }) public updated_at!: string } export type SpeciesDocument = DocumentType const Species2024Model = getModelForClass(Species2024) export default Species2024Model ================================================ FILE: src/models/2024/subclass.ts ================================================ import { getModelForClass, prop } from '@typegoose/typegoose' import { DocumentType } from '@typegoose/typegoose/lib/types' import { Field, Int, ObjectType } from 'type-graphql' import { srdModelOptions } from '@/util/modelOptions' @ObjectType({ description: 'A feature granted by a 2024 subclass at a specific level.' }) export class SubclassFeature2024 { @Field(() => String, { description: 'The name of the subclass feature.' }) @prop({ required: true, type: () => String }) public name!: string @Field(() => Int, { description: 'The character level at which this feature is gained.' }) @prop({ required: true, type: () => Number }) public level!: number @Field(() => String, { description: 'A description of the subclass feature.' }) @prop({ required: true, type: () => String }) public description!: string } @ObjectType({ description: 'A subclass representing a specialization of a class in D&D 5e 2024.' }) @srdModelOptions('2024-subclasses') export class Subclass2024 { @Field(() => String, { description: 'The unique identifier for this subclass.' }) @prop({ required: true, index: true, type: () => String }) public index!: string @Field(() => String, { description: 'The name of the subclass.' }) @prop({ required: true, index: true, type: () => String }) public name!: string @Field(() => String, { description: 'A brief summary of the subclass.' }) @prop({ required: true, type: () => String }) public summary!: string @Field(() => String, { description: 'A full description of the subclass.' }) @prop({ required: true, type: () => String }) public description!: string @Field(() => [SubclassFeature2024], { description: 'Features granted by this subclass.' }) @prop({ required: true, type: () => [SubclassFeature2024] }) public features!: SubclassFeature2024[] @prop({ required: true, index: true, type: () => String }) public url!: string @Field(() => String, { description: 'Timestamp of the last update.' }) @prop({ required: true, index: true, type: () => String }) public updated_at!: string } export type SubclassDocument = DocumentType const SubclassModel = getModelForClass(Subclass2024) export default SubclassModel ================================================ FILE: src/models/2024/subspecies.ts ================================================ import { getModelForClass, prop } from '@typegoose/typegoose' import { DocumentType } from '@typegoose/typegoose/lib/types' import { Field, ObjectType } from 'type-graphql' import { APIReference } from '@/models/common/apiReference' import { srdModelOptions } from '@/util/modelOptions' @ObjectType({ description: 'A subspecies trait reference including the level at which it is gained.' }) export class SubspeciesTrait { @Field(() => String, { description: 'The unique identifier for this trait.' }) @prop({ required: true, type: () => String }) public index!: string @Field(() => String, { description: 'The name of this trait.' }) @prop({ required: true, type: () => String }) public name!: string @Field(() => String, { description: 'The URL of the trait resource.' }) @prop({ required: true, type: () => String }) public url!: string @Field(() => Number, { description: 'The character level at which this trait is gained.' }) @prop({ required: true, type: () => Number }) public level!: number } @ObjectType({ description: 'A subspecies representing a variant of a playable species in D&D 5e 2024.' }) @srdModelOptions('2024-subspecies') export class Subspecies2024 { @Field(() => String, { description: 'The unique identifier for this subspecies.' }) @prop({ required: true, index: true, type: () => String }) public index!: string @Field(() => String, { description: 'The name of the subspecies.' }) @prop({ required: true, index: true, type: () => String }) public name!: string @Field(() => String, { description: 'The URL of the API resource.' }) @prop({ required: true, type: () => String }) public url!: string @Field(() => APIReference, { description: 'The parent species of this subspecies.' }) @prop({ type: () => APIReference, required: true }) public species!: APIReference @Field(() => [SubspeciesTrait], { description: 'Traits granted by this subspecies.' }) @prop({ type: () => [SubspeciesTrait], required: true }) public traits!: SubspeciesTrait[] @Field(() => APIReference, { nullable: true, description: 'The damage type associated with this subspecies (Dragonborn only).' }) @prop({ type: () => APIReference }) public damage_type?: APIReference @prop({ required: true, type: () => String }) public updated_at!: string } export type SubspeciesDocument = DocumentType const Subspecies2024Model = getModelForClass(Subspecies2024) export default Subspecies2024Model ================================================ FILE: src/models/2024/trait.ts ================================================ import { getModelForClass, prop } from '@typegoose/typegoose' import { DocumentType } from '@typegoose/typegoose/lib/types' import { Field, ObjectType } from 'type-graphql' import { APIReference } from '@/models/common/apiReference' import { Choice } from '@/models/common/choice' import { srdModelOptions } from '@/util/modelOptions' @ObjectType({ description: 'A trait granted by a species or subspecies in D&D 5e 2024.' }) @srdModelOptions('2024-traits') export class Trait2024 { @Field(() => String, { description: 'The unique identifier for this trait (e.g., darkvision).' }) @prop({ required: true, index: true, type: () => String }) public index!: string @Field(() => String, { description: 'The name of the trait (e.g., Darkvision).' }) @prop({ required: true, index: true, type: () => String }) public name!: string @Field(() => String, { description: 'The URL of the API resource.' }) @prop({ required: true, type: () => String }) public url!: string @Field(() => String, { description: 'A description of the trait.' }) @prop({ required: true, type: () => String }) public description!: string @Field(() => [APIReference], { description: 'The species that grant this trait.' }) @prop({ type: () => [APIReference], required: true }) public species!: APIReference[] @Field(() => [APIReference], { nullable: true, description: 'The subspecies that grant this trait.' }) @prop({ type: () => [APIReference] }) public subspecies?: APIReference[] @prop({ type: () => Choice }) public proficiency_choices?: Choice @Field(() => Number, { nullable: true, description: 'Speed override granted by this trait.' }) @prop({ type: () => Number }) public speed?: number @prop({ required: true, type: () => String }) public updated_at!: string } export type TraitDocument = DocumentType const Trait2024Model = getModelForClass(Trait2024) export default Trait2024Model ================================================ FILE: src/models/2024/weaponMasteryProperty.ts ================================================ import { getModelForClass, prop } from '@typegoose/typegoose' import { DocumentType } from '@typegoose/typegoose/lib/types' import { Field, ObjectType } from 'type-graphql' import { srdModelOptions } from '@/util/modelOptions' @ObjectType({ description: 'Each weapon has a mastery property, which is usable only by a character who has a feature, such as Weapon Mastery, that unlocks the property for the character' }) @srdModelOptions('2024-weapon-mastery-properties') export class WeaponMasteryProperty2024 { @Field(() => String, { description: 'A description of the weapon mastery property.' }) @prop({ required: true, index: true, type: () => String }) public description!: string @Field(() => String, { description: 'The unique identifier for this mastery property (e.g., cleave).' }) @prop({ required: true, index: true, type: () => String }) public index!: string @Field(() => String, { description: 'The name of the mastery property (e.g., Cleave).' }) @prop({ required: true, index: true, type: () => String }) public name!: string @prop({ required: true, index: true, type: () => String }) public url!: string @Field(() => String, { description: 'Timestamp of the last update.' }) @prop({ required: true, index: true, type: () => String }) public updated_at!: string } export type WeaponMasteryPropertyDocument = DocumentType const WeaponMasteryPropertyModel = getModelForClass(WeaponMasteryProperty2024) export default WeaponMasteryPropertyModel ================================================ FILE: src/models/2024/weaponProperty.ts ================================================ import { getModelForClass, prop } from '@typegoose/typegoose' import { DocumentType } from '@typegoose/typegoose/lib/types' import { Field, ObjectType } from 'type-graphql' import { srdModelOptions } from '@/util/modelOptions' @ObjectType({ description: 'A property that can be applied to a weapon, modifying its use or characteristics.' }) @srdModelOptions('2024-weapon-properties') export class WeaponProperty2024 { @Field(() => String, { description: 'A description of the weapon property.' }) @prop({ required: true, index: true, type: () => String }) public description!: string @Field(() => String, { description: 'The unique identifier for this property (e.g., versatile).' }) @prop({ required: true, index: true, type: () => String }) public index!: string @Field(() => String, { description: 'The name of the property (e.g., Versatile).' }) @prop({ required: true, index: true, type: () => String }) public name!: string @prop({ required: true, index: true, type: () => String }) public url!: string @Field(() => String, { description: 'Timestamp of the last update.' }) @prop({ required: true, index: true, type: () => String }) public updated_at!: string } export type WeaponPropertyDocument = DocumentType const WeaponPropertyModel = getModelForClass(WeaponProperty2024) export default WeaponPropertyModel ================================================ FILE: src/models/common/apiReference.ts ================================================ import { prop } from '@typegoose/typegoose' import { Field, ObjectType } from 'type-graphql' // Base class representing a reference to another resource @ObjectType({ description: 'Reference to another API resource' }) export class APIReference { @Field(() => String, { description: 'The resource index for the API resource' }) @prop({ required: true, index: true, type: () => String }) public index!: string @Field(() => String, { description: 'The name of the API resource' }) @prop({ required: true, type: () => String }) public name!: string @Field(() => String, { description: 'The URL of the API resource' }) @prop({ required: true, type: () => String }) public url!: string } ================================================ FILE: src/models/common/areaOfEffect.ts ================================================ import { prop } from '@typegoose/typegoose' import { Field, Int, ObjectType } from 'type-graphql' @ObjectType({ description: 'Defines an area of effect for spells or abilities.' }) export class AreaOfEffect { @Field(() => Int, { description: 'The size of the area of effect (e.g., radius in feet).' }) @prop({ required: true, type: () => Number }) public size!: number @Field(() => String, { description: 'The shape of the area of effect.' }) @prop({ required: true, index: true, type: () => String }) public type!: 'sphere' | 'cube' | 'cylinder' | 'line' | 'cone' } ================================================ FILE: src/models/common/choice.ts ================================================ import { getModelForClass, prop } from '@typegoose/typegoose' import { APIReference } from '@/models/common/apiReference' import { Damage } from '@/models/common/damage' import { DifficultyClass } from '@/models/common/difficultyClass' // Option Set Classes export class OptionSet { @prop({ required: true, index: true, type: () => String }) public option_set_type!: 'equipment_category' | 'resource_list' | 'options_array' } export class EquipmentCategoryOptionSet extends OptionSet { @prop({ type: () => APIReference, required: true, index: true }) public equipment_category!: APIReference } export class ResourceListOptionSet extends OptionSet { @prop({ required: true, index: true, type: () => String }) public resource_list_url!: string } export class OptionsArrayOptionSet extends OptionSet { @prop({ type: () => [Option], required: true, index: true }) public options!: Option[] } // Option Classes export class Option { @prop({ required: true, index: true, type: () => String }) public option_type!: string } export class ReferenceOption extends Option { @prop({ type: () => APIReference, required: true, index: true }) public item!: APIReference } export class ActionOption extends Option { @prop({ required: true, index: true, type: () => String }) public action_name!: string @prop({ required: true, index: true, type: () => Number }) public count!: number | string @prop({ required: true, index: true, type: () => String }) public type!: 'melee' | 'ranged' | 'ability' | 'magic' @prop({ index: true, type: () => String }) public notes?: string } export class MultipleOption extends Option { @prop({ type: () => [Option], required: true, index: true }) public items!: Option[] } export class StringOption extends Option { @prop({ required: true, index: true, type: () => String }) public string!: string } export class IdealOption extends Option { @prop({ required: true, index: true, type: () => String }) public desc!: string @prop({ type: () => [APIReference], required: true, index: true }) public alignments!: APIReference[] } export class CountedReferenceOption extends Option { @prop({ required: true, index: true, type: () => Number }) public count!: number @prop({ type: () => APIReference, required: true, index: true }) public of!: APIReference @prop({ type: () => [ { type: { type: String, required: true }, proficiency: { type: () => APIReference } } ], index: true }) public prerequisites?: { type: 'proficiency' proficiency?: APIReference }[] } export class ScorePrerequisiteOption extends Option { @prop({ type: () => APIReference, required: true, index: true }) public ability_score!: APIReference @prop({ required: true, index: true, type: () => Number }) public minimum_score!: number } export class AbilityBonusOption extends Option { @prop({ type: () => APIReference, required: true, index: true }) public ability_score!: APIReference @prop({ required: true, index: true, type: () => Number }) public bonus!: number } export class BreathOption extends Option { @prop({ required: true, index: true, type: () => String }) public name!: string @prop({ type: () => DifficultyClass, required: true, index: true }) public dc!: DifficultyClass @prop({ type: () => [Damage], index: true }) public damage?: Damage[] } export class DamageOption extends Option { @prop({ type: () => APIReference, required: true, index: true }) public damage_type!: APIReference @prop({ required: true, index: true, type: () => String }) public damage_dice!: string @prop({ index: true, type: () => String }) public notes?: string } export class Choice { @prop({ type: () => String, required: true }) public desc!: string @prop({ type: () => Number, required: true }) public choose!: number @prop({ type: () => String, required: true }) public type!: string @prop({ type: () => OptionSet, required: true }) public from!: OptionSet } export class ChoiceOption extends Option { @prop({ type: () => Choice, required: true, index: true }) public choice!: Choice } export class MoneyOption extends Option { @prop({ required: true, index: true, type: () => Number }) public count!: number @prop({ required: true, index: true, type: () => String }) public unit!: string } // Export models export const OptionSetModel = getModelForClass(OptionSet) export const OptionModel = getModelForClass(Option) export const ChoiceModel = getModelForClass(Choice) ================================================ FILE: src/models/common/damage.ts ================================================ import { prop } from '@typegoose/typegoose' import { Field, ObjectType } from 'type-graphql' import { DamageType } from '@/models/2014/damageType' import { APIReference } from '@/models/common/apiReference' @ObjectType({ description: 'Represents damage dealt by an ability, spell, or weapon.' }) export class Damage { @Field(() => DamageType, { nullable: true, description: 'The type of damage.' }) @prop({ type: () => APIReference }) public damage_type!: APIReference // This should reference DamageType, not any APIReference @Field(() => String, { description: 'The damage dice roll (e.g., 3d6).' }) @prop({ required: true, index: true, type: () => String }) public damage_dice!: string } ================================================ FILE: src/models/common/difficultyClass.ts ================================================ import { prop } from '@typegoose/typegoose' import { Field, Int, ObjectType } from 'type-graphql' import { APIReference } from './apiReference' // Assuming apiReference.ts is in the same directory import { AbilityScore } from '../2014/abilityScore' // Path to AbilityScore model @ObjectType({ description: 'Represents a Difficulty Class (DC) for saving throws or ability checks where a value is expected.' }) export class DifficultyClass { @Field(() => AbilityScore, { description: 'The ability score associated with this DC.' }) @prop({ type: () => APIReference }) public dc_type!: APIReference @Field(() => Int, { description: 'The value of the DC.' }) @prop({ required: true, index: true, type: () => Number }) public dc_value!: number @Field(() => String, { description: 'The result of a successful save against this DC.' }) @prop({ required: true, index: true, type: () => String }) public success_type!: 'none' | 'half' | 'other' } ================================================ FILE: src/public/index.html ================================================ D&D 5th Edition API

D&D 5e API

The 5th Edition Dungeons and Dragons API

Just a simple api for things within the Official 5th Edition SRD
and easily accessible through a modern RESTful API.

Enjoy the D&D 5th Edition API!

Try it now!

https://www.dnd5eapi.co/api/2014/

Resource for Acid Arrow


        
================================================ FILE: src/routes/api/2014/abilityScores.ts ================================================ import express from 'express' import AbilityScoreController from '@/controllers/api/2014/abilityScoreController' const router = express.Router() router.get('/', function (req, res, next) { AbilityScoreController.index(req, res, next) }) router.get('/:index', function (req, res, next) { AbilityScoreController.show(req, res, next) }) export default router ================================================ FILE: src/routes/api/2014/alignments.ts ================================================ import * as express from 'express' import AlignmentController from '@/controllers/api/2014/alignmentController' const router = express.Router() router.get('/', function (req, res, next) { AlignmentController.index(req, res, next) }) router.get('/:index', function (req, res, next) { AlignmentController.show(req, res, next) }) export default router ================================================ FILE: src/routes/api/2014/backgrounds.ts ================================================ import * as express from 'express' import BackgroundController from '@/controllers/api/2014/backgroundController' const router = express.Router() router.get('/', function (req, res, next) { BackgroundController.index(req, res, next) }) router.get('/:index', function (req, res, next) { BackgroundController.show(req, res, next) }) export default router ================================================ FILE: src/routes/api/2014/classes.ts ================================================ import * as express from 'express' import * as ClassController from '@/controllers/api/2014/classController' const router = express.Router() router.get('/', function (req, res, next) { ClassController.index(req, res, next) }) router.get('/:index', function (req, res, next) { ClassController.show(req, res, next) }) router.get('/:index/subclasses', function (req, res, next) { ClassController.showSubclassesForClass(req, res, next) }) router.get('/:index/starting-equipment', function (req, res, next) { ClassController.showStartingEquipmentForClass(req, res, next) }) router.get('/:index/spellcasting', function (req, res, next) { ClassController.showSpellcastingForClass(req, res, next) }) router.get('/:index/spells', function (req, res, next) { ClassController.showSpellsForClass(req, res, next) }) router.get('/:index/features', function (req, res, next) { ClassController.showFeaturesForClass(req, res, next) }) router.get('/:index/proficiencies', function (req, res, next) { ClassController.showProficienciesForClass(req, res, next) }) router.get('/:index/multi-classing', function (req, res, next) { ClassController.showMulticlassingForClass(req, res, next) }) router.get('/:index/levels/:level/spells', function (req, res, next) { ClassController.showSpellsForClassAndLevel(req, res, next) }) router.get('/:index/levels/:level/features', function (req, res, next) { ClassController.showFeaturesForClassAndLevel(req, res, next) }) router.get('/:index/levels/:level', function (req, res, next) { ClassController.showLevelForClass(req, res, next) }) router.get('/:index/levels', function (req, res, next) { ClassController.showLevelsForClass(req, res, next) }) export default router ================================================ FILE: src/routes/api/2014/conditions.ts ================================================ import * as express from 'express' import ConditionController from '@/controllers/api/2014/conditionController' const router = express.Router() router.get('/', function (req, res, next) { ConditionController.index(req, res, next) }) router.get('/:index', function (req, res, next) { ConditionController.show(req, res, next) }) export default router ================================================ FILE: src/routes/api/2014/damageTypes.ts ================================================ import * as express from 'express' import DamageTypeController from '@/controllers/api/2014/damageTypeController' const router = express.Router() router.get('/', function (req, res, next) { DamageTypeController.index(req, res, next) }) router.get('/:index', function (req, res, next) { DamageTypeController.show(req, res, next) }) export default router ================================================ FILE: src/routes/api/2014/equipment.ts ================================================ import * as express from 'express' import EquipmentController from '@/controllers/api/2014/equipmentController' const router = express.Router() router.get('/', function (req, res, next) { EquipmentController.index(req, res, next) }) router.get('/:index', function (req, res, next) { EquipmentController.show(req, res, next) }) export default router ================================================ FILE: src/routes/api/2014/equipmentCategories.ts ================================================ import * as express from 'express' import EquipmentCategoryController from '@/controllers/api/2014/equipmentCategoryController' const router = express.Router() router.get('/', function (req, res, next) { EquipmentCategoryController.index(req, res, next) }) router.get('/:index', function (req, res, next) { EquipmentCategoryController.show(req, res, next) }) export default router ================================================ FILE: src/routes/api/2014/feats.ts ================================================ import * as express from 'express' import FeatController from '@/controllers/api/2014/featController' const router = express.Router() router.get('/', function (req, res, next) { FeatController.index(req, res, next) }) router.get('/:index', function (req, res, next) { FeatController.show(req, res, next) }) export default router ================================================ FILE: src/routes/api/2014/features.ts ================================================ import * as express from 'express' import FeatureController from '@/controllers/api/2014/featureController' const router = express.Router() router.get('/', function (req, res, next) { FeatureController.index(req, res, next) }) router.get('/:index', function (req, res, next) { FeatureController.show(req, res, next) }) export default router ================================================ FILE: src/routes/api/2014/images.ts ================================================ import * as express from 'express' import ImageController from '@/controllers/api/imageController' const router = express.Router() router.get('/*splat', function (req, res, next) { ImageController.show(req, res, next) }) export default router ================================================ FILE: src/routes/api/2014/languages.ts ================================================ import * as express from 'express' import LanguageController from '@/controllers/api/2014/languageController' const router = express.Router() router.get('/', function (req, res, next) { LanguageController.index(req, res, next) }) router.get('/:index', function (req, res, next) { LanguageController.show(req, res, next) }) export default router ================================================ FILE: src/routes/api/2014/magicItems.ts ================================================ import * as express from 'express' import * as MagicItemController from '@/controllers/api/2014/magicItemController' const router = express.Router() router.get('/', function (req, res, next) { MagicItemController.index(req, res, next) }) router.get('/:index', function (req, res, next) { MagicItemController.show(req, res, next) }) export default router ================================================ FILE: src/routes/api/2014/magicSchools.ts ================================================ import * as express from 'express' import MagicSchoolController from '@/controllers/api/2014/magicSchoolController' const router = express.Router() router.get('/', function (req, res, next) { MagicSchoolController.index(req, res, next) }) router.get('/:index', function (req, res, next) { MagicSchoolController.show(req, res, next) }) export default router ================================================ FILE: src/routes/api/2014/monsters.ts ================================================ import * as express from 'express' import * as MonsterController from '@/controllers/api/2014/monsterController' const router = express.Router() router.get('/', function (req, res, next) { MonsterController.index(req, res, next) }) router.get('/:index', function (req, res, next) { MonsterController.show(req, res, next) }) export default router ================================================ FILE: src/routes/api/2014/proficiencies.ts ================================================ import * as express from 'express' import ProficiencyController from '@/controllers/api/2014/proficiencyController' const router = express.Router() router.get('/', function (req, res, next) { ProficiencyController.index(req, res, next) }) router.get('/:index', function (req, res, next) { ProficiencyController.show(req, res, next) }) export default router ================================================ FILE: src/routes/api/2014/races.ts ================================================ import * as express from 'express' import * as RaceController from '@/controllers/api/2014/raceController' const router = express.Router() router.get('/', function (req, res, next) { RaceController.index(req, res, next) }) router.get('/:index', function (req, res, next) { RaceController.show(req, res, next) }) router.get('/:index/subraces', function (req, res, next) { RaceController.showSubracesForRace(req, res, next) }) router.get('/:index/proficiencies', function (req, res, next) { RaceController.showProficienciesForRace(req, res, next) }) router.get('/:index/traits', function (req, res, next) { RaceController.showTraitsForRace(req, res, next) }) export default router ================================================ FILE: src/routes/api/2014/ruleSections.ts ================================================ import * as express from 'express' import * as RuleSectionController from '@/controllers/api/2014/ruleSectionController' const router = express.Router() router.get('/', function (req, res, next) { RuleSectionController.index(req, res, next) }) router.get('/:index', function (req, res, next) { RuleSectionController.show(req, res, next) }) export default router ================================================ FILE: src/routes/api/2014/rules.ts ================================================ import * as express from 'express' import * as RuleController from '@/controllers/api/2014/ruleController' const router = express.Router() router.get('/', function (req, res, next) { RuleController.index(req, res, next) }) router.get('/:index', function (req, res, next) { RuleController.show(req, res, next) }) export default router ================================================ FILE: src/routes/api/2014/skills.ts ================================================ import * as express from 'express' import SkillController from '@/controllers/api/2014/skillController' const router = express.Router() router.get('/', function (req, res, next) { SkillController.index(req, res, next) }) router.get('/:index', function (req, res, next) { SkillController.show(req, res, next) }) export default router ================================================ FILE: src/routes/api/2014/spells.ts ================================================ import * as express from 'express' import * as SpellController from '@/controllers/api/2014/spellController' const router = express.Router() router.get('/', function (req, res, next) { SpellController.index(req, res, next) }) router.get('/:index', function (req, res, next) { SpellController.show(req, res, next) }) export default router ================================================ FILE: src/routes/api/2014/subclasses.ts ================================================ import * as express from 'express' import * as SubclassController from '@/controllers/api/2014/subclassController' const router = express.Router() router.get('/', function (req, res, next) { SubclassController.index(req, res, next) }) router.get('/:index', function (req, res, next) { SubclassController.show(req, res, next) }) router.get('/:index/features', function (req, res, next) { SubclassController.showFeaturesForSubclass(req, res, next) }) router.get('/:index/levels/:level/features', function (req, res, next) { SubclassController.showFeaturesForSubclassAndLevel(req, res, next) }) router.get('/:index/levels/:level', function (req, res, next) { SubclassController.showLevelForSubclass(req, res, next) }) router.get('/:index/levels', function (req, res, next) { SubclassController.showLevelsForSubclass(req, res, next) }) export default router ================================================ FILE: src/routes/api/2014/subraces.ts ================================================ import * as express from 'express' import * as SubraceController from '@/controllers/api/2014/subraceController' const router = express.Router() router.get('/', function (req, res, next) { SubraceController.index(req, res, next) }) router.get('/:index', function (req, res, next) { SubraceController.show(req, res, next) }) router.get('/:index/traits', function (req, res, next) { SubraceController.showTraitsForSubrace(req, res, next) }) router.get('/:index/proficiencies', function (req, res, next) { SubraceController.showProficienciesForSubrace(req, res, next) }) export default router ================================================ FILE: src/routes/api/2014/traits.ts ================================================ import * as express from 'express' import TraitController from '@/controllers/api/2014/traitController' const router = express.Router() router.get('/', function (req, res, next) { TraitController.index(req, res, next) }) router.get('/:index', function (req, res, next) { TraitController.show(req, res, next) }) export default router ================================================ FILE: src/routes/api/2014/weaponProperties.ts ================================================ import * as express from 'express' import WeaponPropertyController from '@/controllers/api/2014/weaponPropertyController' const router = express.Router() router.get('/', function (req, res, next) { WeaponPropertyController.index(req, res, next) }) router.get('/:index', function (req, res, next) { WeaponPropertyController.show(req, res, next) }) export default router ================================================ FILE: src/routes/api/2014.ts ================================================ import express from 'express' import { index } from '@/controllers/api/v2014Controller' import AbilityScoresHandler from './2014/abilityScores' import AlignmentsHandler from './2014/alignments' import BackgroundsHandler from './2014/backgrounds' import ClassesHandler from './2014/classes' import ConditionsHandler from './2014/conditions' import DamageTypesHandler from './2014/damageTypes' import EquipmentHandler from './2014/equipment' import EquipmentCategoriesHandler from './2014/equipmentCategories' import FeatsHandler from './2014/feats' import FeaturesHandler from './2014/features' import ImageHandler from './2014/images' import LanguagesHandler from './2014/languages' import MagicItemsHandler from './2014/magicItems' import MagicSchoolsHandler from './2014/magicSchools' import MonstersHandler from './2014/monsters' import ProficienciesHandler from './2014/proficiencies' import RacesHandler from './2014/races' import RulesHandler from './2014/rules' import RuleSectionsHandler from './2014/ruleSections' import SkillsHandler from './2014/skills' import SpellsHandler from './2014/spells' import SubclassesHandler from './2014/subclasses' import SubracesHandler from './2014/subraces' import TraitsHandler from './2014/traits' import WeaponPropertiesHandler from './2014/weaponProperties' const router = express.Router() router.get('/', function (req, res, next) { index(req, res, next) }) router.use('/ability-scores', AbilityScoresHandler) router.use('/alignments', AlignmentsHandler) router.use('/backgrounds', BackgroundsHandler) router.use('/classes', ClassesHandler) router.use('/conditions', ConditionsHandler) router.use('/damage-types', DamageTypesHandler) router.use('/equipment-categories', EquipmentCategoriesHandler) router.use('/equipment', EquipmentHandler) router.use('/feats', FeatsHandler) router.use('/features', FeaturesHandler) router.use('/images', ImageHandler) router.use('/languages', LanguagesHandler) router.use('/magic-items', MagicItemsHandler) router.use('/magic-schools', MagicSchoolsHandler) router.use('/monsters', MonstersHandler) router.use('/proficiencies', ProficienciesHandler) router.use('/races', RacesHandler) router.use('/rules', RulesHandler) router.use('/rule-sections', RuleSectionsHandler) router.use('/skills', SkillsHandler) router.use('/spells', SpellsHandler) router.use('/subclasses', SubclassesHandler) router.use('/subraces', SubracesHandler) router.use('/traits', TraitsHandler) router.use('/weapon-properties', WeaponPropertiesHandler) export default router ================================================ FILE: src/routes/api/2024/abilityScores.ts ================================================ import express from 'express' import AbilityScoreController from '@/controllers/api/2024/abilityScoreController' const router = express.Router() router.get('/', function (req, res, next) { AbilityScoreController.index(req, res, next) }) router.get('/:index', function (req, res, next) { AbilityScoreController.show(req, res, next) }) export default router ================================================ FILE: src/routes/api/2024/alignments.ts ================================================ import express from 'express' import AlignmentController from '@/controllers/api/2024/alignmentController' const router = express.Router() router.get('/', function (req, res, next) { AlignmentController.index(req, res, next) }) router.get('/:index', function (req, res, next) { AlignmentController.show(req, res, next) }) export default router ================================================ FILE: src/routes/api/2024/backgrounds.ts ================================================ import * as express from 'express' import BackgroundController from '@/controllers/api/2024/backgroundController' const router = express.Router() router.get('/', function (req, res, next) { BackgroundController.index(req, res, next) }) router.get('/:index', function (req, res, next) { BackgroundController.show(req, res, next) }) export default router ================================================ FILE: src/routes/api/2024/conditions.ts ================================================ import express from 'express' import ConditionController from '@/controllers/api/2024/conditionController' const router = express.Router() router.get('/', function (req, res, next) { ConditionController.index(req, res, next) }) router.get('/:index', function (req, res, next) { ConditionController.show(req, res, next) }) export default router ================================================ FILE: src/routes/api/2024/damageTypes.ts ================================================ import express from 'express' import DamageTypeController from '@/controllers/api/2024/damageTypeController' const router = express.Router() router.get('/', function (req, res, next) { DamageTypeController.index(req, res, next) }) router.get('/:index', function (req, res, next) { DamageTypeController.show(req, res, next) }) export default router ================================================ FILE: src/routes/api/2024/equipment.ts ================================================ import * as express from 'express' import EquipmentController from '@/controllers/api/2024/equipmentController' const router = express.Router() router.get('/', function (req, res, next) { EquipmentController.index(req, res, next) }) router.get('/:index', function (req, res, next) { EquipmentController.show(req, res, next) }) export default router ================================================ FILE: src/routes/api/2024/equipmentCategories.ts ================================================ import * as express from 'express' import EquipmentCategoryController from '@/controllers/api/2024/equipmentCategoryController' const router = express.Router() router.get('/', function (req, res, next) { EquipmentCategoryController.index(req, res, next) }) router.get('/:index', function (req, res, next) { EquipmentCategoryController.show(req, res, next) }) export default router ================================================ FILE: src/routes/api/2024/feats.ts ================================================ import * as express from 'express' import FeatController from '@/controllers/api/2024/featController' const router = express.Router() router.get('/', function (req, res, next) { FeatController.index(req, res, next) }) router.get('/:index', function (req, res, next) { FeatController.show(req, res, next) }) export default router ================================================ FILE: src/routes/api/2024/languages.ts ================================================ import express from 'express' import LanguageController from '@/controllers/api/2024/languageController' const router = express.Router() router.get('/', function (req, res, next) { LanguageController.index(req, res, next) }) router.get('/:index', function (req, res, next) { LanguageController.show(req, res, next) }) export default router ================================================ FILE: src/routes/api/2024/magicItems.ts ================================================ import * as express from 'express' import MagicItemController from '@/controllers/api/2024/magicItemController' const router = express.Router() router.get('/', function (req, res, next) { MagicItemController.index(req, res, next) }) router.get('/:index', function (req, res, next) { MagicItemController.show(req, res, next) }) export default router ================================================ FILE: src/routes/api/2024/magicSchools.ts ================================================ import express from 'express' import MagicSchoolController from '@/controllers/api/2024/magicSchoolController' const router = express.Router() router.get('/', function (req, res, next) { MagicSchoolController.index(req, res, next) }) router.get('/:index', function (req, res, next) { MagicSchoolController.show(req, res, next) }) export default router ================================================ FILE: src/routes/api/2024/proficiencies.ts ================================================ import * as express from 'express' import ProficiencyController from '@/controllers/api/2024/proficiencyController' const router = express.Router() router.get('/', function (req, res, next) { ProficiencyController.index(req, res, next) }) router.get('/:index', function (req, res, next) { ProficiencyController.show(req, res, next) }) export default router ================================================ FILE: src/routes/api/2024/skills.ts ================================================ import * as express from 'express' import SkillController from '@/controllers/api/2024/skillController' const router = express.Router() router.get('/', function (req, res, next) { SkillController.index(req, res, next) }) router.get('/:index', function (req, res, next) { SkillController.show(req, res, next) }) export default router ================================================ FILE: src/routes/api/2024/species.ts ================================================ import * as express from 'express' import * as SpeciesController from '@/controllers/api/2024/speciesController' const router = express.Router() router.get('/', function (req, res, next) { SpeciesController.index(req, res, next) }) router.get('/:index', function (req, res, next) { SpeciesController.show(req, res, next) }) router.get('/:index/subspecies', function (req, res, next) { SpeciesController.showSubspeciesForSpecies(req, res, next) }) router.get('/:index/traits', function (req, res, next) { SpeciesController.showTraitsForSpecies(req, res, next) }) export default router ================================================ FILE: src/routes/api/2024/subclasses.ts ================================================ import * as express from 'express' import SubclassController from '@/controllers/api/2024/subclassController' const router = express.Router() router.get('/', function (req, res, next) { SubclassController.index(req, res, next) }) router.get('/:index', function (req, res, next) { SubclassController.show(req, res, next) }) export default router ================================================ FILE: src/routes/api/2024/subspecies.ts ================================================ import * as express from 'express' import * as SubspeciesController from '@/controllers/api/2024/subspeciesController' const router = express.Router() router.get('/', function (req, res, next) { SubspeciesController.index(req, res, next) }) router.get('/:index', function (req, res, next) { SubspeciesController.show(req, res, next) }) router.get('/:index/traits', function (req, res, next) { SubspeciesController.showTraitsForSubspecies(req, res, next) }) export default router ================================================ FILE: src/routes/api/2024/traits.ts ================================================ import * as express from 'express' import TraitController from '@/controllers/api/2024/traitController' const router = express.Router() router.get('/', function (req, res, next) { TraitController.index(req, res, next) }) router.get('/:index', function (req, res, next) { TraitController.show(req, res, next) }) export default router ================================================ FILE: src/routes/api/2024/weaponMasteryProperties.ts ================================================ import express from 'express' import WeaponMasteryPropertyController from '@/controllers/api/2024/weaponMasteryPropertyController' const router = express.Router() router.get('/', function (req, res, next) { WeaponMasteryPropertyController.index(req, res, next) }) router.get('/:index', function (req, res, next) { WeaponMasteryPropertyController.show(req, res, next) }) export default router ================================================ FILE: src/routes/api/2024/weaponProperty.ts ================================================ import express from 'express' import WeaponPropertyController from '@/controllers/api/2024/weaponPropertyController' const router = express.Router() router.get('/', function (req, res, next) { WeaponPropertyController.index(req, res, next) }) router.get('/:index', function (req, res, next) { WeaponPropertyController.show(req, res, next) }) export default router ================================================ FILE: src/routes/api/2024.ts ================================================ import express from 'express' import { index } from '@/controllers/api/v2024Controller' import AbilityScoresHandler from './2024/abilityScores' import AlignmentsHandler from './2024/alignments' import BackgroundsHandler from './2024/backgrounds' import ConditionsHandler from './2024/conditions' import DamageTypesHandler from './2024/damageTypes' import EquipmentHandler from './2024/equipment' import EquipmentCategoriesHandler from './2024/equipmentCategories' import FeatsHandler from './2024/feats' import LanguagesHandler from './2024/languages' import MagicItemsHandler from './2024/magicItems' import MagicSchoolsHandler from './2024/magicSchools' import ProficienciesHandler from './2024/proficiencies' import SkillsHandler from './2024/skills' import SpeciesHandler from './2024/species' import SubclassesHandler from './2024/subclasses' import SubspeciesHandler from './2024/subspecies' import TraitsHandler from './2024/traits' import WeaponMasteryPropertiesHandler from './2024/weaponMasteryProperties' import WeaponPropertiesHandler from './2024/weaponProperty' const router = express.Router() router.get('/', function (req, res, next) { index(req, res, next) }) router.use('/ability-scores', AbilityScoresHandler) router.use('/alignments', AlignmentsHandler) router.use('/backgrounds', BackgroundsHandler) router.use('/conditions', ConditionsHandler) router.use('/damage-types', DamageTypesHandler) router.use('/equipment', EquipmentHandler) router.use('/equipment-categories', EquipmentCategoriesHandler) router.use('/feats', FeatsHandler) router.use('/languages', LanguagesHandler) router.use('/magic-items', MagicItemsHandler) router.use('/magic-schools', MagicSchoolsHandler) router.use('/proficiencies', ProficienciesHandler) router.use('/skills', SkillsHandler) router.use('/species', SpeciesHandler) router.use('/subclasses', SubclassesHandler) router.use('/subspecies', SubspeciesHandler) router.use('/traits', TraitsHandler) router.use('/weapon-mastery-properties', WeaponMasteryPropertiesHandler) router.use('/weapon-properties', WeaponPropertiesHandler) export default router ================================================ FILE: src/routes/api/images.ts ================================================ import * as express from 'express' import ImageController from '@/controllers/api/imageController' const router = express.Router() router.get('/*splat', function (req, res, next) { ImageController.show(req, res, next) }) export default router ================================================ FILE: src/routes/api.ts ================================================ import express from 'express' import deprecatedApiController from '@/controllers/apiController' import v2014Handler from './api/2014' import v2024Handler from './api/2024' import ImageHandler from './api/images' const router = express.Router() router.use('/2014', v2014Handler) router.use('/2024', v2024Handler) router.use('/images', ImageHandler) router.get('*splat', deprecatedApiController) export default router ================================================ FILE: src/schemas/schemas.ts ================================================ import { z } from 'zod' // --- Helper Functions --- /** * Zod transform helper to ensure the value is an array or undefined. * If the input is a single value, it's wrapped in an array. * If the input is already an array, it's returned as is. * If the input is nullish, undefined is returned. */ const ensureArrayOrUndefined = (val: T | T[] | undefined): T[] | undefined => { if (val === undefined || val === null) { return undefined } if (Array.isArray(val)) { return val } return [val] } // --- Base Schemas --- export const ShowParamsSchema = z.object({ index: z.string().min(1) }) export const NameQuerySchema = z.object({ name: z.string().optional() }) // --- Derived Generic Schemas --- export const NameDescQuerySchema = NameQuerySchema.extend({ desc: z.string().optional() }) export const LevelParamsSchema = ShowParamsSchema.extend({ level: z.coerce.number().int().min(1).max(20) }) // --- Specific Controller Schemas --- // Schemas from api/2014/spellController.ts export const SpellIndexQuerySchema = NameQuerySchema.extend({ level: z .string() .regex(/^\d+$/) .or(z.array(z.string().regex(/^\d+$/))) .optional() .transform(ensureArrayOrUndefined), school: z.string().or(z.array(z.string())).optional().transform(ensureArrayOrUndefined) }) // Schemas from api/2014/classController.ts export const ClassLevelsQuerySchema = z.object({ subclass: z.string().min(1).optional() }) // Schemas from api/2014/monsterController.ts // --- Helper Transformation (for MonsterIndexQuerySchema) --- const transformChallengeRating = (val: string | string[] | undefined) => { if (val == null || val === '' || (Array.isArray(val) && val.length === 0)) return undefined // Ensure it's an array, handling both single string and array inputs const arr = Array.isArray(val) ? val : [val] // Flatten in case of comma-separated strings inside the array, then split const flattened = arr.flatMap((item) => item.split(',')) // Convert to numbers and filter out NaNs const numbers = flattened.map(Number).filter((item) => !isNaN(item)) // Return undefined if no valid numbers result, otherwise return the array return numbers.length > 0 ? numbers : undefined } export const MonsterIndexQuerySchema = NameQuerySchema.extend({ challenge_rating: z.string().or(z.string().array()).optional().transform(transformChallengeRating) }) ================================================ FILE: src/server.ts ================================================ import 'reflect-metadata' // Must be imported first import path from 'path' import { fileURLToPath } from 'url' import { expressMiddleware } from '@as-integrations/express5' import bodyParser from 'body-parser' import cors from 'cors' import express from 'express' import rateLimit from 'express-rate-limit' import morgan from 'morgan' import { buildSchema } from 'type-graphql' import docsController from './controllers/docsController' import { resolvers as resolvers2014 } from './graphql/2014/resolvers' import { resolvers as resolvers2024 } from './graphql/2024/resolvers' import { createApolloMiddleware } from './middleware/apolloServer' import bugsnagMiddleware from './middleware/bugsnag' import errorHandlerMiddleware from './middleware/errorHandler' import apiRoutes from './routes/api' const __filename = fileURLToPath(import.meta.url) const __dirname = path.dirname(__filename) const rateLimitWindowMs = process.env.RATE_LIMIT_WINDOW_MS != null ? parseInt(process.env.RATE_LIMIT_WINDOW_MS, 10) : 1000 // Default 1 second const rateLimitMax = process.env.RATE_LIMIT_MAX != null ? parseInt(process.env.RATE_LIMIT_MAX, 10) : 50 // Default 50 const limiter = rateLimit({ windowMs: rateLimitWindowMs, max: rateLimitMax, message: `Rate limit of ${rateLimitMax} requests per ${rateLimitWindowMs / 1000} second(s) exceeded, try again later.` }) export default async () => { const app = express() // Middleware stuff if (bugsnagMiddleware) { app.use(bugsnagMiddleware.requestHandler) } app.use('/swagger', express.static(__dirname + '/swagger')) app.use('/js', express.static(__dirname + '/js')) app.use('/css', express.static(__dirname + '/css')) app.use('/public', express.static(__dirname + '/public')) app.use(morgan('short')) // Enable all CORS requests app.use(cors()) app.use(limiter) console.log('Building TypeGraphQL schema...') const schema2014 = await buildSchema({ resolvers: resolvers2014, validate: { forbidUnknownValues: false } }) const schema2024 = await buildSchema({ resolvers: resolvers2024, validate: { forbidUnknownValues: false } }) console.log('TypeGraphQL schema built successfully.') console.log('Setting up Apollo GraphQL server') const apolloMiddleware2024 = await createApolloMiddleware(schema2024) await apolloMiddleware2024.start() app.use( '/graphql/2024', cors(), bodyParser.json(), expressMiddleware(apolloMiddleware2024, { context: async ({ req }) => ({ token: req.headers.token }) }) ) const apolloMiddleware2014 = await createApolloMiddleware(schema2014) await apolloMiddleware2014.start() app.use( '/graphql/2014', cors(), bodyParser.json(), expressMiddleware(apolloMiddleware2014, { context: async ({ req }) => ({ token: req.headers.token }) }) ) // DEPRECATED app.use( '/graphql', cors(), bodyParser.json(), expressMiddleware(apolloMiddleware2014, { context: async ({ req }) => ({ token: req.headers.token }) }) ) // Register routes app.get('/', (req, res) => { res.sendFile(path.join(__dirname, 'public/index.html')) }) app.get('/docs', docsController) app.use('/api', apiRoutes) if (bugsnagMiddleware?.errorHandler) { app.use(bugsnagMiddleware.errorHandler) } app.use(errorHandlerMiddleware) return app } ================================================ FILE: src/start.ts ================================================ import mongoose from 'mongoose' import createApp from './server' import { mongodbUri, prewarmCache, redisClient } from './util' const start = async () => { console.log('Setting up MongoDB') // Mongoose: the `strictQuery` option will be switched back to `false` by // default in Mongoose 7, when we update to Mongoose 7 we can remove this. mongoose.set('strictQuery', false) await mongoose.connect(mongodbUri) console.log('Database connection ready') redisClient.on('error', (err) => console.log('Redis Client Error', err)) await redisClient.connect() console.log('Redis connection ready') console.log('Flushing Redis') await redisClient.flushAll() console.log('Prewarm Redis') await prewarmCache() console.log('Setting up Express server') const app = await createApp() console.log('Starting server...') const port = process.env.PORT ?? 3000 app.listen(port, () => { console.log(`Listening on port ${port}! 🚀`) }) } start().catch((err) => { console.error(err) process.exit(1) }) ================================================ FILE: src/swagger/README.md ================================================ # OpenAPI for the DND API The `/swagger` directory contains an OpenAPI 3.0 definition for the DND API. We use [swagger-cli](https://github.com/APIDevTools/swagger-cli) to validate and bundle our OpenAPI definition, and [RapiDoc](https://mrin9.github.io/RapiDoc/index.html) as the documentation viewer. **_Note:_** there are currently a handful of small inconsistencies between the documentation and actual API response. If you come across any please let us know! **_Possible Future Improvements (PRs Encouraged :)_** - [ ] standardize schema object naming (mostly cleanup the /schemas directory) - [ ] validate schemas against models or actual api responses - [ ] validate schema and field descriptions are accurate - [ ] reorganize tag ordering - [ ] add tag descriptions - [ ] enumerate the `class.class_specific` field - [ ] give user option to change render style and schema style (rapidoc) - [ ] generate pieces of documentation based on source code e.g., generate OpenAPI `SchemaObject` from a TypeScript `type` definition - [ ] code snippet examples in various languages - [ ] ...anything else you want! ## Background ### What is the OpenAPI Specification? > The OpenAPI Specification (OAS) defines a standard, programming language-agnostic interface description for HTTP APIs, which allows both humans and computers to discover and understand the capabilities of a service without requiring access to source code, additional documentation, or inspection of network traffic. When properly defined via OpenAPI, a consumer can understand and interact with the remote service with a minimal amount of implementation logic. Similar to what interface descriptions have done for lower-level programming, the OpenAPI Specification removes guesswork in calling a service.[^openapi] [^openapi]: https://github.com/OAI/OpenAPI-Specification/ ### What is Swagger? > Swagger is a set of open-source tools built around the OpenAPI Specification that can help you design, build, document and consume REST APIs.[^swagger] [^swagger]: https://swagger.io/docs/specification/about/ ### Demo A valid OpenAPI definition gives us a bunch of options when it comes to surfacing docs to end users. ![example!](./assets/demo.gif 'example') ## Documenting an Endpoint We need 3 pieces to document an endpoint under the OpenAPI spec: - [PathItemObject][pathobj]: Describes the operations available on a single path. Defines the shape of parameters, requests, and responses for a particular endpoint. - [Parameter Object][paramobj]: Describes a single operation parameter. The expected format, acceptable values, and whether the parameter is required or optional. - [Schema Object][schemaobj]: Describes the definition of an input or output data types. These types can be objects, but also primitives and arrays. [schemaobj]: https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#schemaObject [pathobj]: https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#pathItemObject [paramobj]: https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#parameterObject ## File Organization > An OpenAPI document MAY be made up of a single document or be divided into multiple, connected parts at the discretion of the user.[^oas_org] [^oas_org]: An OpenAPI document is a document (or set of documents) that defines or describes an API. An OpenAPI definition uses and conforms to the OpenAPI Specification. An OpenAPI document that conforms to the OpenAPI Specification is itself a JSON object, which may be represented either in JSON or YAML format. https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#document-structure The root of our OpenAPI definition lives in `swagger.yml`. This file contains general information about the API, endpoint definitions, and definitions of reusable components. Reference Objects[^oas_ref] are used to allow components to reference each other. [^oas_ref]: A simple object to allow referencing other components in the specification, internally and externally. https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#referenceObject For reusability and readability, definitions are split into 3 directories. - `/schemas` - Each `.yml` file contains definitions of one or more `SchemaObject`. Each schema more or less corresponds to one of the models in the [`src/models` directory](https://github.com/5e-bits/5e-srd-api/tree/main/src/models). We use these schemas to describe the structure of response bodies. - `/paths` - Each `.yml` file contains definitions of one or more `PathItemObject`, where each of those objects defines an operation on an endpoint. Each file more or less corresponds to a controller in the [`src/controllers/api` directory](https://github.com/5e-bits/5e-srd-api/tree/main/src/controllers/api). These objects also include the example response shown in the endpoint documentation. - `/parameters` - Contains definitions of reusable path and query parameters. Each of those directories contains a file named `combined.yml` consisting of named references to each of the objects defined in sibling `.yml` files in that directory. The `combined.yml` files provides a single source we can reference from other components. By only referencing objects from the `combined.yml` files, we avoid any problems with circular references. ## Developing Locally There's many possible ways to make changes and view them locally, I'll describe my personal setup and workflow here. - local copy of the [database](https://github.com/5e-bits/5e-database) running in a custom built docker container on a machine on my local network - [`redis`](https://redis.io/) running on my laptop - local copy of the [api](https://github.com/5e-bits/5e-srd-api) running against my local database - I start this by running `MONGODB_URI=mongodb:///5e-database npm start` where `LOCAL_IP` is the ip address of the machine running the database docker container - [Swagger Viewer](https://marketplace.visualstudio.com/items?itemName=Arjun.swagger-viewer) extension for VSCode - be sure to trigger the "Preview Swagger" command from the `swagger.yml` file ### Useful Commands From the root of the project directory two `npm` commands are available related to these docs. `npm run validate-swagger` - checks that the OpenAPI definition in `swagger/swagger.yml` is valid `npm run bundle-swagger` - bundles the OpenAPI definition in `swagger/swagger.json` with all the associated referenced files, and writes the file to the `swagger/dist` directory ## Postman [Postman](https://learning.postman.com/docs/getting-started/introduction/) is a platform for building and using APIs, it provides a user friendly GUI for creating and testing HTTP requests, and is free for personal use. We don't use Postman to build this API, but it can be a useful tool for testing and exploration. You can generate a Postman collection based on the OpenAPI definition for the API via the `npm run gen-postman` task, and then [import that collection into Postman](https://learning.postman.com/docs/getting-started/importing-and-exporting-data/#importing-data-into-postman) to use it. Under the hood the `gen-postman` task uses [`portman`](https://github.com/apideck-libraries/portman), based on the configuration defined in `portman-cli.json`. There's a number of configuration options that affect how the collection is generated, you can experiment with these options either by changing your local copy of that config file and running the `gen-postman` task, or by executing `portman` from the command line with different options. For example, the CLI command equivalent to the config file would be: ```bash portman -l src/swagger/dist/openapi.yml -o collection.postman.json -t ``` ================================================ FILE: src/swagger/parameters/2014/combined.yml ================================================ ability-score-index: $ref: './path/ability-scores.yml' alignment-index: $ref: './path/alignments.yml' language-index: $ref: './path/languages.yml' proficiency-index: $ref: './path/proficiencies.yml' skill-index: $ref: './path/skills.yml' class-index: $ref: './path/classes.yml#/class-index' background-index: $ref: './path/backgrounds.yml' weapon-property-index: $ref: './path/weapon-properties.yml' class-level: $ref: './path/classes.yml#/class-level' spell-level: $ref: './path/classes.yml#/spell-level' condition-index: $ref: './path/conditions.yml' damage-type-index: $ref: './path/damage-types.yml' magic-school-index: $ref: './path/magic-schools.yml' equipment-index: $ref: './path/equipment.yml' feature-index: $ref: './path/features.yml' rule-index: $ref: './path/rules.yml' rule-section-index: $ref: './path/rule-sections.yml' race-index: $ref: './path/races.yml' subclass-index: $ref: './path/subclasses.yml' subrace-index: $ref: './path/subraces.yml' trait-index: $ref: './path/traits.yml' monster-index: $ref: './path/monsters.yml' spell-index: $ref: './path/spells.yml' level-filter: $ref: './query/spells.yml#/level-filter' school-filter: $ref: './query/spells.yml#/school-filter' challenge-rating-filter: $ref: './query/monsters.yml#/challenge-rating-filter' levels-subclass-filter: $ref: './query/classes.yml#/levels-subclass-filter' base-endpoint-index: $ref: './path/common.yml' ================================================ FILE: src/swagger/parameters/2014/path/ability-scores.yml ================================================ name: index in: path required: true description: | The `index` of the ability score to get. schema: type: string enum: [cha, con, dex, int, str, wis] example: cha ================================================ FILE: src/swagger/parameters/2014/path/alignments.yml ================================================ name: index in: path required: true description: | The `index` of the alignment to get. schema: type: string enum: - chaotic-neutral - chaotic-evil - chaotic-good - lawful-neutral - lawful-evil - lawful-good - neutral - neutral-evil - neutral-good example: chaotic-neutral ================================================ FILE: src/swagger/parameters/2014/path/backgrounds.yml ================================================ name: index in: path required: true description: | The `index` of the background to get. schema: type: string enum: [acolyte] example: acolyte ================================================ FILE: src/swagger/parameters/2014/path/classes.yml ================================================ class-index: name: index in: path required: true description: | The `index` of the class to get. schema: type: string enum: - barbarian - bard - cleric - druid - fighter - monk - paladin - ranger - rogue - sorcerer - warlock - wizard example: paladin class-level: name: class_level in: path required: true schema: type: number minimum: 0 maximum: 20 example: 3 spell-level: name: spell_level in: path required: true schema: type: number minimum: 1 maximum: 9 example: 4 ================================================ FILE: src/swagger/parameters/2014/path/common.yml ================================================ name: endpoint in: path required: true schema: type: string enum: - ability-scores - alignments - backgrounds - classes - conditions - damage-types - equipment - equipment-categories - feats - features - languages - magic-items - magic-schools - monsters - proficiencies - races - rule-sections - rules - skills - spells - subclasses - subraces - traits - weapon-properties example: ability-scores ================================================ FILE: src/swagger/parameters/2014/path/conditions.yml ================================================ name: index in: path required: true description: | The `index` of the condition to get. schema: type: string enum: - blinded - charmed - deafened - exhaustion - frightened - grappled - incapacitated - invisible - paralyzed - petrified - poisoned - prone - restrained - stunned - unconscious example: blinded ================================================ FILE: src/swagger/parameters/2014/path/damage-types.yml ================================================ name: index in: path required: true description: | The `index` of the damage type to get. schema: type: string enum: - acid - bludgeoning - cold - fire - force - lightning - necrotic - piercing - poison - psychic - radiant - slashing - thunder example: acid ================================================ FILE: src/swagger/parameters/2014/path/equipment.yml ================================================ name: index in: path required: true description: | The `index` of the equipment to get. Available values can be found in the [`ResourceList`](#get-/api/2014/-endpoint-) for `equipment`. schema: type: string example: club ================================================ FILE: src/swagger/parameters/2014/path/features.yml ================================================ name: index in: path required: true description: | The `index` of the feature to get. Available values can be found in the [`ResourceList`](#get-/api/2014/-endpoint-) for `features`. schema: type: string example: action-surge-1-use ================================================ FILE: src/swagger/parameters/2014/path/languages.yml ================================================ name: index in: path required: true description: | The `index` of the language to get. schema: type: string enum: - abyssal - celestial - common - deep-speech - draconic - dwarvish - elvish - giant - gnomish - goblin - halfling - infernal - orc - primordial - sylvan - undercommon example: abyssal ================================================ FILE: src/swagger/parameters/2014/path/magic-schools.yml ================================================ name: index in: path required: true description: | The `index` of the magic school to get. schema: type: string enum: - abjuration - conjuration - divination - enchantment - evocation - illusion - necromancy - transmutation example: abjuration ================================================ FILE: src/swagger/parameters/2014/path/monsters.yml ================================================ name: index in: path required: true description: | The `index` of the `Monster` to get. schema: type: string example: aboleth ================================================ FILE: src/swagger/parameters/2014/path/proficiencies.yml ================================================ name: index in: path required: true description: | The `index` of the proficiency to get. Available values can be found in the [`ResourceList`](#get-/api/2014/-endpoint-) for `proficiencies`. schema: type: string example: medium-armor ================================================ FILE: src/swagger/parameters/2014/path/races.yml ================================================ name: index in: path required: true description: | The `index` of the race to get. schema: type: string enum: - dragonborn - dwarf - elf - gnome - half-elf - half-orc - halfling - human - tiefling example: elf ================================================ FILE: src/swagger/parameters/2014/path/rule-sections.yml ================================================ name: index in: path required: true description: | The `index` of the rule section to get. schema: type: string enum: - ability-checks - ability-scores-and-modifiers - actions-in-combat - activating-an-item - advantage-and-disadvantage - attunement - between-adventures - casting-a-spell - cover - damage-and-healing - diseases - fantasy-historical-pantheons - madness - making-an-attack - mounted-combat - movement - movement-and-position - objects - poisons - proficiency-bonus - resting - saving-throws - sentient-magic-items - standard-exchange-rates - the-environment - the-order-of-combat - the-planes-of-existence - time - traps - underwater-combat - using-each-ability - wearing-and-wielding-items - what-is-a-spell example: traps ================================================ FILE: src/swagger/parameters/2014/path/rules.yml ================================================ name: index in: path required: true description: | The `index` of the rule to get. schema: type: string enum: - adventuring - appendix - combat - equipment - spellcasting - using-ability-scores example: adventuring ================================================ FILE: src/swagger/parameters/2014/path/skills.yml ================================================ name: index in: path required: true description: | The `index` of the skill to get. schema: type: string enum: - acrobatics - animal-handling - arcana - athletics - deception - history - insight - intimidation - investigation - medicine - nature - perception - performance - persuasion - religion - sleight-of-hand - stealth - survival example: nature ================================================ FILE: src/swagger/parameters/2014/path/spells.yml ================================================ name: index in: path required: true description: | The `index` of the `Spell` to get. Available values can be found in the [`ResourceList`](#get-/api/2014/-endpoint-) for `spells`. schema: type: string example: sacred-flame ================================================ FILE: src/swagger/parameters/2014/path/subclasses.yml ================================================ name: index in: path required: true description: | The `index` of the subclass to get. schema: type: string enum: - berserker - champion - devotion - draconic - evocation - fiend - hunter - land - life - lore - open-hand - thief example: fiend ================================================ FILE: src/swagger/parameters/2014/path/subraces.yml ================================================ name: index in: path required: true description: | The `index` of the subrace to get. schema: type: string enum: - high-elf - hill-dwarf - lightfoot-halfling - rock-gnome example: hill-dwarf ================================================ FILE: src/swagger/parameters/2014/path/traits.yml ================================================ name: index in: path required: true description: The `index` of the `Trait` to get. schema: type: string enum: - artificers-lore - brave - breath-weapon - damage-resistance - darkvision - draconic-ancestry - draconic-ancestry-black - draconic-ancestry-blue - draconic-ancestry-brass - draconic-ancestry-bronze - draconic-ancestry-copper - draconic-ancestry-gold - draconic-ancestry-green - draconic-ancestry-red - draconic-ancestry-silver - draconic-ancestry-white - dwarven-combat-training - dwarven-resilience - dwarven-toughness - elf-weapon-training - extra-language - fey-ancestry - gnome-cunning - halfling-nimbleness - hellish-resistance - high-elf-cantrip - infernal-legacy - keen-senses - lucky - menacing - naturally-stealthy - relentless-endurance - savage-attacks - skill-versatility - stonecunning - tinker - tool-proficiency - trance example: trance ================================================ FILE: src/swagger/parameters/2014/path/weapon-properties.yml ================================================ name: index in: path required: true description: | The `index` of the weapon property to get. schema: type: string enum: - ammunition - finesse - heavy - light - loading - monk - reach - special - thrown - two-handed - versatile example: ammunition ================================================ FILE: src/swagger/parameters/2014/query/classes.yml ================================================ levels-subclass-filter: name: subclass in: query required: false description: Adds subclasses for class to the response schema: type: string examples: single-value: value: berserker partial-value: value: ber ================================================ FILE: src/swagger/parameters/2014/query/monsters.yml ================================================ challenge-rating-filter: name: challenge_rating in: query required: false description: The challenge rating or ratings to filter on. schema: type: array items: type: number examples: single-value: value: [1] multiple-value: value: [1, 2] multiple-value-with-float: value: [2, 0.25] ================================================ FILE: src/swagger/parameters/2014/query/spells.yml ================================================ level-filter: name: level in: query required: false description: The level or levels to filter on. schema: type: array items: type: integer examples: single-value: value: [1] multiple-value: value: [1, 2] school-filter: name: school in: query required: false description: The magic school or schools to filter on. schema: type: array items: type: string examples: single-value: value: [illusion] multiple-value: value: [evocation, illusion] partial-value: value: [illu] ================================================ FILE: src/swagger/parameters/2024/.keepme ================================================ ================================================ FILE: src/swagger/paths/2014/ability-scores.yml ================================================ get: summary: Get an ability score by index. description: | # Ability Score Represents one of the six abilities that describes a creature's physical and mental characteristics. The three main rolls of the game - the ability check, the saving throw, and the attack roll - rely on the ability scores. [[SRD p76](https://media.wizards.com/2016/downloads/DND/SRD-OGL_V5.1.pdf#page=76)] tags: - Character Data parameters: - $ref: '../../parameters/2014/combined.yml#/ability-score-index' responses: '200': description: OK content: application/json: schema: $ref: '../../schemas/2014/combined.yml#/AbilityScore' example: index: 'cha' name: 'CHA' url: '/api/2014/ability-scores/cha' desc: [ 'Charisma measures your ability to interact effectively with others. It includes such factors as confidence and eloquence, and it can represent a charming or commanding personality.', 'A Charisma check might arise when you try to influence or entertain others, when you try to make an impression or tell a convincing lie, or when you are navigating a tricky social situation. The Deception, Intimidation, Performance, and Persuasion skills reflect aptitude in certain kinds of Charisma checks.' ] full_name: 'Charisma' skills: - index: 'deception' name: 'Deception' url: '/api/2014/skills/deception' - index: 'intimidation' name: 'Intimidation' url: '/api/2014/skills/intimidation' - index: 'performance' name: 'Performance' url: '/api/2014/skills/performance' - index: 'persuasion' name: 'Persuasion' url: '/api/2014/skills/persuasion' ================================================ FILE: src/swagger/paths/2014/alignments.yml ================================================ get: summary: Get an alignment by index. description: | # Alignment A typical creature in the game world has an alignment, which broadly describes its moral and personal attitudes. Alignment is a combination of two factors: one identifies morality (good, evil, or neutral), and the other describes attitudes toward society and order (lawful, chaotic, or neutral). Thus, nine distinct alignments define the possible combinations.[[SRD p58](https://media.wizards.com/2016/downloads/DND/SRD-OGL_V5.1.pdf#page=58)] tags: - Character Data parameters: - $ref: '../../parameters/2014/combined.yml#/alignment-index' responses: '200': description: 'OK' content: application/json: schema: $ref: '../../schemas/2014/combined.yml#/Alignment' example: index: chaotic-neutral name: Chaotic Neutral url: '/api/2014/alignments/chaotic-neutral' desc: Chaotic neutral (CN) creatures follow their whims, holding their personal freedom above all else. Many barbarians and rogues, and some bards, are chaotic neutral. abbreviation: CN ================================================ FILE: src/swagger/paths/2014/backgrounds.yml ================================================ get: summary: Get a background by index. description: | # Background Every story has a beginning. Your character's background reveals where you came from, how you became an adventurer, and your place in the world. Choosing a background provides you with important story cues about your character's identity. [[SRD p60](https://media.wizards.com/2016/downloads/DND/SRD-OGL_V5.1.pdf#page=60)] _Note:_ acolyte is the only background included in the SRD. tags: - Character Data parameters: - $ref: '../../parameters/2014/combined.yml#/background-index' responses: '200': description: OK content: application/json: schema: $ref: '../../schemas/2014/combined.yml#/Background' example: index: acolyte name: Acolyte starting_proficiencies: - index: skill-insight name: 'Skill: Insight' url: '/api/2014/proficiencies/skill-insight' - index: skill-religion name: 'Skill: Religion' url: '/api/2014/proficiencies/skill-religion' language_options: choose: 2 type: languages from: option_set_type: resource_list resource_list_url: '/api/2014/languages' starting_equipment: - equipment: index: clothes-common name: Clothes, common url: '/api/2014/equipment/clothes-common' quantity: 1 - equipment: index: pouch name: Pouch url: '/api/2014/equipment/pouch' quantity: 1 starting_equipment_options: - choose: 1 type: equipment from: option_set_type: equipment_category equipment_category: index: holy-symbols name: Holy Symbols url: '/api/2014/equipment-categories/holy-symbols' feature: name: Shelter of the Faithful desc: - As an acolyte, you command the respect of those who share your faith, and you can perform the religious ceremonies of your deity. You and your adventuring companions can expect to receive free healing and care at a temple, shrine, or other established presence of your faith, though you must provide any material components needed for spells. Those who share your religion will support you (but only you) at a modest lifestyle. - You might also have ties to a specific temple dedicated to your chosen deity or pantheon, and you have a residence there. This could be the temple where you used to serve, if you remain on good terms with it, or a temple where you have found a new home. While near your temple, you can call upon the priests for assistance, provided the assistance you ask for is not hazardous and you remain in good standing with your temple. personality_traits: choose: 2 type: personality_traits from: option_set_type: options_array options: - option_type: string string: I idolize a particular hero of my faith, and constantly refer to that person's deeds and example. - option_type: string string: I can find common ground between the fiercest enemies, empathizing with them and always working toward peace. - option_type: string string: I see omens in every event and action. The gods try to speak to us, we just need to listen. - option_type: string string: Nothing can shake my optimistic attitude. - option_type: string string: I quote (or misquote) sacred texts and proverbs in almost every situation. - option_type: string string: I am tolerant (or intolerant) of other faiths and respect (or condemn) the worship of other gods. - option_type: string string: I've enjoyed fine food, drink, and high society among my temple's elite. Rough living grates on me. - option_type: string string: I've spent so long in the temple that I have little practical experience dealing with people in the outside world. ideals: choose: 1 type: ideals from: option_set_type: options_array options: - option_type: ideal desc: Tradition. The ancient traditions of worship and sacrifice must be preserved and upheld. alignments: - index: lawful-good name: Lawful Good url: '/api/2014/alignments/lawful-good' - index: lawful-neutral name: Lawful Neutral url: '/api/2014/alignments/lawful-neutral' - index: lawful-evil name: Lawful Evil url: '/api/2014/alignments/lawful-evil' - option_type: ideal desc: Charity. I always try to help those in need, no matter what the personal cost. alignments: - index: lawful-good name: Lawful Good url: '/api/2014/alignments/lawful-good' - index: neutral-good name: Neutral Good url: '/api/2014/alignments/neutral-good' - index: chaotic-good name: Chaotic Good url: '/api/2014/alignments/chaotic-good' - option_type: ideal desc: Change. We must help bring about the changes the gods are constantly working in the world. alignments: - index: chaotic-good name: Chaotic Good url: '/api/2014/alignments/chaotic-good' - index: chaotic-neutral name: Chaotic Neutral url: '/api/2014/alignments/chaotic-neutral' - index: chaotic-evil name: Chaotic Evil url: '/api/2014/alignments/chaotic-evil' - option_type: ideal desc: Power. I hope to one day rise to the top of my faith's religious hierarchy. alignments: - index: lawful-good name: Lawful Good url: '/api/2014/alignments/lawful-good' - index: lawful-neutral name: Lawful Neutral url: '/api/2014/alignments/lawful-neutral' - index: lawful-evil name: Lawful Evil url: '/api/2014/alignments/lawful-evil' - option_type: ideal desc: Faith. I trust that my deity will guide my actions. I have faith that if I work hard, things will go well. alignments: - index: lawful-good name: Lawful Good url: '/api/2014/alignments/lawful-good' - index: lawful-neutral name: Lawful Neutral url: '/api/2014/alignments/lawful-neutral' - index: lawful-evil name: Lawful Evil url: '/api/2014/alignments/lawful-evil' - option_type: ideal desc: Aspiration. I seek to prove myself worthy of my god's favor by matching my actions against his or her teachings. alignments: - index: lawful-good name: Lawful Good url: '/api/2014/alignments/lawful-good' - index: neutral-good name: Neutral Good url: '/api/2014/alignments/neutral-good' - index: chaotic-good name: Chaotic Good url: '/api/2014/alignments/chaotic-good' - index: lawful-neutral name: Lawful Neutral url: '/api/2014/alignments/lawful-neutral' - index: neutral name: Neutral url: '/api/2014/alignments/neutral' - index: chaotic-neutral name: Chaotic Neutral url: '/api/2014/alignments/chaotic-neutral' - index: lawful-evil name: Lawful Evil url: '/api/2014/alignments/lawful-evil' - index: neutral-evil name: Neutral Evil url: '/api/2014/alignments/neutral-evil' - index: chaotic-evil name: Chaotic Evil url: '/api/2014/alignments/chaotic-evil' bonds: choose: 1 type: bonds from: option_set_type: options_array options: - option_type: string string: I would die to recover an ancient relic of my faith that was lost long ago. - option_type: string string: I will someday get revenge on the corrupt temple hierarchy who branded me a heretic. - option_type: string string: I owe my life to the priest who took me in when my parents died. - option_type: string string: Everything I do is for the common people. - option_type: string string: I will do anything to protect the temple where I served. - option_type: string string: I seek to preserve a sacred text that my enemies consider heretical and seek to destroy. flaws: choose: 1 type: flaws from: option_set_type: options_array options: - option_type: string string: I judge others harshly, and myself even more severely. - option_type: string string: I put too much trust in those who wield power within my temple's hierarchy. - option_type: string string: My piety sometimes leads me to blindly trust those that profess faith in my god. - option_type: string string: I am inflexible in my thinking. - option_type: string string: I am suspicious of strangers and expect the worst of them. - option_type: string string: Once I pick a goal, I become obsessed with it to the detriment of everything else in my life. url: '/api/2014/backgrounds/acolyte' ================================================ FILE: src/swagger/paths/2014/classes.yml ================================================ # /api/2014/classes/{indexParam} class-path: get: summary: Get a class by index. description: | # Class A character class is a fundamental part of the identity and nature of characters in the Dungeons & Dragons role-playing game. A character's capabilities, strengths, and weaknesses are largely defined by its class. A character's class affects a character's available skills and abilities. [[SRD p8-55](https://media.wizards.com/2016/downloads/DND/SRD-OGL_V5.1.pdf#page=8)] tags: - Class parameters: - $ref: '../../parameters/2014/combined.yml#/class-index' responses: '200': description: OK content: application/json: schema: $ref: '../../schemas/2014/combined.yml#/Class' example: class_levels: '/api/2014/classes/barbarian/levels' hit_die: 12 index: barbarian multi_classing: prerequisites: - ability_score: index: str name: STR url: '/api/2014/ability-scores/str' minimum_score: 13 proficiencies: - index: shields name: Shields url: '/api/2014/proficiencies/shields' - index: simple-weapons name: Simple Weapons url: '/api/2014/proficiencies/simple-weapons' - index: martial-weapons name: Martial Weapons url: '/api/2014/proficiencies/martial-weapons' proficiency_choices: [] name: Barbarian proficiencies: - index: light-armor name: Light Armor url: '/api/2014/proficiencies/light-armor' - index: medium-armor name: Medium Armor url: '/api/2014/proficiencies/medium-armor' - index: shields name: Shields url: '/api/2014/proficiencies/shields' - index: simple-weapons name: Simple Weapons url: '/api/2014/proficiencies/simple-weapons' - index: martial-weapons name: Martial Weapons url: '/api/2014/proficiencies/martial-weapons' proficiency_choices: - desc: Choose two from Animal Handling, Athletics, Intimidation, Nature, Perception, and Survival choose: 2 type: proficiencies from: option_set_type: options_array options: - option_type: reference item: index: skill-animal-handling name: 'Skill: Animal Handling' url: '/api/2014/proficiencies/skill-animal-handling' - option_type: reference item: index: skill-athletics name: 'Skill: Athletics' url: '/api/2014/proficiencies/skill-athletics' - option_type: reference item: index: skill-intimidation name: 'Skill: Intimidation' url: '/api/2014/proficiencies/skill-intimidation' - option_type: reference item: index: skill-nature name: 'Skill: Nature' url: '/api/2014/proficiencies/skill-nature' - option_type: reference item: index: skill-perception name: 'Skill: Perception' url: '/api/2014/proficiencies/skill-perception' - option_type: reference item: index: skill-survival name: 'Skill: Survival' url: '/api/2014/proficiencies/skill-survival' saving_throws: - index: str name: STR url: '/api/2014/ability-scores/str' - index: con name: CON url: '/api/2014/ability-scores/con' starting_equipment: - equipment: index: explorers-pack name: Explorer's Pack url: '/api/2014/equipment/explorers-pack' quantity: 1 - equipment: index: javelin name: Javelin url: '/api/2014/equipment/javelin' quantity: 4 starting_equipment_options: - desc: (a) a greataxe or (b) any martial melee weapon choose: 1 type: equipment from: option_set_type: options_array options: - option_type: counted_reference count: 1 of: index: greataxe name: Greataxe url: '/api/2014/equipment/greataxe' - option_type: choice choice: desc: any martial melee weapon choose: 1 type: equipment from: option_set_type: equipment_category equipment_category: index: martial-melee-weapons name: Martial Melee Weapons url: '/api/2014/equipment-categories/martial-melee-weapons' - desc: (a) two handaxes or (b) any simple weapon choose: 1 type: equipment from: option_set_type: options_array options: - option_type: counted_reference count: 2 of: index: handaxe name: Handaxe url: '/api/2014/equipment/handaxe' - option_type: choice choice: desc: any simple weapon choose: 1 type: equipment from: option_set_type: equipment_category equipment_category: index: simple-weapons name: Simple Weapons url: '/api/2014/equipment-categories/simple-weapons' subclasses: - index: berserker name: Berserker url: '/api/2014/subclasses/berserker' url: '/api/2014/classes/barbarian' # /api/2014/classes/{indexParam}/subclasses class-subclass-path: get: summary: Get subclasses available for a class. tags: - Class Resource Lists parameters: - $ref: '../../parameters/2014/combined.yml#/class-index' responses: '200': description: OK content: application/json: schema: $ref: '../../schemas/2014/combined.yml#/APIReferenceList' example: count: 1 results: - index: berserker name: Berserker url: '/api/2014/subclasses/berserker' #/api/2014/classes/{indexParam}/spells class-spells-path: get: summary: Get spells available for a class. tags: - Class Resource Lists parameters: - $ref: '../../parameters/2014/combined.yml#/class-index' - $ref: '../../parameters/2014/combined.yml#/level-filter' responses: '200': description: OK content: application/json: schema: $ref: '../../schemas/2014/combined.yml#/ClassSpellList' example: count: 2 results: - index: power-word-kill name: Power Word Kill url: '/api/2014/spells/power-word-kill' level: 9 - index: true-polymorph name: True Polymorph url: '/api/2014/spells/true-polymorph' level: 9 # /api/2014/classes/{index}/spellcasting class-spellcasting-path: get: summary: Get spellcasting info for a class. tags: - Class parameters: - $ref: '../../parameters/2014/combined.yml#/class-index' responses: '200': description: OK content: application/json: schema: $ref: '../../schemas/2014/combined.yml#/Spellcasting' example: level: 1 spellcasting_ability: index: cha name: CHA url: '/api/2014/ability-scores/cha' info: - name: Cantrips desc: - You know two cantrips of your choice from the bard spell list. You learn additional bard cantrips of your choice at higher levels, as shown in the Cantrips Known column of the Bard table. - name: Spell Slots desc: - The Bard table shows how many spell slots you have to cast your spells of 1st level and higher. To cast one of these spells, you must expend a slot of the spell's level or higher. You regain all expended spell slots when you finish a long rest. - For example, if you know the 1st-level spell cure wounds and have a 1st-level and a 2nd-level spell slot available, you can cast cure wounds using either slot. - name: Spells Known of 1st Level and Higher desc: - You know four 1st-level spells of your choice from the bard spell list. - The Spells Known column of the Bard table shows when you learn more bard spells of your choice. - Each of these spells must be of a level for which you have spell slots, as shown on the table. For instance, when you reach 3rd level in this class, you can learn one new spell of 1st or 2nd level. - Additionally, when you gain a level in this class, you can choose one of the bard spells you know and replace it with another spell from the bard spell list, which also must be of a level for which you have spell slots. - name: Spellcasting Ability desc: - Charisma is your spellcasting ability for your bard spells. Your magic comes from the heart and soul you pour into the performance of your music or oration. You use your Charisma whenever a spell refers to your spellcasting ability. In addition, you use your Charisma modifier when setting the saving throw DC for a bard spell you cast and when making an attack roll with one. - Spell save DC = 8 + your proficiency bonus + your Charisma modifier. - Spell attack modifier = your proficiency bonus + your Charisma modifier. - name: Ritual Casting desc: - You can cast any bard spell you know as a ritual if that spell has the ritual tag. - name: Spellcasting Focus desc: - You can use a musical instrument (see Equipment) as a spellcasting focus for your bard spells. '404': description: Not found. content: application/json: schema: $ref: '../../schemas/2014/combined.yml#/error-response' example: error: Not found # /api/2014/classes/{index}/features class-features-path: get: summary: Get features available for a class. tags: - Class Resource Lists parameters: - $ref: '../../parameters/2014/combined.yml#/class-index' responses: '200': description: List of features for the class. content: application/json: schema: $ref: '../../schemas/2014/combined.yml#/APIReferenceList' # /api/2014/classes/{index}/proficiencies class-proficiencies-path: get: summary: Get proficiencies available for a class. tags: - Class Resource Lists parameters: - $ref: '../../parameters/2014/combined.yml#/class-index' responses: '200': description: List of proficiencies for the class. content: application/json: schema: $ref: '../../schemas/2014/combined.yml#/APIReferenceList' # /api/2014/classes/{index}/multi-classing: class-multi-classing-path: get: summary: Get multiclassing resource for a class. tags: - Class parameters: - $ref: '../../parameters/2014/combined.yml#/class-index' responses: '200': description: OK content: application/json: schema: $ref: '../../schemas/2014/combined.yml#/Multiclassing' example: prerequisites: - ability_score: index: str name: STR url: '/api/2014/ability-scores/str' minimum_score: 13 proficiencies: - index: shields name: Shields url: '/api/2014/proficiencies/shields' - index: simple-weapons name: Simple Weapons url: '/api/2014/proficiencies/simple-weapons' - index: martial-weapons name: Martial Weapons url: '/api/2014/proficiencies/martial-weapons' proficiency_choices: [] # /api/2014/classes/{index}/levels class-levels-path: get: summary: Get all level resources for a class. tags: - Class Levels parameters: - $ref: '../../parameters/2014/combined.yml#/class-index' - $ref: '../../parameters/2014/combined.yml#/levels-subclass-filter' responses: '200': description: OK content: application/json: schema: type: array items: $ref: '../../schemas/2014/combined.yml#/ClassLevel' # /api/2014/classes/{index}/levels/{class_level} class-level-path: get: summary: Get level resource for a class and level. tags: - Class Levels parameters: - $ref: '../../parameters/2014/combined.yml#/class-index' - $ref: '../../parameters/2014/combined.yml#/class-level' responses: '200': description: OK content: application/json: schema: $ref: '../../schemas/2014/combined.yml#/ClassLevel' example: level: 1 ability_score_bonuses: 0 prof_bonus: 2 features: - index: rage name: Rage url: '/api/2014/features/rage' - index: barbarian-unarmored-defense name: Unarmored Defense url: '/api/2014/features/barbarian-unarmored-defense' class_specific: rage_count: 2 rage_damage_bonus: 2 brutal_critical_dice: 0 index: barbarian-1 class: index: barbarian name: Barbarian url: '/api/2014/classes/barbarian' url: '/api/2014/classes/barbarian/levels/1' # /api/2014/classes/{index}/levels/{class_level}/features: class-level-features-path: get: summary: Get features available to a class at the requested level. tags: - Class Levels parameters: - $ref: '../../parameters/2014/combined.yml#/class-index' - $ref: '../../parameters/2014/combined.yml#/class-level' responses: '200': description: OK content: application/json: schema: $ref: '../../schemas/2014/combined.yml#/APIReferenceList' example: count: 2 results: - index: barbarian-unarmored-defense name: Unarmored Defense url: '/api/2014/features/barbarian-unarmored-defense' - index: rage name: Rage url: '/api/2014/features/rage' # /api/2014/classes/{index}/levels/{spell_level}/spells class-spell-level-spells-path: get: summary: Get spells of the requested level available to the class. tags: - Class Levels parameters: - $ref: '../../parameters/2014/combined.yml#/class-index' - $ref: '../../parameters/2014/combined.yml#/spell-level' responses: '200': description: OK content: application/json: schema: $ref: '../../schemas/2014/combined.yml#/APIReferenceList' example: count: 5 results: - index: dominate-monster name: Dominate Monster url: '/api/2014/spells/dominate-monster' - index: earthquake name: Earthquake url: '/api/2014/spells/earthquake' - index: incendiary-cloud name: Incendiary Cloud url: '/api/2014/spells/incendiary-cloud' - index: power-word-stun name: Power Word Stun url: '/api/2014/spells/power-word-stun' - index: sunburst name: Sunburst url: '/api/2014/spells/sunburst' ================================================ FILE: src/swagger/paths/2014/combined.yml ================================================ base: $ref: './common.yml#/api-base' list: $ref: './common.yml#/resource-list' ability-scores: $ref: './ability-scores.yml' alignments: $ref: './alignments.yml' backgrounds: $ref: './backgrounds.yml' classes: $ref: './classes.yml#/class-path' class-subclass: $ref: './classes.yml#/class-subclass-path' class-spells: $ref: './classes.yml#/class-spells-path' class-spellcasting: $ref: './classes.yml#/class-spellcasting-path' class-features: $ref: './classes.yml#/class-features-path' class-proficiencies: $ref: './classes.yml#/class-proficiencies-path' class-multi-classing: $ref: './classes.yml#/class-multi-classing-path' class-levels: $ref: './classes.yml#/class-levels-path' class-level: $ref: './classes.yml#/class-level-path' class-level-features: $ref: './classes.yml#/class-level-features-path' class-spell-level-spells: $ref: './classes.yml#/class-spell-level-spells-path' conditions: $ref: './conditions.yml' damage-types: $ref: './damage-types.yml' equipment: $ref: './equipment.yml' equipment-categories: $ref: './equipment-categories.yml' feats: $ref: './feats.yml' features: $ref: './features.yml' languages: $ref: './languages.yml' magic-items: $ref: './magic-items.yml' magic-schools: $ref: './magic-schools.yml' monsters: $ref: './monsters.yml#/monster-resource-list' monster: $ref: './monsters.yml#/monster-index' proficiencies: $ref: './proficiencies.yml' races: $ref: './races.yml#/race-path' race-subraces: $ref: './races.yml#/race-subraces-path' race-proficiencies: $ref: './races.yml#/race-proficiencies-path' race-traits: $ref: './races.yml#/race-traits-path' rule-sections: $ref: './rule-sections.yml' rules: $ref: './rules.yml' skills: $ref: './skills.yml' spells: $ref: './spells.yml#/spells-resource-list' spell: $ref: './spells.yml#/spell-by-index' subclasses: $ref: './subclasses.yml#/subclass-path' subclass-features: $ref: './subclasses.yml#/subclass-features-path' subclass-levels: $ref: './subclasses.yml#/subclass-levels-path' subclass-level: $ref: './subclasses.yml#/subclass-level-path' subclass-level-features: $ref: './subclasses.yml#/subclass-level-features-path' subraces: $ref: './subraces.yml#/subraces-path' subrace-proficiencies: $ref: './subraces.yml#/subrace-proficiencies-path' subrace-traits: $ref: './subraces.yml#/subrace-traits-path' traits: $ref: './traits.yml' weapon-properties: $ref: './weapon-properties.yml' ================================================ FILE: src/swagger/paths/2014/common.yml ================================================ api-base: get: summary: Get all resource URLs. description: Making a request to the API's base URL returns an object containing available endpoints. tags: - Common responses: '200': description: OK content: application/json: schema: type: object additionalProperties: type: string example: ability-scores: '/api/2014/ability-scores' alignments: '/api/2014/alignments' backgrounds: '/api/2014/backgrounds' classes: '/api/2014/classes' conditions: '/api/2014/conditions' damage-types: '/api/2014/damage-types' equipment-categories: '/api/2014/equipment-categories' equipment: '/api/2014/equipment' feats: '/api/2014/feats' features: '/api/2014/features' languages: '/api/2014/languages' magic-items: '/api/2014/magic-items' magic-schools: '/api/2014/magic-schools' monsters: '/api/2014/monsters' proficiencies: '/api/2014/proficiencies' races: '/api/2014/races' rules: '/api/2014/rules' rule-sections: '/api/2014/rule-sections' skills: '/api/2014/skills' spells: '/api/2014/spells' subclasses: '/api/2014/subclasses' subraces: '/api/2014/subraces' traits: '/api/2014/traits' weapon-properties: '/api/2014/weapon-properties' resource-list: get: summary: 'Get list of all available resources for an endpoint.' description: | Currently only the [`/spells`](#get-/api/2014/spells) and [`/monsters`](#get-/api/2014/monsters) endpoints support filtering with query parameters. Use of these query parameters is documented under the respective [Spells](#tag--Spells) and [Monsters](#tag--Monsters) sections. tags: - Common parameters: - $ref: '../../parameters/2014/combined.yml#/base-endpoint-index' responses: '200': description: 'OK' content: application/json: schema: $ref: '../../schemas/2014/combined.yml#/APIReferenceList' ================================================ FILE: src/swagger/paths/2014/conditions.yml ================================================ get: summary: 'Get a condition by index.' description: | # Condition A condition alters a creature’s capabilities in a variety of ways and can arise as a result of a spell, a class feature, a monster’s attack, or other effect. Most conditions, such as blinded, are impairments, but a few, such as invisible, can be advantageous. tags: - Game Mechanics parameters: - $ref: '../../parameters/2014/combined.yml#/condition-index' responses: '200': description: OK content: application/json: schema: $ref: '../../schemas/2014/combined.yml#/Condition' example: index: blinded name: Blinded url: '/api/2014/conditions/blinded' desc: - "- A blinded creature can't see and automatically fails any ability check that requires sight." - "- Attack rolls against the creature have advantage, and the creature's attack rolls have disadvantage." ================================================ FILE: src/swagger/paths/2014/damage-types.yml ================================================ get: summary: 'Get a damage type by index.' description: | # Damage type Different attacks, damaging spells, and other harmful effects deal different types of damage. Damage types have no rules of their own, but other rules, such as damage resistance, rely on the types. tags: - Game Mechanics parameters: - $ref: '../../parameters/2014/combined.yml#/damage-type-index' responses: '200': description: 'OK' content: application/json: schema: $ref: '../../schemas/2014/combined.yml#/DamageType' example: index: acid name: Acid url: '/api/2014/damage-types/acid' desc: - "The corrosive spray of a black dragon's breath and the dissolving enzymes secreted by a black pudding deal acid damage." ================================================ FILE: src/swagger/paths/2014/equipment-categories.yml ================================================ get: summary: Get an equipment category by index. description: These are the categories that various equipment fall under. tags: - Equipment parameters: - name: index in: path required: true description: | The `index` of the equipment category score to get. Available values can be found in the resource list for this endpoint. schema: type: string example: 'waterborne-vehicles' responses: '200': description: OK content: application/json: schema: $ref: '../../schemas/2014/combined.yml#/EquipmentCategory' example: index: waterborne-vehicles name: Waterborne Vehicles url: '/api/2014/equipment-categories/waterborne-vehicles' equipment: - index: galley name: Galley url: '/api/2014/equipment/galley' - index: keelboat name: Keelboat url: '/api/2014/equipment/keelboat' - index: longship name: Longship url: '/api/2014/equipment/longship' - index: rowboat name: Rowboat url: '/api/2014/equipment/rowboat' - index: sailing-ship name: Sailing ship url: '/api/2014/equipment/sailing-ship' - index: warship name: Warship url: '/api/2014/equipment/warship' ================================================ FILE: src/swagger/paths/2014/equipment.yml ================================================ get: summary: 'Get an equipment item by index.' description: | # Equipment Opportunities abound to find treasure, equipment, weapons, armor, and more in the dungeons you explore. Normally, you can sell your treasures and trinkets when you return to a town or other settlement, provided that you can find buyers and merchants interested in your loot. tags: - Equipment parameters: - $ref: '../../parameters/2014/combined.yml#/equipment-index' responses: '200': description: OK content: application/json: schema: $ref: '../../schemas/2014/combined.yml#/Equipment' example: category_range: Simple Melee contents: [] cost: quantity: 1 unit: sp damage: damage_dice: 1d4 damage_type: index: bludgeoning name: Bludgeoning url: '/api/2014/damage-types/bludgeoning' desc: [] equipment_category: index: weapon name: Weapon url: '/api/2014/equipment-categories/weapon' index: club name: Club properties: - index: light name: Light url: '/api/2014/weapon-properties/light' - index: monk name: Monk url: '/api/2014/weapon-properties/monk' range: long: normal: 5 special: [] url: '/api/2014/equipment/club' weapon_category: Simple weapon_range: Melee weight: 2 ================================================ FILE: src/swagger/paths/2014/feats.yml ================================================ get: summary: 'Get a feat by index.' description: | # Feat A feat is a boon a character can receive at level up instead of an ability score increase. tags: - Feats parameters: - name: index in: path required: true description: | The `index` of the feat to get. schema: type: string enum: [grappler] responses: '200': description: 'OK' content: application/json: schema: $ref: '../../schemas/2014/combined.yml#/Feat' example: index: grappler name: Grappler url: '/api/2014/feats/grappler' desc: - 'You’ve developed the Skills necessary to hold your own in close--quarters Grappling. You gain the following benefits:' - '- You have advantage on Attack Rolls against a creature you are Grappling.' - '- You can use your action to try to pin a creature Grappled by you. To do so, make another grapple check. If you succeed, you and the creature are both Restrained until the grapple ends.' prerequisites: - ability_score: index: str name: STR url: '/api/2014/ability-scores/str' minimum_score: 13 ================================================ FILE: src/swagger/paths/2014/features.yml ================================================ get: summary: Get a feature by index. description: | # Feature When you gain a new level in a class, you get its features for that level. You don’t, however, receive the class’s starting Equipment, and a few features have additional rules when you’re multiclassing: Channel Divinity, Extra Attack, Unarmored Defense, and Spellcasting. tags: - Features parameters: - $ref: '../../parameters/2014/combined.yml#/feature-index' responses: '200': description: OK content: application/json: schema: $ref: '../../schemas/2014/combined.yml#/Feature' example: index: action-surge-1-use name: 'Action Surge (1 use)' url: '/api/2014/features/action-surge-1-use' class: index: fighter name: Fighter url: '/api/2014/classes/fighter' desc: - Starting at 2nd level, you can push yourself beyond your normal limits for a moment. On your turn, you can take one additional action on top of your regular action and a possible bonus action. - Once you use this feature, you must finish a short or long rest before you can use it again. Starting at 17th level, you can use it twice before a rest, but only once on the same turn. level: 2 prerequisites: [] ================================================ FILE: src/swagger/paths/2014/languages.yml ================================================ get: summary: Get a language by index. description: | # Language Your race indicates the languages your character can speak by default, and your background might give you access to one or more additional languages of your choice. [[SRD p59](https://media.wizards.com/2016/downloads/DND/SRD-OGL_V5.1.pdf#page=59)] tags: - Character Data parameters: - $ref: '../../parameters/2014/combined.yml#/language-index' responses: '200': description: 'OK' content: application/json: schema: $ref: '../../schemas/2014/combined.yml#/Language' example: index: elvish name: Elvish url: '/api/2014/languages/elvish' desc: 'Elvish is fluid, with subtle intonations and intricate grammar. Elven literature is rich and varied, and their songs and poems are famous among other races. Many bards learn their language so they can add Elvish ballads to their repertoires.' type: Standard script: Elvish typical_speakers: - Elves ================================================ FILE: src/swagger/paths/2014/magic-items.yml ================================================ get: summary: Get a magic item by index. description: These are the various magic items you can find in the game. tags: - Equipment parameters: - name: index in: path required: true description: | The `index` of the magic item to get. Available values can be found in the resource list for this endpoint. schema: type: string example: 'adamantine-armor' responses: '200': description: OK content: application/json: schema: $ref: '../../schemas/2014/combined.yml#/MagicItem' example: index: adamantine-armor name: Adamantine Armor url: '/api/2014/magic-items/adamantine-armor' desc: - Armor (medium or heavy, but not hide), uncommon - This suit of armor is reinforced with adamantine, one of the hardest substances in existence. While you're wearing it, any critical hit against you becomes a normal hit. equipment_category: index: armor name: Armor url: '/api/2014/equipment-categories/armor' rarity: name: Uncommon variants: [] variant: false ================================================ FILE: src/swagger/paths/2014/magic-schools.yml ================================================ get: summary: Get a magic school by index. description: | # Magic School Academies of magic group spells into eight categories called schools of magic. Scholars, particularly wizards, apply these categories to all spells, believing that all magic functions in essentially the same way, whether it derives from rigorous study or is bestowed by a deity. tags: - Game Mechanics parameters: - $ref: '../../parameters/2014/combined.yml#/magic-school-index' responses: '200': description: OK content: application/json: schema: $ref: '../../schemas/2014/combined.yml#/MagicSchool' example: index: conjuration name: Conjuration url: '/api/2014/magic-schools/conjuration' desc: "Conjuration spells involve the transportation of objects and creatures from one location to another. Some spells summon creatures or objects to the caster's side, whereas others allow the caster to teleport to another location. Some conjurations create objects or effects out of nothing." ================================================ FILE: src/swagger/paths/2014/monsters.yml ================================================ monster-resource-list: get: summary: 'Get list of monsters with optional filtering' tags: - Monsters parameters: - $ref: '../../parameters/2014/combined.yml#/challenge-rating-filter' responses: '200': description: OK content: application/json: schema: $ref: '../../schemas/2014/combined.yml#/APIReferenceList' monster-index: get: summary: Get monster by index. tags: - Monsters parameters: - $ref: '../../parameters/2014/combined.yml#/monster-index' responses: '200': description: OK content: application/json: schema: $ref: '../../schemas/2014/combined.yml#/Monster' example: index: aboleth name: Aboleth url: '/api/2014/monsters/aboleth' actions: - attacks: [] damage: [] desc: The aboleth makes three tentacle attacks. name: Multiattack multiattack_type: actions actions: - action_name: Tentacle count: 3 type: melee - attack_bonus: 9 attacks: [] damage: - damage_dice: 2d6+5 damage_type: index: bludgeoning name: Bludgeoning url: '/api/2014/damage-types/bludgeoning' - damage_dice: 1d12 damage_type: index: acid name: Acid url: '/api/2014/damage-types/acid' dc: dc_type: index: con name: CON url: '/api/2014/ability-scores/con' dc_value: 14 success_type: none desc: "Melee Weapon Attack: +9 to hit, reach 10 ft., one target. Hit: 12 (2d6 + 5) bludgeoning damage. If the target is a creature, it must succeed on a DC 14 Constitution saving throw or become diseased. The disease has no effect for 1 minute and can be removed by any magic that cures disease. After 1 minute, the diseased creature's skin becomes translucent and slimy, the creature can't regain hit points unless it is underwater, and the disease can be removed only by heal or another disease-curing spell of 6th level or higher. When the creature is outside a body of water, it takes 6 (1d12) acid damage every 10 minutes unless moisture is applied to the skin before 10 minutes have passed." name: Tentacle - attack_bonus: 9 attacks: [] damage: - damage_dice: 3d6+5 damage_type: index: bludgeoning name: Bludgeoning url: '/api/2014/damage-types/bludgeoning' desc: 'Melee Weapon Attack: +9 to hit, reach 10 ft. one target. Hit: 15 (3d6 + 5) bludgeoning damage.' name: Tail - attacks: [] damage: [] dc: dc_type: index: wis name: WIS url: '/api/2014/ability-scores/wis' dc_value: 14 success_type: none desc: |- The aboleth targets one creature it can see within 30 ft. of it. The target must succeed on a DC 14 Wisdom saving throw or be magically charmed by the aboleth until the aboleth dies or until it is on a different plane of existence from the target. The charmed target is under the aboleth's control and can't take reactions, and the aboleth and the target can communicate telepathically with each other over any distance. Whenever the charmed target takes damage, the target can repeat the saving throw. On a success, the effect ends. No more than once every 24 hours, the target can also repeat the saving throw when it is at least 1 mile away from the aboleth. name: Enslave usage: times: 3 type: per day alignment: lawful evil armor_class: - type: natural value: 17 challenge_rating: 10 proficiency_bonus: 4 charisma: 18 condition_immunities: [] constitution: 15 damage_immunities: [] damage_resistances: [] damage_vulnerabilities: [] dexterity: 9 forms: [] hit_dice: 18d10 hit_points: 135 hit_points_roll: 18d10+36 intelligence: 18 languages: Deep Speech, telepathy 120 ft. legendary_actions: - damage: [] desc: The aboleth makes a Wisdom (Perception) check. name: Detect - damage: [] desc: The aboleth makes one tail attack. name: Tail Swipe - attack_bonus: 0 damage: - damage_dice: 3d6 damage_type: index: psychic name: Psychic url: '/api/2014/damage-types/psychic' desc: One creature charmed by the aboleth takes 10 (3d6) psychic damage, and the aboleth regains hit points equal to the damage the creature takes. name: Psychic Drain (Costs 2 Actions) proficiencies: - proficiency: index: saving-throw-con name: 'Saving Throw: CON' url: '/api/2014/proficiencies/saving-throw-con' value: 6 - proficiency: index: saving-throw-int name: 'Saving Throw: INT' url: '/api/2014/proficiencies/saving-throw-int' value: 8 - proficiency: index: saving-throw-wis name: 'Saving Throw: WIS' url: '/api/2014/proficiencies/saving-throw-wis' value: 6 - proficiency: index: skill-history name: 'Skill: History' url: '/api/2014/proficiencies/skill-history' value: 12 - proficiency: index: skill-perception name: 'Skill: Perception' url: '/api/2014/proficiencies/skill-perception' value: 10 reactions: [] senses: darkvision: 120 ft. passive_perception: 20 size: Large special_abilities: - damage: [] desc: The aboleth can breathe air and water. name: Amphibious - damage: [] dc: dc_type: index: con name: CON url: '/api/2014/ability-scores/con' dc_value: 14 success_type: none desc: While underwater, the aboleth is surrounded by transformative mucus. A creature that touches the aboleth or that hits it with a melee attack while within 5 ft. of it must make a DC 14 Constitution saving throw. On a failure, the creature is diseased for 1d4 hours. The diseased creature can breathe only underwater. name: Mucous Cloud - damage: [] desc: If a creature communicates telepathically with the aboleth, the aboleth learns the creature's greatest desires if the aboleth can see the creature. name: Probing Telepathy speed: swim: 40 ft. walk: 10 ft. strength: 21 subtype: type: aberration wisdom: 15 xp: 5900 ================================================ FILE: src/swagger/paths/2014/proficiencies.yml ================================================ get: summary: 'Get a proficiency by index.' description: | # Proficiency By virtue of race, class, and background a character is proficient at using certain skills, weapons, and equipment. Characters can also gain additional proficiencies at higher levels or by multiclassing. A characters starting proficiencies are determined during character creation. tags: - Character Data parameters: - $ref: '../../parameters/2014/combined.yml#/proficiency-index' responses: '200': description: 'OK' content: application/json: schema: $ref: '../../schemas/2014/combined.yml#/Proficiency' example: index: medium-armor name: Medium Armor url: '/api/2014/proficiencies/medium-armor' type: Armor classes: - index: barbarian name: Barbarian url: '/api/2014/classes/barbarian' - index: cleric name: Cleric url: '/api/2014/classes/cleric' - index: druid name: Druid url: '/api/2014/classes/druid' - index: ranger name: Ranger url: '/api/2014/classes/ranger' races: [] reference: index: medium-armor name: Medium Armor url: '/api/2014/equipment-categories/medium-armor' ================================================ FILE: src/swagger/paths/2014/races.yml ================================================ # /api/2014/races/{index} race-path: get: summary: Get a race by index. description: Each race grants your character ability and skill bonuses as well as racial traits. tags: - Races parameters: - $ref: '../../parameters/2014/combined.yml#/race-index' responses: '200': description: OK content: application/json: schema: $ref: '../../schemas/2014/combined.yml#/Race' example: index: elf name: Elf url: '/api/2014/races/elf' ability_bonuses: - ability_score: index: dex name: DEX url: '/api/2014/ability-scores/dex' bonus: 2 age: Although elves reach physical maturity at about the same age as humans, the elven understanding of adulthood goes beyond physical growth to encompass worldly experience. An elf typically claims adulthood and an adult name around the age of 100 and can live to be 750 years old. alignment: Elves love freedom, variety, and self-expression, so they lean strongly toward the gentler aspects of chaos. They value and protect others' freedom as well as their own, and they are more often good than not. The drow are an exception; their exile has made them vicious and dangerous. Drow are more often evil than not. language_desc: You can speak, read, and write Common and Elvish. Elvish is fluid, with subtle intonations and intricate grammar. Elven literature is rich and varied, and their songs and poems are famous among other races. Many bards learn their language so they can add Elvish ballads to their repertoires. languages: - index: common name: Common url: '/api/2014/languages/common' - index: elvish name: Elvish url: '/api/2014/languages/elvish' size: Medium size_description: Elves range from under 5 to over 6 feet tall and have slender builds. Your size is Medium. speed: 30 subraces: - index: high-elf name: High Elf url: '/api/2014/subraces/high-elf' traits: - index: darkvision name: Darkvision url: '/api/2014/traits/darkvision' - index: fey-ancestry name: Fey Ancestry url: '/api/2014/traits/fey-ancestry' - index: trance name: Trance url: '/api/2014/traits/trance' # /api/2014/races/{index}/subraces: race-subraces-path: get: summary: 'Get subraces available for a race.' tags: - Races parameters: - $ref: '../../parameters/2014/combined.yml#/race-index' responses: '200': description: 'List of subraces for the race.' content: application/json: schema: $ref: '../../schemas/2014/combined.yml#/APIReferenceList' # /api/2014/races/{index}/proficiencies: race-proficiencies-path: get: summary: 'Get proficiencies available for a race.' tags: - Races parameters: - $ref: '../../parameters/2014/combined.yml#/race-index' responses: '200': description: 'List of proficiencies for the race.' content: application/json: schema: $ref: '../../schemas/2014/combined.yml#/APIReferenceList' # /api/2014/races/{index}/traits: race-traits-path: get: summary: 'Get traits available for a race.' tags: - Races parameters: - $ref: '../../parameters/2014/combined.yml#/race-index' responses: '200': description: 'List of traits for the race.' content: application/json: schema: $ref: '../../schemas/2014/combined.yml#/APIReferenceList' ================================================ FILE: src/swagger/paths/2014/rule-sections.yml ================================================ get: summary: 'Get a rule section by index.' description: Rule sections represent a sub-heading and text that can be found underneath a rule heading in the SRD. tags: - Rules parameters: - $ref: '../../parameters/2014/combined.yml#/rule-section-index' responses: '200': description: OK content: application/json: schema: $ref: '../../schemas/2014/combined.yml#/RuleSection' example: index: time name: Time url: '/api/2014/rule-sections/time' desc: | ## Time In situations where keeping track of the passage of time is important, the GM determines the time a task requires. The GM might use a different time scale depending on the context of the situation at hand. In a dungeon environment, the adventurers' movement happens on a scale of **minutes**. It takes them about a minute to creep down a long hallway, another minute to check for traps on the door at the end of the hall, and a good ten minutes to search the chamber beyond for anything interesting or valuable. In a city or wilderness, a scale of **hours** is often more appropriate. Adventurers eager to reach the lonely tower at the heart of the forest hurry across those fifteen miles in just under four hours' time. For long journeys, a scale of **days** works best. Following the road from Baldur's Gate to Waterdeep, the adventurers spend four uneventful days before a goblin ambush interrupts their journey. In combat and other fast-paced situations, the game relies on **rounds**, a 6-second span of time. ================================================ FILE: src/swagger/paths/2014/rules.yml ================================================ get: summary: 'Get a rule by index.' description: | # Rule Rules are pages in the SRD that document the mechanics of Dungeons and Dragons. Rules have descriptions which is the text directly underneath the rule heading in the SRD. Rules also have subsections for each heading underneath the rule in the SRD. tags: - Rules parameters: - $ref: '../../parameters/2014/combined.yml#/rule-index' responses: '200': description: OK content: application/json: schema: $ref: '../../schemas/2014/combined.yml#/Rule' example: index: using-ability-scores name: Using Ability Scores url: '/api/2014/rules/using-ability-scores' desc: | # Using Ability Scores Six abilities provide a quick description of every creature's physical and mental characteristics: - **Strength**, measuring physical power - **Dexterity**, measuring agility - **Constitution**, measuring endurance - **Intelligence**, measuring reasoning and memory - **Wisdom**, measuring perception and insight - **Charisma**, measuring force of personality Is a character muscle-bound and insightful? Brilliant and charming? Nimble and hardy? Ability scores define these qualities-a creature's assets as well as weaknesses. The three main rolls of the game-the ability check, the saving throw, and the attack roll-rely on the six ability scores. The book's introduction describes the basic rule behind these rolls: roll a d20, add an ability modifier derived from one of the six ability scores, and compare the total to a target number. **Ability Scores and Modifiers** Each of a creature's abilities has a score, a number that defines the magnitude of that ability. An ability score is not just a measure of innate capabilities, but also encompasses a creature's training and competence in activities related to that ability. A score of 10 or 11 is the normal human average, but adventurers and many monsters are a cut above average in most abilities. A score of 18 is the highest that a person usually reaches. Adventurers can have scores as high as 20, and monsters and divine beings can have scores as high as 30. Each ability also has a modifier, derived from the score and ranging from -5 (for an ability score of 1) to +10 (for a score of 30). The Ability Scores and Modifiers table notes the ability modifiers for the range of possible ability scores, from 1 to 30. subsections: - index: ability-scores-and-modifiers name: Ability Scores and Modifiers url: '/api/2014/rule-sections/ability-scores-and-modifiers' - index: advantage-and-disadvantage name: Advantage and Disadvantage url: '/api/2014/rule-sections/advantage-and-disadvantage' - index: proficiency-bonus name: Proficiency Bonus url: '/api/2014/rule-sections/proficiency-bonus' - index: ability-checks name: Ability Checks url: '/api/2014/rule-sections/ability-checks' - index: using-each-ability name: Using Each Ability url: '/api/2014/rule-sections/using-each-ability' - index: saving-throws name: Saving Throws url: '/api/2014/rule-sections/saving-throws' ================================================ FILE: src/swagger/paths/2014/skills.yml ================================================ get: summary: Get a skill by index. description: | # Skill Each ability covers a broad range of capabilities, including skills that a character or a monster can be proficient in. A skill represents a specific aspect of an ability score, and an individual's proficiency in a skill demonstrates a focus on that aspect. [[SRD p77](https://media.wizards.com/2016/downloads/DND/SRD-OGL_V5.1.pdf#page=77)] tags: - Character Data parameters: - $ref: '../../parameters/2014/combined.yml#/skill-index' responses: '200': description: 'OK' content: application/json: schema: $ref: '../../schemas/2014/combined.yml#/Skill' example: index: acrobatics name: Acrobatics url: '/api/2014/skills/acrobatics' ability_score: index: dex name: DEX url: '/api/2014/ability-scores/dex' desc: - Your Dexterity (Acrobatics) check covers your attempt to stay on your feet in a tricky situation, such as when you're trying to run across a sheet of ice, balance on a tightrope, or stay upright on a rocking ship's deck. The GM might also call for a Dexterity (Acrobatics) check to see if you can perform acrobatic stunts, including dives, rolls, somersaults, and flips. ================================================ FILE: src/swagger/paths/2014/spells.yml ================================================ spells-resource-list: get: summary: 'Get list of spells with optional filtering.' tags: - Spells parameters: - $ref: '../../parameters/2014/combined.yml#/level-filter' - $ref: '../../parameters/2014/combined.yml#/school-filter' responses: '200': description: 'OK' content: application/json: schema: $ref: '../../schemas/2014/combined.yml#/APIReferenceList' spell-by-index: get: summary: Get a spell by index. tags: - Spells parameters: - $ref: '../../parameters/2014/combined.yml#/spell-index' responses: '200': description: OK content: application/json: schema: $ref: '../../schemas/2014/combined.yml#/Spell' example: index: sacred-flame name: Sacred Flame url: '/api/2014/spells/sacred-flame' attack_type: ranged casting_time: 1 action classes: - index: cleric name: Cleric url: '/api/2014/classes/cleric' components: - V - S concentration: false damage: damage_at_character_level: '1': 1d8 '5': 2d8 '11': 3d8 '17': 4d8 damage_type: index: radiant name: Radiant url: '/api/2014/damage-types/radiant' dc: dc_success: none dc_type: index: dex name: DEX url: '/api/2014/ability-scores/dex' desc: - Flame-like radiance descends on a creature that you can see within range. The target must succeed on a dexterity saving throw or take 1d8 radiant damage. The target gains no benefit from cover for this saving throw. - The spell's damage increases by 1d8 when you reach 5th level (2d8), 11th level (3d8), and 17th level (4d8). duration: Instantaneous higher_level: [] level: 0 range: 60 feet ritual: false school: index: evocation name: Evocation url: '/api/2014/magic-schools/evocation' subclasses: - index: lore name: Lore url: '/api/2014/subclasses/lore' ================================================ FILE: src/swagger/paths/2014/subclasses.yml ================================================ # /api/2014/subclass/{index} subclass-path: get: summary: Get a subclass by index. description: Subclasses reflect the different paths a class may take as levels are gained. tags: - Subclasses parameters: - $ref: '../../parameters/2014/combined.yml#/subclass-index' responses: '200': description: OK content: application/json: schema: $ref: '../../schemas/2014/combined.yml#/Subclass' example: index: fiend name: Fiend url: '/api/2014/subclasses/fiend' class: index: warlock name: Warlock url: '/api/2014/classes/warlock' desc: - You have made a pact with a fiend from the lower planes of existence, a being whose aims are evil, even if you strive against those aims. Such beings desire the corruption or destruction of all things, ultimately including you. Fiends powerful enough to forge a pact include demon lords such as Demogorgon, Orcus, Fraz'Urb-luu, and Baphomet; archdevils such as Asmodeus, Dispater, Mephistopheles, and Belial; pit fiends and balors that are especially mighty; and ultroloths and other lords of the yugoloths. spells: - prerequisites: - index: warlock-1 name: Warlock 1 type: level url: '/api/2014/classes/warlock/levels/1' spell: index: burning-hands name: Burning Hands url: '/api/2014/spells/burning-hands' - prerequisites: - index: warlock-1 name: Warlock 1 type: level url: '/api/2014/classes/warlock/levels/1' spell: index: command name: Command url: '/api/2014/spells/command' - prerequisites: - index: warlock-3 name: Warlock 3 type: level url: '/api/2014/classes/warlock/levels/3' spell: index: blindness-deafness name: Blindness/Deafness url: '/api/2014/spells/blindness-deafness' - prerequisites: - index: warlock-3 name: Warlock 3 type: level url: '/api/2014/classes/warlock/levels/3' spell: index: scorching-ray name: Scorching Ray url: '/api/2014/spells/scorching-ray' - prerequisites: - index: warlock-5 name: Warlock 5 type: level url: '/api/2014/classes/warlock/levels/5' spell: index: fireball name: Fireball url: '/api/2014/spells/fireball' - prerequisites: - index: warlock-5 name: Warlock 5 type: level url: '/api/2014/classes/warlock/levels/5' spell: index: stinking-cloud name: Stinking Cloud url: '/api/2014/spells/stinking-cloud' - prerequisites: - index: warlock-7 name: Warlock 7 type: level url: '/api/2014/classes/warlock/levels/7' spell: index: fire-shield name: Fire Shield url: '/api/2014/spells/fire-shield' - prerequisites: - index: warlock-7 name: Warlock 7 type: level url: '/api/2014/classes/warlock/levels/7' spell: index: wall-of-fire name: Wall of Fire url: '/api/2014/spells/wall-of-fire' - prerequisites: - index: warlock-9 name: Warlock 9 type: level url: '/api/2014/classes/warlock/levels/9' spell: index: flame-strike name: Flame Strike url: '/api/2014/spells/flame-strike' - prerequisites: - index: warlock-9 name: Warlock 9 type: level url: '/api/2014/classes/warlock/levels/9' spell: index: hallow name: Hallow url: '/api/2014/spells/hallow' subclass_flavor: Otherworldly Patron subclass_levels: '/api/2014/subclasses/fiend/levels' # /api/2014/subclasses/{index}/features: subclass-features-path: get: summary: 'Get features available for a subclass.' tags: - Subclasses parameters: - $ref: '../../parameters/2014/combined.yml#/subclass-index' responses: '200': description: 'List of features for the subclass.' content: application/json: schema: $ref: '../../schemas/2014/combined.yml#/APIReferenceList' # /api/2014/subclasses/{index}/levels: subclass-levels-path: get: summary: 'Get all level resources for a subclass.' tags: - Subclasses parameters: - $ref: '../../parameters/2014/combined.yml#/subclass-index' responses: '200': description: 'List of level resource for the subclass.' content: application/json: schema: type: array items: $ref: '../../schemas/2014/combined.yml#/SubclassLevelResource' # /api/2014/subclasses/{index}/levels/{subclass_level}: subclass-level-path: get: summary: 'Get level resources for a subclass and level.' tags: - Subclasses parameters: - $ref: '../../parameters/2014/combined.yml#/subclass-index' - name: subclass_level in: path required: true schema: type: integer minimum: 1 maximum: 20 example: 6 responses: '200': description: 'Level resource for the subclass and level.' content: application/json: schema: $ref: '../../schemas/2014/combined.yml#/SubclassLevel' example: index: devotion-3 url: '/api/2014/subclasses/devotion/levels/3' class: index: paladin name: Paladin url: '/api/2014/classes/paladin' features: - index: channel-divinity name: Channel Divinity url: '/api/2014/features/channel-divinity' level: 3 subclass: index: devotion name: Devotion url: '/api/2014/subclasses/devotion' # /api/2014/subclasses/{index}/levels/{subclass_level}/features: subclass-level-features-path: get: summary: 'Get features of the requested spell level available to the class.' tags: - Subclasses parameters: - $ref: '../../parameters/2014/combined.yml#/subclass-index' - name: subclass_level in: path required: true schema: type: integer minimum: 0 maximum: 20 example: 6 responses: '200': description: 'List of features for the subclass and level.' content: application/json: schema: $ref: '../../schemas/2014/combined.yml#/APIReferenceList' ================================================ FILE: src/swagger/paths/2014/subraces.yml ================================================ # /api/2014/subraces/{index} subraces-path: get: summary: Get a subrace by index. description: Subraces reflect the different varieties of a certain parent race. tags: - Subraces parameters: - $ref: '../../parameters/2014/combined.yml#/subrace-index' responses: '200': description: OK content: application/json: schema: $ref: '../../schemas/2014/combined.yml#/Subrace' example: index: hill-dwarf name: Hill Dwarf url: '/api/2014/subraces/hill-dwarf' ability_bonuses: - ability_score: index: wis name: WIS url: '/api/2014/ability-scores/wis' bonus: 1 desc: As a hill dwarf, you have keen senses, deep intuition, and remarkable resilience. race: index: dwarf name: Dwarf url: '/api/2014/races/dwarf' racial_traits: - index: dwarven-toughness name: Dwarven Toughness url: '/api/2014/traits/dwarven-toughness' # /api/2014/subraces/{index}/proficiencies subrace-proficiencies-path: get: summary: 'Get proficiences available for a subrace.' tags: - Subraces parameters: - $ref: '../../parameters/2014/combined.yml#/subrace-index' responses: '200': description: 'List of proficiences for the subrace.' content: application/json: schema: $ref: '../../schemas/2014/combined.yml#/APIReferenceList' # /api/2014/subraces/{index}/traits subrace-traits-path: get: summary: 'Get traits available for a subrace.' tags: - Subraces parameters: - $ref: '../../parameters/2014/combined.yml#/subrace-index' responses: '200': description: 'List of traits for the subrace.' content: application/json: schema: $ref: '../../schemas/2014/combined.yml#/APIReferenceList' ================================================ FILE: src/swagger/paths/2014/traits.yml ================================================ get: summary: 'Get a trait by index.' tags: - Traits parameters: - $ref: '../../parameters/2014/combined.yml#/trait-index' responses: '200': description: OK content: application/json: schema: $ref: '../../schemas/2014/combined.yml#/Trait' example: index: trance name: Trance url: '/api/2014/traits/trance' desc: - Elves do not need to sleep. Instead, they meditate deeply, remaining semiconscious, for 4 hours a day. (The Common word for such meditation is "trance.") While meditating, you can dream after a fashion; such dreams are actually mental exercises that have become reflexive through years of practice. After resting this way, you gain the same benefit that a human does from 8 hours of sleep. proficiencies: [] races: - index: elf name: Elf url: '/api/2014/races/elf' subraces: [] ================================================ FILE: src/swagger/paths/2014/weapon-properties.yml ================================================ get: summary: 'Get a weapon property by index.' tags: - Equipment parameters: - $ref: '../../parameters/2014/combined.yml#/weapon-property-index' responses: '200': description: OK content: application/json: schema: $ref: '../../schemas/2014/combined.yml#/WeaponProperty' example: index: ammunition name: Ammunition url: '/api/2014/weapon-properties/ammunition' desc: - You can use a weapon that has the ammunition property to make a ranged attack only if you have ammunition to fire from the weapon. Each time you attack with the weapon, you expend one piece of ammunition. Drawing the ammunition from a quiver, case, or other container is part of the attack (you need a free hand to load a one-handed weapon). - At the end of the battle, you can recover half your expended ammunition by taking a minute to search the battlefield. If you use a weapon that has the ammunition property to make a melee attack, you treat the weapon as an improvised weapon (see "Improvised Weapons" later in the section). A sling must be loaded to deal any damage when used in this way. ================================================ FILE: src/swagger/paths/2024/.keepme ================================================ ================================================ FILE: src/swagger/schemas/2014/ability-scores.yml ================================================ ability-score-model: description: | `AbilityScore` allOf: - $ref: './combined.yml#/APIReference' - $ref: './combined.yml#/ResourceDescription' - type: object properties: full_name: description: 'Full name of the ability score.' type: string skills: description: 'List of skills that use this ability score.' type: array items: $ref: './combined.yml#/APIReference' AbilityBonus: type: object properties: bonus: description: 'Bonus amount for this ability score.' type: number ability_score: $ref: './combined.yml#/APIReference' ================================================ FILE: src/swagger/schemas/2014/alignments.yml ================================================ description: | `Alignment` allOf: - $ref: './combined.yml#/APIReference' - type: object properties: desc: description: Brief description of the resource. type: string abbreviation: description: Abbreviation/initials/acronym for the alignment. type: string ================================================ FILE: src/swagger/schemas/2014/armor.yml ================================================ description: | `Armor` allOf: - $ref: './combined.yml#/APIReference' - $ref: './combined.yml#/ResourceDescription' - type: object properties: equipment_category: $ref: './combined.yml#/APIReference' armor_category: description: 'The category of armor this falls into.' type: string armor_class: description: 'Details on how to calculate armor class.' type: object additionalProperties: type: string str_minimum: description: 'Minimum STR required to use this armor.' type: number stealth_disadvantage: description: 'Whether the armor gives disadvantage for Stealth.' type: boolean cost: $ref: './combined.yml#/Cost' weight: description: 'How much the equipment weighs.' type: number ================================================ FILE: src/swagger/schemas/2014/backgrounds.yml ================================================ background-model: description: | `Background` allOf: - $ref: './combined.yml#/APIReference' - type: object properties: starting_proficiencies: description: 'Starting proficiencies for all new characters of this background.' type: array items: $ref: './combined.yml#/APIReference' starting_equipment: description: 'Starting equipment for all new characters of this background.' type: array items: type: object properties: quantity: type: number equipment: $ref: './combined.yml#/APIReference' starting_equipment_options: description: List of choices of starting equipment. type: array items: $ref: './combined.yml#/Choice' language_options: $ref: './combined.yml#/Choice' feature: description: Special feature granted to new characters of this background. type: object properties: name: type: string desc: type: array items: type: string personality_traits: $ref: './combined.yml#/Choice' ideals: $ref: './combined.yml#/Choice' bonds: $ref: './combined.yml#/Choice' flaws: $ref: './combined.yml#/Choice' ================================================ FILE: src/swagger/schemas/2014/classes.yml ================================================ # TODO: add descriptions to these class-specific fields. cs-barbarian: description: Barbarian Class Specific Features type: object properties: rage_count: type: number rage_damage_bonus: type: number brutal_critical_dice: type: number cs-bard: description: Bard Class Specific Features type: object properties: bardic_inspiration_dice: type: number song_of_rest_die: type: number magical_secrets_max_5: type: number magical_secrets_max_7: type: number magical_secrets_max_9: type: number cs-cleric: description: Cleric Class Specific Features type: object properties: channel_divinity_charges: type: number destroy_undead_cr: type: number cs-druid: description: Druid Class Specific Features type: object properties: wild_shape_max_cr: type: number wild_shape_swim: type: boolean wild_shape_fly: type: boolean cs-fighter: description: Fighter Class Specific Features type: object properties: action_surges: type: number indomitable_uses: type: number extra_attacks: type: number cs-monk: description: Monk Class Specific Features type: object properties: ki_points: type: number unarmored_movement: type: number martial_arts: type: object properties: dice_count: type: number dice_value: type: number cs-paladin: description: Paladin Class Specific Features type: object properties: aura_range: type: number cs-ranger: description: Bard Ranger Specific Features type: object properties: favored_enemies: type: number favored_terrain: type: number cs-rogue: description: Bard Rogue Specific Features type: object properties: sneak_attack: type: object properties: dice_count: type: number dice_value: type: number cs-sorcerer: description: Bard Sorcerer Specific Features type: object properties: sorcery_points: type: number metamagic_known: type: number creating_spell_slots: type: array items: type: object properties: spell_slot_level: type: number sorcery_point_cost: type: number cs-warlock: description: Bard Warlock Specific Features type: object properties: invocations_known: type: number mystic_arcanum_level_6: type: number mystic_arcanum_level_7: type: number mystic_arcanum_level_8: type: number mystic_arcanum_level_9: type: number cs-wizard: description: Wizard Class Specific Features type: object properties: arcane_recover_levels: type: number class-model: description: | `Class` allOf: - $ref: './combined.yml#/APIReference' - type: object properties: hit_die: description: 'Hit die of the class. (ex: 12 == 1d12).' type: number class_levels: description: URL of the level resource for the class. type: string multi_classing: $ref: './combined.yml#/Multiclassing' spellcasting: $ref: './combined.yml#/Spellcasting' spells: description: URL of the spell resource list for the class. type: string starting_equipment: description: List of equipment and their quantities all players of the class start with. type: array items: type: object properties: quantity: type: number equipment: $ref: './combined.yml#/APIReference' starting_equipment_options: description: List of choices of starting equipment. type: array items: $ref: './combined.yml#/Choice' proficiency_choices: description: List of choices of starting proficiencies. type: array items: $ref: './combined.yml#/Choice' proficiencies: description: List of starting proficiencies for all new characters of this class. type: array items: $ref: './combined.yml#/APIReference' saving_throws: description: Saving throws the class is proficient in. type: array items: $ref: './combined.yml#/APIReference' subclasses: description: List of all possible subclasses this class can specialize in. type: array items: $ref: './combined.yml#/APIReference' class-level-model: description: | `ClassLevel` type: object properties: index: description: 'Resource index for shorthand searching.' type: string url: description: 'URL of the referenced resource.' type: string level: description: 'The number value for the current level object.' type: number ability_score_bonuses: description: 'Total number of ability score bonuses gained, added from previous levels.' type: number prof_bonus: description: 'Proficiency bonus for this class at the specified level.' type: number features: description: 'Features automatically gained at this level.' type: array items: $ref: './combined.yml#/APIReference' spellcasting: description: 'Summary of spells known at this level.' type: object properties: cantrips_known: type: number spells_known: type: number spell_slots_level_1: type: number spell_slots_level_2: type: number spell_slots_level_3: type: number spell_slots_level_4: type: number spell_slots_level_5: type: number spell_slots_level_6: type: number spell_slots_level_7: type: number spell_slots_level_8: type: number spell_slots_level_9: type: number class_specific: description: 'Class specific information such as dice values for bard songs and number of warlock invocations.' anyOf: - $ref: '#/cs-barbarian' - $ref: '#/cs-bard' - $ref: '#/cs-cleric' - $ref: '#/cs-druid' - $ref: '#/cs-fighter' - $ref: '#/cs-monk' - $ref: '#/cs-paladin' - $ref: '#/cs-ranger' - $ref: '#/cs-rogue' - $ref: '#/cs-sorcerer' - $ref: '#/cs-warlock' - $ref: '#/cs-wizard' class-level-spell-model: description: | `ClassLevelSpell` allOf: - $ref: './combined.yml#/APIReference' - type: object properties: level: type: number description: 'The level of the spell slot used to cast the spell.' class-spell-list-model: description: | `ClassSpellList` type: object properties: count: description: 'Total number of resources available.' type: number results: type: array items: $ref: '#/class-level-spell-model' ================================================ FILE: src/swagger/schemas/2014/combined.yml ================================================ APIReference: $ref: './common.yml#/api-ref-model' APIReferenceList: $ref: './common.yml#/api-ref-list-model' Damage: $ref: './common.yml#/damage-model' Choice: $ref: './common.yml#/choice-model' AreaOfEffect: $ref: './common.yml#/area-of-effect-model' Prerequisite: $ref: './common.yml#/prerequisite-model' ResourceDescription: $ref: './common.yml#/resource-description-model' AbilityScore: $ref: './ability-scores.yml#/ability-score-model' Alignment: $ref: './alignments.yml' Class: $ref: './classes.yml#/class-model' Multiclassing: $ref: './multiclassing.yml' Spellcasting: $ref: './spellcasting.yml' Gear: $ref: './equipment.yml#/gear-model' EquipmentPack: $ref: './equipment.yml#/equipment-pack-model' EquipmentCategory: $ref: './equipment.yml#/equipment-category-model' Equipment: $ref: './equipment.yml#/equipment-model' Cost: $ref: './common.yml#/cost-model' Weapon: $ref: './weapon.yml#/weapon-model' Armor: $ref: './armor.yml' MagicItem: $ref: './equipment.yml#/magic-item-model' DamageType: $ref: './game-mechanics.yml#/damage-type-model' DamageAtCharacterLevel: $ref: './spell.yml#/damage-at-character-level-model' DamageAtSlotLevel: $ref: './spell.yml#/damage-at-slot-level-model' Condition: $ref: './game-mechanics.yml#/condition-model' MagicSchool: $ref: './game-mechanics.yml#/magic-school-model' Skill: $ref: './skills.yml' Proficiency: $ref: './proficiencies.yml' Language: $ref: './language.yml' Background: $ref: './backgrounds.yml#/background-model' Feat: $ref: './feats.yml' Subclass: $ref: './subclass.yml#/subclass-model' SubclassLevel: $ref: './subclass.yml#/subclass-level-model' SubclassLevelResource: $ref: './subclass.yml#/subclass-level-resource-model' ClassLevel: $ref: './classes.yml#/class-level-model' Feature: $ref: './features.yml' Race: $ref: './races.yml' AbilityBonus: $ref: './ability-scores.yml#/AbilityBonus' Spell: $ref: './spell.yml#/spell-model' Subrace: $ref: './subrace.yml' Trait: $ref: './traits.yml#/trait' WeaponProperty: $ref: './weapon.yml#/weapon-property-model' Rule: $ref: './rules.yml#/rule-model' RuleSection: $ref: './rules.yml#/rule-section-model' Monster: $ref: './monsters.yml#/monster-model' MonsterAbility: $ref: './monsters-common.yml#/monster-ability-model' MonsterAction: $ref: './monsters-common.yml#/monster-action-model' MonsterArmorClass: $ref: './monsters-common.yml#/monster-armor-class-model' MonsterAttack: $ref: './monsters-common.yml#/monster-attack-model' MonsterMultiAttackAction: $ref: './monsters-common.yml#/monster-multi-attack-action-model' MonsterProficiency: $ref: './monsters-common.yml#/monster-proficiency-model' MonsterSense: $ref: './monsters-common.yml#/monster-sense-model' MonsterSpecialAbility: $ref: './monsters-common.yml#/monster-special-ability-model' MonsterSpell: $ref: './monsters-common.yml#/monster-spell-model' MonsterSpellcasting: $ref: './monsters-common.yml#/monster-spellcasting-model' MonsterUsage: $ref: './monsters-common.yml#/monster-usage-model' SpellPrerequisite: $ref: './subclass.yml#/spell-prerequisite' error-response: $ref: './common.yml#/error-response-model' DC: $ref: './common.yml#/dc-model' OptionSet: $ref: './common.yml#/option-set-model' Option: $ref: './common.yml#/option-model' ClassLevelSpell: $ref: './classes.yml#/class-level-spell-model' ClassSpellList: $ref: './classes.yml#/class-spell-list-model' ================================================ FILE: src/swagger/schemas/2014/common.yml ================================================ api-ref-model: description: | `APIReference` type: object properties: index: description: Resource index for shorthand searching. type: string name: description: 'Name of the referenced resource.' type: string url: description: 'URL of the referenced resource.' type: string updated_at: description: 'Date and time the resource was last updated.' type: string api-ref-list-model: description: | `APIReferenceList` type: object properties: count: description: 'Total number of resources available.' type: number results: type: array items: $ref: './combined.yml#/APIReference' damage-model: description: | `Damage` type: object properties: damage_dice: type: string damage_type: $ref: './combined.yml#/APIReference' dc-model: description: | `DC` type: object properties: dc_type: $ref: './combined.yml#/APIReference' dc_value: description: 'Value to beat' type: number success_type: description: 'Result of a successful save. Can be \"none\", \"half\", or \"other\"' type: string option-model: description: | `Option` oneOf: - type: object properties: option_type: description: 'Type of option; determines other attributes.' type: string item: $ref: './combined.yml#/APIReference' - type: object properties: option_type: description: 'Type of option; determines other attributes.' type: string action_name: description: 'The name of the action.' type: string count: description: 'The number of times this action can be repeated if chosen.' type: number type: description: 'For attack options that can be melee, ranged, abilities, or thrown.' type: string enum: [melee, ranged, ability, magic] - type: object properties: option_type: description: 'Type of option; determines other attributes.' type: string items: type: array items: $ref: './combined.yml#/Option' - type: object properties: option_type: description: 'Type of option; determines other attributes.' type: string choice: $ref: './combined.yml#/Choice' - type: object properties: option_type: description: 'Type of option; determines other attributes.' type: string string: description: 'The string.' type: string - type: object properties: option_type: description: 'Type of option; determines other attributes.' type: string desc: description: 'A description of the ideal.' type: string alignments: description: 'A list of alignments of those who might follow the ideal.' type: array items: $ref: './combined.yml#/APIReference' - type: object properties: option_type: description: 'Type of option; determines other attributes.' type: string count: description: 'Count' type: number of: $ref: './combined.yml#/APIReference' - type: object properties: option_type: description: 'Type of option; determines other attributes.' type: string ability_score: $ref: './combined.yml#/APIReference' minimum_score: description: 'The minimum score required to satisfy the prerequisite.' type: number - type: object properties: option_type: description: 'Type of option; determines other attributes.' type: string ability_score: $ref: './combined.yml#/APIReference' bonus: description: 'The bonus being applied to the ability score' type: number - type: object properties: option_type: description: 'Type of option; determines other attributes.' type: string name: description: 'Name of the breath' type: string dc: $ref: './combined.yml#/DC' damage: description: 'Damage dealt by the breath attack, if any.' type: array items: $ref: './combined.yml#/Damage' - type: object properties: option_type: description: 'Type of option; determines other attributes.' type: string damage_type: $ref: './combined.yml#/APIReference' damage_dice: description: 'Damage expressed in dice (e.g. "13d6").' type: string notes: description: 'Information regarding the damage.' type: string option-set-model: description: | `Option Set` oneOf: - type: object properties: option_set_type: description: 'Type of option set; determines other attributes.' type: string options_array: description: 'Array of options to choose from.' type: array items: $ref: './combined.yml#/Option' - type: object properties: option_set_type: description: 'Type of option set; determines other attributes.' type: string equipment_category: $ref: './combined.yml#/APIReference' - type: object properties: option_set_type: description: 'Type of option set; determines other attributes.' type: string resource_list_url: description: 'A reference (by URL) to a collection in the database.' type: string choice-model: description: | `Choice` type: object properties: desc: description: 'Description of the choice to be made.' type: string choose: description: 'Number of items to pick from the list.' type: number type: description: 'Type of the resources to choose from.' type: string from: $ref: './combined.yml#/OptionSet' cost-model: description: | `Cost` type: object properties: quantity: description: 'Numerical amount of coins.' type: number unit: description: 'Unit of coinage.' type: string prerequisite-model: description: | `Prerequisite` type: object properties: ability_score: allOf: - $ref: './combined.yml#/APIReference' minimum_score: description: 'Minimum score to meet the prerequisite.' type: number resource-description-model: type: object properties: desc: description: 'Description of the resource.' type: array items: type: string error-response-model: type: object properties: error: type: string required: - error area-of-effect-model: type: object properties: size: type: number type: type: string enum: [sphere, cone, cylinder, line, cube] ================================================ FILE: src/swagger/schemas/2014/equipment.yml ================================================ equipment-model: description: | `Equipment` anyOf: - $ref: './combined.yml#/Weapon' - $ref: './combined.yml#/Armor' - $ref: './combined.yml#/Gear' - $ref: './combined.yml#/EquipmentPack' equipment-category-model: description: | `EquipmentCategory` allOf: - $ref: './combined.yml#/APIReference' - type: object properties: image: description: 'The image url of the magic item.' type: string equipment: description: 'A list of the equipment that falls into this category.' type: array items: $ref: './combined.yml#/APIReference' gear-model: description: | `Gear` allOf: - $ref: './combined.yml#/APIReference' - $ref: './combined.yml#/ResourceDescription' - type: object properties: image: description: 'The image url of the gear.' type: string equipment_category: $ref: './combined.yml#/APIReference' gear_category: $ref: './combined.yml#/APIReference' cost: $ref: './combined.yml#/Cost' weight: description: 'How much the equipment weighs.' type: number equipment-pack-model: description: | `EquipmentPack` allOf: - $ref: './combined.yml#/APIReference' - $ref: './combined.yml#/ResourceDescription' - type: object properties: image: description: 'The image url of the equipment pack.' type: string equipment_category: $ref: './combined.yml#/APIReference' gear_category: $ref: './combined.yml#/APIReference' cost: $ref: './combined.yml#/Cost' contents: description: 'The list of adventuring gear in the pack.' type: array items: $ref: './combined.yml#/APIReference' magic-item-model: description: | `MagicItem` allOf: - $ref: './combined.yml#/APIReference' - $ref: './combined.yml#/ResourceDescription' - type: object properties: image: description: 'The image url of the magic item.' type: string equipment_category: $ref: './combined.yml#/APIReference' rarity: type: object properties: name: description: 'The rarity of the item.' type: string enum: - Varies - Common - Uncommon - Rare - Very Rare - Legendary - Artifact variants: type: array items: $ref: './combined.yml#/APIReference' variant: description: 'Whether this is a variant or not' type: boolean ================================================ FILE: src/swagger/schemas/2014/feats.yml ================================================ description: | `Feat` allOf: - $ref: './combined.yml#/APIReference' - $ref: './combined.yml#/ResourceDescription' - type: object properties: prerequisites: description: 'An object of APIReferences to ability scores and minimum scores.' type: array items: $ref: './combined.yml#/Prerequisite' ================================================ FILE: src/swagger/schemas/2014/features.yml ================================================ description: | `Feature` allOf: - $ref: './combined.yml#/APIReference' - $ref: './combined.yml#/ResourceDescription' - type: object properties: level: description: 'The level this feature is gained.' type: number class: $ref: './combined.yml#/APIReference' subclass: $ref: './combined.yml#/APIReference' parent: $ref: './combined.yml#/APIReference' prerequisites: description: 'The prerequisites for this feature.' type: array items: anyOf: - type: object properties: type: type: string level: type: number - type: object properties: type: type: string feature: type: string - type: object properties: type: type: string spell: type: string example: - type: 'level' level: 3 - type: 'feature' feature: 'martial-archetype' - type: 'spell' spell: 'shield' feature_specific: description: 'Information specific to this feature.' type: object properties: subfeature_options: $ref: './combined.yml#/Choice' expertise_options: $ref: './combined.yml#/Choice' terrain_type_options: $ref: './combined.yml#/Choice' enemy_type_options: $ref: './combined.yml#/Choice' invocations: type: array items: $ref: './combined.yml#/APIReference' additionalProperties: true ================================================ FILE: src/swagger/schemas/2014/game-mechanics.yml ================================================ damage-type-model: description: | `DamageType` allOf: - $ref: './combined.yml#/APIReference' - $ref: './combined.yml#/ResourceDescription' condition-model: description: | `Condition` allOf: - $ref: './combined.yml#/APIReference' - $ref: './combined.yml#/ResourceDescription' magic-school-model: description: | `MagicSchool` allOf: - $ref: './combined.yml#/APIReference' - type: object properties: desc: description: Brief description of the resource. type: string ================================================ FILE: src/swagger/schemas/2014/language.yml ================================================ description: | `Language` allOf: - $ref: './combined.yml#/APIReference' - type: object properties: desc: description: 'Brief description of the language.' type: string type: type: string enum: [Standard, Exotic] script: description: 'Script used for writing in the language.' type: string typical_speakers: description: 'List of races that tend to speak the language.' type: array items: type: string ================================================ FILE: src/swagger/schemas/2014/monsters-common.yml ================================================ monster-attack-model: type: object properties: name: type: string dc: $ref: './combined.yml#/DC' damage: $ref: './combined.yml#/Damage' monster-ability-model: description: | `Monster Ability` type: object properties: charisma: description: "A monster's ability to charm or intimidate a player." type: number constitution: description: How sturdy a monster is." type: number dexterity: description: "The monster's ability for swift movement or stealth" type: number intelligence: description: "The monster's ability to outsmart a player." type: number strength: description: 'How hard a monster can hit a player.' type: number wisdom: description: "A monster's ability to ascertain the player's plan." type: number monster-action-model: description: Action available to a `Monster` in addition to the standard creature actions. type: object properties: name: type: string desc: type: string action_options: $ref: './combined.yml#/Choice' actions: type: array items: $ref: './combined.yml#/MonsterMultiAttackAction' options: $ref: './combined.yml#/Choice' multiattack_type: type: string attack_bonus: type: number dc: $ref: './combined.yml#/DC' attacks: type: array items: $ref: './combined.yml#/MonsterAttack' damage: type: array items: $ref: './combined.yml#/Damage' monster-armor-class-model: description: The armor class of a monster. type: object oneOf: - type: object properties: type: type: string enum: [dex] value: type: number desc: type: string - type: object properties: type: type: string enum: [natural] value: type: number desc: type: string - type: object properties: type: type: string enum: [armor] value: type: number armor: type: array items: $ref: './combined.yml#/APIReference' desc: type: string - type: object properties: type: type: string enum: [spell] value: type: number spell: $ref: './combined.yml#/APIReference' desc: type: string - type: object properties: type: type: string enum: [condition] value: type: number condition: $ref: './combined.yml#/APIReference' desc: type: string monster-multi-attack-action-model: type: object properties: action_name: type: string count: type: number type: type: string enum: [melee, ranged, ability, magic] monster-proficiency-model: type: object properties: value: type: number proficiency: $ref: './combined.yml#/APIReference' monster-sense-model: type: object properties: passive_perception: description: The monster's passive perception (wisdom) score. type: number blindsight: description: A monster with blindsight can perceive its surroundings without relying on sight, within a specific radius. type: string darkvision: description: A monster with darkvision can see in the dark within a specific radius. type: string tremorsense: description: A monster with tremorsense can detect and pinpoint the origin of vibrations within a specific radius, provided that the monster and the source of the vibrations are in contact with the same ground or substance. type: string truesight: description: A monster with truesight can, out to a specific range, see in normal and magical darkness, see invisible creatures and objects, automatically detect visual illusions and succeed on saving throws against them, and perceive the original form of a shapechanger or a creature that is transformed by magic. Furthermore, the monster can see into the Ethereal Plane within the same range. type: string monster-special-ability-model: type: object properties: name: type: string desc: type: string attack_bonus: type: number damage: $ref: './combined.yml#/Damage' dc: $ref: './combined.yml#/DC' spellcasting: $ref: './combined.yml#/MonsterSpellcasting' usage: $ref: './combined.yml#/MonsterUsage' monster-spell-model: type: object properties: name: type: string level: type: number url: type: string usage: $ref: './combined.yml#/MonsterUsage' monster-spellcasting-model: type: object properties: ability: $ref: './combined.yml#/APIReference' dc: type: number modifier: type: number components_required: type: array items: type: string school: type: string slots: type: object additionalProperties: type: number spells: type: array items: $ref: './combined.yml#/MonsterSpell' monster-usage-model: type: object properties: type: type: string enum: - at will - per day - recharge after rest - recharge on roll rest_types: type: array items: type: string times: type: number ================================================ FILE: src/swagger/schemas/2014/monsters.yml ================================================ monster-model: description: | `Monster` allOf: - $ref: './combined.yml#/APIReference' - $ref: './combined.yml#/ResourceDescription' - $ref: './combined.yml#/MonsterAbility' - type: object properties: image: description: 'The image url of the monster.' type: string size: description: The size of the monster ranging from Tiny to Gargantuan." type: string enum: - Tiny - Small - Medium - Large - Huge - Gargantuan type: description: 'The type of monster.' type: string subtype: description: The sub-category of a monster used for classification of monsters." type: string alignment: description: "A creature's general moral and personal attitudes." type: string armor_class: description: 'The difficulty for a player to successfully deal damage to a monster.' type: array items: $ref: './combined.yml#/MonsterArmorClass' hit_points: description: 'The hit points of a monster determine how much damage it is able to take before it can be defeated.' type: number hit_dice: description: 'The hit die of a monster can be used to make a version of the same monster whose hit points are determined by the roll of the die. For example: A monster with 2d6 would have its hit points determine by rolling a 6 sided die twice.' type: string hit_points_roll: description: "The roll for determining a monster's hit points, which consists of the hit dice (e.g. 18d10) and the modifier determined by its Constitution (e.g. +36). For example, 18d10+36" type: string actions: description: 'A list of actions that are available to the monster to take during combat.' type: array items: $ref: './combined.yml#/MonsterAction' legendary_actions: description: 'A list of legendary actions that are available to the monster to take during combat.' type: array items: $ref: './combined.yml#/MonsterAction' challenge_rating: description: "A monster's challenge rating is a guideline number that says when a monster becomes an appropriate challenge against the party's average level. For example. A group of 4 players with an average level of 4 would have an appropriate combat challenge against a monster with a challenge rating of 4 but a monster with a challenge rating of 8 against the same group of players would pose a significant threat." type: number minimum: 0 maximum: 21 proficiency_bonus: description: "A monster's proficiency bonus is the number added to ability checks, saving throws and attack rolls in which the monster is proficient, and is linked to the monster's challenge rating. This bonus has already been included in the monster's stats where applicable." type: number minimum: 2 maximum: 9 condition_immunities: description: 'A list of conditions that a monster is immune to.' type: array items: $ref: './combined.yml#/APIReference' damage_immunities: description: 'A list of damage types that a monster will take double damage from.' type: array items: type: string damage_resistances: description: 'A list of damage types that a monster will take half damage from.' type: array items: type: string damage_vulnerabilities: description: 'A list of damage types that a monster will take double damage from.' type: array items: type: string forms: description: 'List of other related monster entries that are of the same form. Only applicable to Lycanthropes that have multiple forms.' type: array items: $ref: './combined.yml#/APIReference' languages: description: 'The languages a monster is able to speak.' type: string proficiencies: description: 'A list of proficiencies of a monster.' type: array items: $ref: './combined.yml#/MonsterProficiency' reactions: description: 'A list of reactions that is available to the monster to take during combat.' type: array items: $ref: './combined.yml#/MonsterAction' senses: description: 'Monsters typically have a passive perception but they might also have other senses to detect players.' allOf: - $ref: './combined.yml#/MonsterSense' special_abilities: description: "A list of the monster's special abilities." type: array items: $ref: './combined.yml#/MonsterSpecialAbility' speed: description: 'Speed for a monster determines how fast it can move per turn.' type: object properties: walk: description: All creatures have a walking speed, simply called the monster’s speed. Creatures that have no form of ground-based locomotion have a walking speed of 0 feet. type: string burrow: description: A monster that has a burrowing speed can use that speed to move through sand, earth, mud, or ice. A monster can’t burrow through solid rock unless it has a special trait that allows it to do so. type: string climb: description: A monster that has a climbing speed can use all or part of its movement to move on vertical surfaces. The monster doesn’t need to spend extra movement to climb. type: string fly: description: A monster that has a flying speed can use all or part of its movement to fly. type: string swim: description: A monster that has a swimming speed doesn’t need to spend extra movement to swim. type: string xp: description: The number of experience points (XP) a monster is worth is based on its challenge rating. type: number ================================================ FILE: src/swagger/schemas/2014/multiclassing.yml ================================================ description: | `Multiclassing` type: object properties: prerequisites: description: List of prerequisites that must be met. type: array items: $ref: './combined.yml#/Prerequisite' prerequisite_options: description: List of choices of prerequisites to meet for. type: array items: $ref: './combined.yml#/Choice' proficiencies: description: 'List of proficiencies available when multiclassing.' type: array items: $ref: './combined.yml#/APIReference' proficiency_choices: description: List of choices of proficiencies that are given when multiclassing. type: array items: $ref: './combined.yml#/Choice' ================================================ FILE: src/swagger/schemas/2014/proficiencies.yml ================================================ description: | `Proficiency` allOf: - $ref: './combined.yml#/APIReference' - type: object properties: type: description: The general category of the proficiency. type: string classes: description: Classes that start with this proficiency. type: array items: $ref: './combined.yml#/APIReference' races: description: Races that start with this proficiency. type: array items: $ref: './combined.yml#/APIReference' reference: description: | `APIReference` to the full description of the related resource. allOf: - $ref: './combined.yml#/APIReference' ================================================ FILE: src/swagger/schemas/2014/races.yml ================================================ description: | `Race` allOf: - $ref: './combined.yml#/APIReference' - type: object properties: speed: description: 'Base move speed for this race (in feet per round).' type: number ability_bonuses: description: 'Racial bonuses to ability scores.' type: array items: $ref: './combined.yml#/AbilityBonus' alignment: description: 'Flavor description of likely alignments this race takes.' type: string age: description: 'Flavor description of possible ages for this race.' type: string size: description: 'Size class of this race.' type: string size_description: description: 'Flavor description of height and weight for this race.' type: string languages: description: 'Starting languages for all new characters of this race.' type: array items: $ref: './combined.yml#/APIReference' language_desc: description: 'Flavor description of the languages this race knows.' type: string traits: description: 'Racial traits that provide benefits to its members.' type: array items: $ref: './combined.yml#/APIReference' subraces: description: 'All possible subraces that this race includes.' type: array items: $ref: './combined.yml#/APIReference' ================================================ FILE: src/swagger/schemas/2014/rules.yml ================================================ rule-model: description: | `Rule` allOf: - $ref: './combined.yml#/APIReference' - type: object properties: desc: description: Description of the rule. type: string subsections: description: 'List of sections for each subheading underneath the rule in the SRD.' type: array items: $ref: './combined.yml#/APIReference' rule-section-model: description: | `RuleSection` allOf: - $ref: './combined.yml#/APIReference' - type: object properties: desc: description: Description of the rule. type: string ================================================ FILE: src/swagger/schemas/2014/skills.yml ================================================ description: | `Skill` allOf: - $ref: './combined.yml#/APIReference' - $ref: './combined.yml#/ResourceDescription' - type: object properties: ability_score: $ref: './combined.yml#/APIReference' ================================================ FILE: src/swagger/schemas/2014/spell.yml ================================================ spell-model: description: | `Spell` allOf: - $ref: './combined.yml#/APIReference' - $ref: './combined.yml#/ResourceDescription' - type: object properties: higher_level: description: 'List of descriptions for casting the spell at higher levels.' type: array items: type: string range: description: 'Range of the spell, usually expressed in feet.' type: string components: description: | List of shorthand for required components of the spell. V: verbal S: somatic M: material type: array items: type: string enum: [V, S, M] material: description: 'Material component for the spell to be cast.' type: string area_of_effect: $ref: './combined.yml#/AreaOfEffect' ritual: description: 'Determines if a spell can be cast in a 10-min(in-game) ritual.' type: boolean duration: description: 'How long the spell effect lasts.' type: string concentration: description: 'Determines if a spell needs concentration to persist.' type: boolean casting_time: description: 'How long it takes for the spell to activate.' type: string level: description: 'Level of the spell.' type: number attack_type: description: 'Attack type of the spell.' type: string damage: oneOf: - $ref: './combined.yml#/DamageAtCharacterLevel' - $ref: './combined.yml#/DamageAtSlotLevel' school: description: 'Magic school this spell belongs to.' $ref: './combined.yml#/APIReference' classes: description: 'List of classes that are able to learn the spell.' type: array items: $ref: './combined.yml#/APIReference' subclasses: description: 'List of subclasses that have access to the spell.' type: array items: $ref: './combined.yml#/APIReference' damage-at-character-level-model: description: | 'Spell Damage scaling by character level' type: object properties: damage_at_character_level: description: 'Damage at each character level, keyed by level number.' type: object additionalProperties: type: string damage_type: $ref: './combined.yml#/APIReference' damage-at-slot-level-model: description: | 'Spell Damage scaling by spell slot level' type: object properties: damage_at_slot_level: description: 'Damage at each spell slot level, keyed by slot level number.' type: object additionalProperties: type: string damage_type: $ref: './combined.yml#/APIReference' ================================================ FILE: src/swagger/schemas/2014/spellcasting.yml ================================================ type: object description: | `Spellcasting` properties: level: description: Level at which the class can start using its spellcasting abilities. type: number info: description: "Descriptions of the class' ability to cast spells." type: array items: type: object properties: name: description: Feature name. type: string desc: description: Feature description. type: array items: type: string spellcasting_ability: description: Reference to the `AbilityScore` used for spellcasting by the class. allOf: - $ref: './combined.yml#/APIReference' ================================================ FILE: src/swagger/schemas/2014/subclass.yml ================================================ spell-prerequisite: description: | `SpellPrerequisite` allOf: - $ref: './combined.yml#/APIReference' - type: object properties: type: description: The type of prerequisite. type: string subclass-model: description: | `Subclass` allOf: - $ref: './combined.yml#/APIReference' - $ref: './combined.yml#/ResourceDescription' - type: object properties: class: $ref: './combined.yml#/APIReference' subclass_flavor: description: Lore-friendly flavor text for a classes respective subclass. type: string subclass_levels: description: Resource url that shows the subclass level progression. type: string spells: type: array items: type: object properties: prerequisites: type: array items: $ref: './combined.yml#/SpellPrerequisite' spell: $ref: './combined.yml#/APIReference' subclass-level-resource-model: type: object properties: index: type: string url: type: string level: type: number features: type: array items: $ref: './combined.yml#/APIReference' class: $ref: './combined.yml#/APIReference' subclass: $ref: './combined.yml#/APIReference' subclass-level-model: description: | `SubclassLevel` type: object properties: index: description: 'Resource index for shorthand searching.' type: string url: description: 'URL of the referenced resource.' type: string level: description: 'Number value for the current level object.' type: number ability_score_bonuses: description: 'Total number of ability score bonuses gained, added from previous levels.' type: number prof_bonus: description: 'Proficiency bonus for this class at the specified level.' type: number features: description: List of features gained at this level. type: array items: $ref: './combined.yml#/APIReference' spellcasting: description: 'Summary of spells known at this level.' type: object properties: cantrips_known: type: number spells_known: type: number spell_slots_level_1: type: number spell_slots_level_2: type: number spell_slots_level_3: type: number spell_slots_level_4: type: number spell_slots_level_5: type: number spell_slots_level_6: type: number spell_slots_level_7: type: number spell_slots_level_8: type: number spell_slots_level_9: type: number classspecific: description: 'Class specific information such as dice values for bard songs and number of warlock invocations.' additionalProperties: {} ================================================ FILE: src/swagger/schemas/2014/subrace.yml ================================================ description: | `Subrace` allOf: - $ref: './combined.yml#/APIReference' - type: object properties: desc: description: Description of the subrace. type: string race: description: 'Parent race for the subrace.' allOf: - $ref: './combined.yml#/APIReference' ability_bonuses: description: 'Additional ability bonuses for the subrace.' type: array items: $ref: './combined.yml#/AbilityBonus' racial_traits: description: 'List of traits that for the subrace.' type: array items: $ref: './combined.yml#/APIReference' ================================================ FILE: src/swagger/schemas/2014/traits.yml ================================================ trait-damage: type: object properties: damage-type: description: A damage type associated with this trait. allOf: - $ref: './combined.yml#/APIReference' breath-weapon: description: The breath weapon action associated with a draconic ancestry. type: object properties: name: type: string desc: type: string area_of_effect: $ref: './combined.yml#/AreaOfEffect' damage: type: object properties: damage_at_character_level: type: object additionalProperties: type: string damage_type: allOf: - $ref: './combined.yml#/APIReference' dc: $ref: './combined.yml#/DC' usage: description: Description of the usage constraints of this action. type: object properties: times: type: number type: type: string trait: description: | `Trait` allOf: - $ref: './combined.yml#/APIReference' - $ref: './combined.yml#/ResourceDescription' - type: object properties: races: description: 'List of `Races` that have access to the trait.' type: array items: $ref: './combined.yml#/APIReference' subraces: description: 'List of `Subraces` that have access to the trait.' type: array items: $ref: './combined.yml#/APIReference' proficiencies: description: 'List of `Proficiencies` this trait grants.' type: array items: $ref: './combined.yml#/APIReference' proficiency_choices: $ref: './combined.yml#/Choice' language_options: $ref: './combined.yml#/Choice' trait_specific: description: 'Information specific to this trait' oneOf: - $ref: './combined.yml#/Choice' - $ref: './combined.yml#/Choice' - $ref: '#/trait-damage' ================================================ FILE: src/swagger/schemas/2014/weapon.yml ================================================ weapon-model: description: | `Weapon` allOf: - $ref: './combined.yml#/APIReference' - $ref: './combined.yml#/ResourceDescription' - type: object properties: equipment_category: $ref: './combined.yml#/APIReference' weapon_category: description: 'The category of weapon this falls into.' type: string weapon_range: description: 'Whether this is a Melee or Ranged weapon.' type: string category_range: description: 'A combination of weapon_category and weapon_range.' type: string range: type: object properties: normal: description: "The weapon's normal range in feet." type: number long: description: "The weapon's long range in feet." type: number damage: $ref: './combined.yml#/Damage' two_handed_damage: $ref: './combined.yml#/Damage' properties: description: 'A list of the properties this weapon has.' type: array items: $ref: './combined.yml#/APIReference' cost: $ref: './combined.yml#/Cost' weight: description: 'How much the equipment weighs.' type: number weapon-property-model: description: WeaponProperty allOf: - $ref: './combined.yml#/APIReference' - $ref: './combined.yml#/ResourceDescription' ================================================ FILE: src/swagger/schemas/2024/.keepme ================================================ ================================================ FILE: src/swagger/swagger.yml ================================================ openapi: 3.0.1 info: title: D&D 5e API description: | # Introduction Welcome to the dnd5eapi, the Dungeons & Dragons 5th Edition API! This documentation should help you familiarize yourself with the resources available and how to consume them with HTTP requests. Read through the getting started section before you dive in. Most of your problems should be solved just by reading through it. ## Getting Started Let's make our first API request to the D&D 5th Edition API! Open up a terminal and use [curl](http://curl.haxx.se/) or [httpie](http://httpie.org/) to make an API request for a resource. You can also scroll through the definitions below and send requests directly from the endpoint documentation! For example, if you paste and run this `curl` command: ```bash curl -X GET "https://www.dnd5eapi.co/api/2014/ability-scores/cha" -H "Accept: application/json" ``` We should see a result containing details about the Charisma ability score: ```bash { "index": "cha", "name": "CHA", "full_name": "Charisma", "desc": [ "Charisma measures your ability to interact effectively with others. It includes such factors as confidence and eloquence, and it can represent a charming or commanding personality.", "A Charisma check might arise when you try to influence or entertain others, when you try to make an impression or tell a convincing lie, or when you are navigating a tricky social situation. The Deception, Intimidation, Performance, and Persuasion skills reflect aptitude in certain kinds of Charisma checks." ], "skills": [ { "name": "Deception", "index": "deception", "url": "/api/2014/skills/deception" }, { "name": "Intimidation", "index": "intimidation", "url": "/api/2014/skills/intimidation" }, { "name": "Performance", "index": "performance", "url": "/api/2014/skills/performance" }, { "name": "Persuasion", "index": "persuasion", "url": "/api/2014/skills/persuasion" } ], "url": "/api/2014/ability-scores/cha" } ``` ## Authentication The dnd5eapi is a completely open API. No authentication is required to query and get data. This also means that we've limited what you can do to just `GET`-ing the data. If you find a mistake in the data, feel free to [message us](https://discord.gg/TQuYTv7). ## GraphQL This API supports [GraphQL](https://graphql.org/). The GraphQL URL for this API is `https://www.dnd5eapi.co/graphql`. Most of your questions regarding the GraphQL schema can be answered by querying the endpoint with the Apollo sandbox explorer. ## Schemas Definitions of all schemas will be accessible in a future update. Two of the most common schemas are described here. ### `APIReference` Represents a minimal representation of a resource. The detailed representation of the referenced resource can be retrieved by making a request to the referenced `URL`. ``` APIReference { index string name string url string } ```
### `DC` Represents a difficulty check. ``` DC { dc_type APIReference dc_value number success_type "none" | "half" | "other" } ```
### `Damage` Represents damage. ``` Damage { damage_type APIReference damage_dice string } ```
### `Choice` Represents a choice made by a player. Commonly seen related to decisions made during character creation or combat (e.g.: the description of the cleric class, under **Proficiencies**, states "Skills: Choose two from History, Insight, Medicine, Persuasion, and Religion" [[SRD p15]](https://media.wizards.com/2016/downloads/DND/SRD-OGL_V5.1.pdf#page=15)) ``` Choice { desc string choose number type string from OptionSet } ```
### `OptionSet` The OptionSet structure provides the options to be chosen from, or sufficient data to fetch and interpret the options. All OptionSets have an `option_set_type` attribute that indicates the structure of the object that contains the options. The possible values are `options_array`, `equipment_category`, and `reference_list`. Other attributes on the OptionSet depend on the value of this attribute. - `options_array` - `options` (array): An array of Option objects. Each item in the array represents an option that can be chosen. - `equipment_category` - `equipment_category` (APIReference): A reference to an EquipmentCategory. Each item in the EquipmentCategory's `equipment` array represents one option that can be chosen. - `resource_list` - `resource_list_url` (string): A reference (by URL) to a collection in the database. The URL may include query parameters. Each item in the resulting ResourceList's `results` array represents one option that can be chosen.
### `Option` When the options are given in an `options_array`, each item in the array inherits from the Option structure. All Options have an `option_type` attribute that indicates the structure of the option. The value of this attribute indicates how the option should be handled, and each type has different attributes. The possible values and their corresponding attributes are listed below. - `reference` - A terminal option. Contains a reference to a Document that can be added to the list of options chosen. - `item` (APIReference): A reference to the chosen item. - `action` - A terminal option. Contains information describing an action, for use within Multiattack actions. - `action_name` (string): The name of the action, according to its `name` attribute. - `count` (number | string): The number of times this action can be repeated if this option is chosen. - `type` (string = `"melee" | "ranged" | "ability" | "magic"`, optional): For attack actions that can be either melee, ranged, abilities, or magic. - `multiple` - When this option is chosen, all of its child options are chosen, and must be resolved the same way as a normal option. - `items` (array): An array of Option objects. All of them must be taken if the option is chosen. - `choice` - A nested choice. If this option is chosen, the Choice structure contained within must be resolved like a normal Choice structure, and the results are the chosen options. - `choice` (Choice): The Choice to resolve. - `string` - A terminal option. Contains a reference to a string. - `string` (string): The string. - `ideal` - A terminal option. Contains information about an ideal. - `desc` (string): A description of the ideal. - `alignments` (ApiReference[]): A list of alignments of those who might follow the ideal. - `counted_reference` - A terminal option. Contains a reference to something else in the API along with a count. - `count` (number): Count. - `of` (ApiReference): Thing being referenced. - `score_prerequisite` - A terminal option. Contains a reference to an ability score and a minimum score. - `ability_score` (ApiReference): Ability score being referenced. - `minimum_score` (number): The minimum score required to satisfy the prerequisite. - `ability_bonus` - A terminal option. Contains a reference to an ability score and a bonus - `ability_score` (ApiReference): Ability score being referenced - `bonus` (number): The bonus being applied to the ability score - `breath` - A terminal option: Contains a reference to information about a breath attack. - `name` (string): Name of the breath. - `dc` (DC): Difficulty check of the breath attack. - `damage` ([Damage]): Damage dealt by the breath attack, if any. - `damage` - A terminal option. Contains information about damage. - `damage_type` (ApiReference): Reference to type of damage. - `damage_dice` (string): Damage expressed in dice (e.g. "13d6"). - `notes` (string): Information regarding the damage. ## FAQ ### What is the SRD? The SRD, or Systems Reference Document, contains guidelines for publishing content under the OGL. This allows for some of the data for D&D 5e to be open source. The API only covers data that can be found in the SRD. [Here's a link to the full text of the SRD.](https://media.wizards.com/2016/downloads/DND/SRD-OGL_V5.1.pdf) ### What is the OGL? The Open Game License (OGL) is a public copyright license by Wizards of the Coast that may be used by tabletop role-playing game developers to grant permission to modify, copy, and redistribute some of the content designed for their games, notably game mechanics. However, they must share-alike copies and derivative works. [More information about the OGL can be found here.](https://en.wikipedia.org/wiki/Open_Game_License) ### A monster, spell, subclass, etc. is missing from the API / Database. Can I add it? Please check if the data is within the SRD. If it is, feel free to open an issue or PR to add it yourself. Otherwise, due to legal reasons, we cannot add it. ### Can this API be self hosted? Yes it can! You can also host the data yourself if you don't want to use the API at all. You can also make changes and add extra data if you like. However, it is up to you to merge in new changes to the data and API. #### Can I publish is on ? Is this free use? Yes, you can. The API itself is under the [MIT license](https://opensource.org/licenses/MIT), and the underlying data accessible via the API is supported under the SRD and OGL. # Status Page The status page for the API can be found here: https://5e-bits.github.io/dnd-uptime/ # Chat Come hang out with us [on Discord](https://discord.gg/TQuYTv7)! # Contribute This API is built from two repositories. - The repo containing the data lives here: https://github.com/bagelbits/5e-database - The repo with the API implementation lives here: https://github.com/bagelbits/5e-srd-api This is a evolving API and having fresh ideas are always welcome! You can open an issue in either repo, open a PR for changes, or just discuss with other users in this discord. version: '0.1' license: name: MIT License url: 'https://github.com/5e-bits/5e-srd-api/blob/main/LICENSE.md' contact: name: 5eBits url: https://github.com/5e-bits servers: - url: https://www.dnd5eapi.co description: Production - url: http://127.0.0.1:3000 description: Local Development tags: - name: Common description: General API endpoints and utilities - name: Character Data description: Endpoints related to character creation and management data paths: /api/2014: $ref: './paths/2014/combined.yml#/base' /api/2014/{endpoint}: $ref: './paths/2014/combined.yml#/list' /api/2014/ability-scores/{index}: $ref: './paths/2014/combined.yml#/ability-scores' /api/2014/alignments/{index}: $ref: './paths/2014/combined.yml#/alignments' /api/2014/backgrounds/{index}: $ref: './paths/2014/combined.yml#/backgrounds' /api/2014/classes/{index}: $ref: './paths/2014/combined.yml#/classes' /api/2014/classes/{index}/subclasses: $ref: './paths/2014/combined.yml#/class-subclass' /api/2014/classes/{index}/spells: $ref: './paths/2014/combined.yml#/class-spells' /api/2014/classes/{index}/spellcasting: $ref: './paths/2014/combined.yml#/class-spellcasting' /api/2014/classes/{index}/features: $ref: './paths/2014/combined.yml#/class-features' /api/2014/classes/{index}/proficiencies: $ref: './paths/2014/combined.yml#/class-proficiencies' /api/2014/classes/{index}/multi-classing: $ref: './paths/2014/combined.yml#/class-multi-classing' /api/2014/classes/{index}/levels: $ref: './paths/2014/combined.yml#/class-levels' /api/2014/classes/{index}/levels/{class_level}: $ref: './paths/2014/combined.yml#/class-level' /api/2014/classes/{index}/levels/{class_level}/features: $ref: './paths/2014/combined.yml#/class-level-features' /api/2014/classes/{index}/levels/{spell_level}/spells: $ref: './paths/2014/combined.yml#/class-spell-level-spells' /api/2014/conditions/{index}: $ref: './paths/2014/combined.yml#/conditions' /api/2014/damage-types/{index}: $ref: './paths/2014/combined.yml#/damage-types' /api/2014/equipment/{index}: $ref: './paths/2014/combined.yml#/equipment' /api/2014/equipment-categories/{index}: $ref: './paths/2014/combined.yml#/equipment-categories' /api/2014/feats/{index}: $ref: './paths/2014/combined.yml#/feats' /api/2014/features/{index}: $ref: './paths/2014/combined.yml#/features' /api/2014/languages/{index}: $ref: './paths/2014/combined.yml#/languages' /api/2014/magic-items/{index}: $ref: './paths/2014/combined.yml#/magic-items' /api/2014/magic-schools/{index}: $ref: './paths/2014/combined.yml#/magic-schools' /api/2014/monsters: $ref: './paths/2014/combined.yml#/monsters' /api/2014/monsters/{index}: $ref: './paths/2014/combined.yml#/monster' /api/2014/proficiencies/{index}: $ref: './paths/2014/combined.yml#/proficiencies' /api/2014/races/{index}: $ref: './paths/2014/combined.yml#/races' /api/2014/races/{index}/subraces: $ref: './paths/2014/combined.yml#/race-subraces' /api/2014/races/{index}/proficiencies: $ref: './paths/2014/combined.yml#/race-proficiencies' /api/2014/races/{index}/traits: $ref: './paths/2014/combined.yml#/race-traits' /api/2014/rule-sections/{index}: $ref: './paths/2014/combined.yml#/rule-sections' /api/2014/rules/{index}: $ref: './paths/2014/combined.yml#/rules' /api/2014/skills/{index}: $ref: './paths/2014/combined.yml#/skills' /api/2014/spells: $ref: './paths/2014/combined.yml#/spells' /api/2014/spells/{index}: $ref: './paths/2014/combined.yml#/spell' /api/2014/subclasses/{index}: $ref: './paths/2014/combined.yml#/subclasses' /api/2014/subclasses/{index}/features: $ref: './paths/2014/combined.yml#/subclass-features' /api/2014/subclasses/{index}/levels: $ref: './paths/2014/combined.yml#/subclass-levels' /api/2014/subclasses/{index}/levels/{subclass_level}: $ref: './paths/2014/combined.yml#/subclass-level' /api/2014/subclasses/{index}/levels/{subclass_level}/features: $ref: './paths/2014/combined.yml#/subclass-level-features' /api/2014/subraces/{index}: $ref: './paths/2014/combined.yml#/subraces' /api/2014/subraces/{index}/proficiencies: $ref: './paths/2014/combined.yml#/subrace-proficiencies' /api/2014/subraces/{index}/traits: $ref: './paths/2014/combined.yml#/subrace-traits' /api/2014/traits/{index}: $ref: './paths/2014/combined.yml#/traits' /api/2014/weapon-properties/{index}: $ref: './paths/2014/combined.yml#/weapon-properties' components: parameters: $ref: './parameters/2014/combined.yml' schemas: $ref: './schemas/2014/combined.yml' # Add security definitions security: - {} # Empty security requirement means no authentication required ================================================ FILE: src/tests/controllers/api/2014/abilityScoreController.test.ts ================================================ import { createRequest, createResponse } from 'node-mocks-http' import { describe, expect, it, vi } from 'vitest' import AbilityScoreController from '@/controllers/api/2014/abilityScoreController' import AbilityScoreModel from '@/models/2014/abilityScore' import { abilityScoreFactory } from '@/tests/factories/2014/abilityScore.factory' import { mockNext as defaultMockNext } from '@/tests/support' import { generateUniqueDbUri, setupIsolatedDatabase, setupModelCleanup, teardownIsolatedDatabase } from '@/tests/support/db' const mockNext = vi.fn(defaultMockNext) // Generate URI for this test file const dbUri = generateUniqueDbUri('abilityscore') // Setup hooks using helpers setupIsolatedDatabase(dbUri) teardownIsolatedDatabase() setupModelCleanup(AbilityScoreModel) describe('AbilityScoreController', () => { describe('index', () => { it('returns a list of ability scores', async () => { // Arrange: Seed the database const abilityScoresData = abilityScoreFactory.buildList(3) await AbilityScoreModel.insertMany(abilityScoresData) const request = createRequest() const response = createResponse() // Act await AbilityScoreController.index(request, response, mockNext) // Assert expect(response.statusCode).toBe(200) const responseData = JSON.parse(response._getData()) expect(responseData.count).toBe(3) expect(responseData.results.length).toBe(3) // Check if the returned data loosely matches the seeded data (checking name/index) expect(responseData.results).toEqual( expect.arrayContaining([ expect.objectContaining({ index: abilityScoresData[0].index, name: abilityScoresData[0].name }), expect.objectContaining({ index: abilityScoresData[1].index, name: abilityScoresData[1].name }), expect.objectContaining({ index: abilityScoresData[2].index, name: abilityScoresData[2].name }) ]) ) expect(mockNext).not.toHaveBeenCalled() }) // Skipping the explicit 'find error' mock test for now. }) describe('show', () => { it('returns a single ability score when found', async () => { // Arrange: Seed the database const abilityScoreData = abilityScoreFactory.build({ index: 'cha', name: 'CHA' }) await AbilityScoreModel.insertMany([abilityScoreData]) const request = createRequest({ params: { index: 'cha' } }) const response = createResponse() // Act await AbilityScoreController.show(request, response, mockNext) // Assert expect(response.statusCode).toBe(200) const responseData = JSON.parse(response._getData()) expect(responseData.index).toBe('cha') expect(responseData.name).toBe('CHA') expect(mockNext).not.toHaveBeenCalled() }) it('calls next with an error if the ability score is not found', async () => { // Arrange: Database is empty (guaranteed by setupModelCleanup) const request = createRequest({ params: { index: 'nonexistent' } }) const response = createResponse() // Act await AbilityScoreController.show(request, response, mockNext) // Assert expect(response.statusCode).toBe(200) // Default node-mocks-http status expect(response._getData()).toBe('') // No data written before error expect(mockNext).toHaveBeenCalledOnce() expect(mockNext).toHaveBeenCalledWith() // Expect next() called with no arguments }) // Skipping the explicit 'findOne error' mock test for similar reasons as above. }) }) ================================================ FILE: src/tests/controllers/api/2014/alignmentController.test.ts ================================================ import { createRequest, createResponse } from 'node-mocks-http' import { describe, expect, it, vi } from 'vitest' import AlignmentController from '@/controllers/api/2014/alignmentController' import AlignmentModel from '@/models/2014/alignment' import { alignmentFactory } from '@/tests/factories/2014/alignment.factory' import { mockNext as defaultMockNext } from '@/tests/support' // Assuming support helper location import { generateUniqueDbUri, setupIsolatedDatabase, setupModelCleanup, teardownIsolatedDatabase } from '@/tests/support/db' const mockNext = vi.fn(defaultMockNext) // Generate URI for this test file const dbUri = generateUniqueDbUri('alignment') setupIsolatedDatabase(dbUri) teardownIsolatedDatabase() setupModelCleanup(AlignmentModel) describe('AlignmentController', () => { describe('index', () => { it('returns a list of alignments', async () => { const alignmentsData = alignmentFactory.buildList(3) const alignmentDocs = alignmentsData.map((data) => new AlignmentModel(data)) await AlignmentModel.insertMany(alignmentDocs) const request = createRequest({ query: {} }) const response = createResponse() await AlignmentController.index(request, response, mockNext) expect(response.statusCode).toBe(200) const responseData = JSON.parse(response._getData()) expect(responseData.count).toBe(3) expect(responseData.results).toHaveLength(3) expect(responseData.results).toEqual( expect.arrayContaining([ expect.objectContaining({ index: alignmentsData[0].index, name: alignmentsData[0].name }), expect.objectContaining({ index: alignmentsData[1].index, name: alignmentsData[1].name }), expect.objectContaining({ index: alignmentsData[2].index, name: alignmentsData[2].name }) ]) ) expect(mockNext).not.toHaveBeenCalled() }) it('returns an empty list when no alignments exist', async () => { const request = createRequest({ query: {} }) const response = createResponse() await AlignmentController.index(request, response, mockNext) expect(response.statusCode).toBe(200) const responseData = JSON.parse(response._getData()) expect(responseData.count).toBe(0) expect(responseData.results).toEqual([]) expect(mockNext).not.toHaveBeenCalled() }) }) describe('show', () => { it('returns a single alignment when found', async () => { const alignmentData = alignmentFactory.build({ index: 'lg', name: 'Lawful Good' }) await AlignmentModel.insertMany([alignmentData]) const request = createRequest({ params: { index: 'lg' } }) const response = createResponse() await AlignmentController.show(request, response, mockNext) expect(response.statusCode).toBe(200) const responseData = JSON.parse(response._getData()) expect(responseData.index).toBe('lg') expect(responseData.name).toBe('Lawful Good') expect(responseData.desc).toEqual(alignmentData.desc) expect(mockNext).not.toHaveBeenCalled() }) it('calls next() when the alignment is not found', async () => { const request = createRequest({ params: { index: 'nonexistent' } }) const response = createResponse() await AlignmentController.show(request, response, mockNext) expect(response.statusCode).toBe(200) expect(response._getData()).toBe('') expect(mockNext).toHaveBeenCalledOnce() expect(mockNext).toHaveBeenCalledWith() }) }) }) ================================================ FILE: src/tests/controllers/api/2014/backgroundController.test.ts ================================================ import { createRequest, createResponse } from 'node-mocks-http' import { describe, expect, it, vi } from 'vitest' import BackgroundController from '@/controllers/api/2014/backgroundController' import BackgroundModel from '@/models/2014/background' import { backgroundFactory } from '@/tests/factories/2014/background.factory' import { mockNext as defaultMockNext } from '@/tests/support' import { generateUniqueDbUri, setupIsolatedDatabase, setupModelCleanup, teardownIsolatedDatabase } from '@/tests/support/db' const mockNext = vi.fn(defaultMockNext) // Generate URI for this test file const dbUri = generateUniqueDbUri('background') // Setup hooks using helpers setupIsolatedDatabase(dbUri) teardownIsolatedDatabase() setupModelCleanup(BackgroundModel) describe('BackgroundController', () => { describe('index', () => { it('returns a list of backgrounds', async () => { const backgroundsData = backgroundFactory.buildList(3) const backgroundDocs = backgroundsData.map((data) => new BackgroundModel(data)) await BackgroundModel.insertMany(backgroundDocs) const request = createRequest({ query: {} }) const response = createResponse() await BackgroundController.index(request, response, mockNext) expect(response.statusCode).toBe(200) const responseData = JSON.parse(response._getData()) expect(responseData.count).toBe(3) expect(responseData.results).toHaveLength(3) expect(responseData.results).toEqual( expect.arrayContaining([ expect.objectContaining({ index: backgroundsData[0].index, name: backgroundsData[0].name }), expect.objectContaining({ index: backgroundsData[1].index, name: backgroundsData[1].name }), expect.objectContaining({ index: backgroundsData[2].index, name: backgroundsData[2].name }) ]) ) expect(mockNext).not.toHaveBeenCalled() }) it('returns an empty list when no backgrounds exist', async () => { const request = createRequest({ query: {} }) const response = createResponse() await BackgroundController.index(request, response, mockNext) expect(response.statusCode).toBe(200) const responseData = JSON.parse(response._getData()) expect(responseData.count).toBe(0) expect(responseData.results).toEqual([]) expect(mockNext).not.toHaveBeenCalled() }) }) describe('show', () => { it('returns a single background when found', async () => { const backgroundData = backgroundFactory.build({ index: 'acolyte', name: 'Acolyte' }) await BackgroundModel.insertMany([backgroundData]) const request = createRequest({ params: { index: 'acolyte' } }) const response = createResponse() await BackgroundController.show(request, response, mockNext) expect(response.statusCode).toBe(200) const responseData = JSON.parse(response._getData()) expect(responseData.index).toBe('acolyte') expect(responseData.name).toBe('Acolyte') // Add more specific checks if needed, e.g., for nested properties expect(responseData.feature.name).toEqual(backgroundData.feature.name) expect(mockNext).not.toHaveBeenCalled() }) it('calls next() when the background is not found', async () => { const request = createRequest({ params: { index: 'nonexistent' } }) const response = createResponse() await BackgroundController.show(request, response, mockNext) expect(response.statusCode).toBe(200) expect(response._getData()).toBe('') expect(mockNext).toHaveBeenCalledOnce() expect(mockNext).toHaveBeenCalledWith() }) }) }) ================================================ FILE: src/tests/controllers/api/2014/classController.test.ts ================================================ import { createRequest, createResponse } from 'node-mocks-http' import { describe, expect, it, vi } from 'vitest' import * as ClassController from '@/controllers/api/2014/classController' import ClassModel from '@/models/2014/class' import FeatureModel from '@/models/2014/feature' import LevelModel from '@/models/2014/level' import ProficiencyModel from '@/models/2014/proficiency' import SpellModel from '@/models/2014/spell' import SubclassModel from '@/models/2014/subclass' import { classFactory } from '@/tests/factories/2014/class.factory' import { featureFactory } from '@/tests/factories/2014/feature.factory' import { levelFactory } from '@/tests/factories/2014/level.factory' import { proficiencyFactory } from '@/tests/factories/2014/proficiency.factory' import { spellFactory } from '@/tests/factories/2014/spell.factory' import { subclassFactory } from '@/tests/factories/2014/subclass.factory' import { mockNext as defaultMockNext } from '@/tests/support' import { generateUniqueDbUri, setupIsolatedDatabase, setupModelCleanup, teardownIsolatedDatabase } from '@/tests/support/db' const mockNext = vi.fn(defaultMockNext) const dbUri = generateUniqueDbUri('class') // Setup hooks using helpers setupIsolatedDatabase(dbUri) teardownIsolatedDatabase() setupModelCleanup(ClassModel) setupModelCleanup(LevelModel) setupModelCleanup(SubclassModel) setupModelCleanup(SpellModel) setupModelCleanup(FeatureModel) setupModelCleanup(ProficiencyModel) describe('ClassController', () => { describe('index', () => { it('returns a list of classes', async () => { const classesData = classFactory.buildList(3) await ClassModel.insertMany(classesData) const request = createRequest({ query: {} }) const response = createResponse() await ClassController.index(request, response, mockNext) expect(response.statusCode).toBe(200) const responseData = JSON.parse(response._getData()) expect(responseData.count).toBe(3) expect(responseData.results).toHaveLength(3) expect(responseData.results).toEqual( expect.arrayContaining([ expect.objectContaining({ index: classesData[0].index, name: classesData[0].name, url: classesData[0].url }), expect.objectContaining({ index: classesData[1].index, name: classesData[1].name, url: classesData[1].url }), expect.objectContaining({ index: classesData[2].index, name: classesData[2].name, url: classesData[2].url }) ]) ) expect(mockNext).not.toHaveBeenCalled() }) it('handles index database errors', async () => { const request = createRequest({ query: {} }) const response = createResponse() const error = new Error('Database find failed') vi.spyOn(ClassModel, 'find').mockImplementationOnce( () => ({ select: vi.fn().mockReturnThis(), sort: vi.fn().mockReturnThis(), exec: vi.fn().mockRejectedValueOnce(error) }) as any ) await ClassController.index(request, response, mockNext) expect(mockNext).toHaveBeenCalledWith(error) }) it('returns an empty list when no classes exist', async () => { const request = createRequest({ query: {} }) const response = createResponse() await ClassController.index(request, response, mockNext) expect(response.statusCode).toBe(200) expect(JSON.parse(response._getData())).toEqual({ count: 0, results: [] }) expect(mockNext).not.toHaveBeenCalled() }) }) // === show === describe('show', () => { const classIndex = 'barbarian' it('returns a single class when found', async () => { const classData = classFactory.build({ index: classIndex }) await ClassModel.insertMany([classData]) const request = createRequest({ params: { index: classIndex } }) const response = createResponse() await ClassController.show(request, response, mockNext) expect(response.statusCode).toBe(200) const responseData = JSON.parse(response._getData()) expect(responseData).toMatchObject({ index: classData.index, name: classData.name, url: classData.url }) expect(mockNext).not.toHaveBeenCalled() }) it('calls next() when the class is not found', async () => { const request = createRequest({ params: { index: 'nonexistent' } }) const response = createResponse() await ClassController.show(request, response, mockNext) expect(mockNext).toHaveBeenCalledWith() }) it('handles show database errors', async () => { const request = createRequest({ params: { index: classIndex } }) const response = createResponse() const error = new Error('Database findOne failed') vi.spyOn(ClassModel, 'findOne').mockRejectedValueOnce(error) await ClassController.show(request, response, mockNext) expect(mockNext).toHaveBeenCalledWith(error) }) }) // === showLevelsForClass === describe('showLevelsForClass', () => { const classIndex = 'wizard' const classUrl = `/api/2014/classes/${classIndex}` it('returns levels for a specific class', async () => { const classData = classFactory.build({ index: classIndex, url: classUrl }) await ClassModel.insertMany([classData]) const levelsData = levelFactory.buildList(5, { class: { index: classIndex, name: classData.name, url: classUrl } }) await LevelModel.insertMany(levelsData) await LevelModel.insertMany(levelFactory.buildList(2)) const request = createRequest({ query: {}, params: { index: classIndex } }) const response = createResponse() await ClassController.showLevelsForClass(request, response, mockNext) expect(response.statusCode).toBe(200) const responseData = JSON.parse(response._getData()) expect(responseData).toBeInstanceOf(Array) expect(responseData).toHaveLength(5) expect(mockNext).not.toHaveBeenCalled() }) it('returns 404 if class has no levels', async () => { const classData = classFactory.build({ index: classIndex, url: classUrl }) await ClassModel.insertMany([classData]) const request = createRequest({ query: {}, params: { index: classIndex } }) const response = createResponse() await ClassController.showLevelsForClass(request, response, mockNext) expect(response.statusCode).toBe(404) expect(JSON.parse(response._getData())).toEqual({ error: 'Not found' }) expect(mockNext).not.toHaveBeenCalled() }) it('returns 404 if class index is invalid', async () => { const request = createRequest({ query: {}, params: { index: 'nonexistent' } }) const response = createResponse() await ClassController.showLevelsForClass(request, response, mockNext) expect(response.statusCode).toBe(404) expect(JSON.parse(response._getData())).toEqual({ error: 'Not found' }) expect(mockNext).not.toHaveBeenCalled() }) it('handles levels database errors', async () => { const classData = classFactory.build({ index: classIndex, url: classUrl }) await ClassModel.insertMany([classData]) const request = createRequest({ query: {}, params: { index: classIndex } }) const response = createResponse() const error = new Error('Level find failed') vi.spyOn(LevelModel, 'find').mockImplementationOnce( () => ({ sort: vi.fn().mockRejectedValueOnce(error) }) as any ) await ClassController.showLevelsForClass(request, response, mockNext) expect(mockNext).toHaveBeenCalledWith(error) }) }) // === showLevelForClass === describe('showLevelForClass', () => { const classIndex = 'wizard' const targetLevel = 5 const levelUrl = `/api/2014/classes/${classIndex}/levels/${targetLevel}` it('returns a specific level for a class', async () => { const classData = classFactory.build({ index: classIndex }) await ClassModel.insertMany([classData]) const levelData = levelFactory.build({ level: targetLevel, class: { index: classIndex, name: classData.name, url: `/api/2014/classes/${classIndex}` }, url: levelUrl }) await LevelModel.insertMany([levelData]) const request = createRequest({ params: { index: classIndex, level: String(targetLevel) } }) const response = createResponse() await ClassController.showLevelForClass(request, response, mockNext) expect(response.statusCode).toBe(200) const responseData = JSON.parse(response._getData()) expect(responseData).toMatchObject({ level: levelData.level, url: levelData.url }) expect(mockNext).not.toHaveBeenCalled() }) it('calls next() when the specific level is not found', async () => { const request = createRequest({ params: { index: classIndex, level: '10' } }) const response = createResponse() await ClassController.showLevelForClass(request, response, mockNext) expect(mockNext).toHaveBeenCalledWith() }) it('handles level findOne database errors', async () => { const request = createRequest({ params: { index: classIndex, level: String(targetLevel) } }) const response = createResponse() const error = new Error('Level findOne failed') vi.spyOn(LevelModel, 'findOne').mockRejectedValueOnce(error) await ClassController.showLevelForClass(request, response, mockNext) expect(mockNext).toHaveBeenCalledWith(error) }) it('returns 400 for invalid level parameter (non-numeric)', async () => { const request = createRequest({ params: { index: classIndex, level: 'invalid' } }) const response = createResponse() await ClassController.showLevelForClass(request, response, mockNext) expect(response.statusCode).toBe(400) expect(JSON.parse(response._getData()).error).toContain('Invalid path parameters') }) it('returns 400 for invalid level parameter (out of range)', async () => { const request = createRequest({ params: { index: classIndex, level: '21' } }) const response = createResponse() await ClassController.showLevelForClass(request, response, mockNext) expect(response.statusCode).toBe(400) expect(JSON.parse(response._getData()).error).toContain('Invalid path parameters') }) }) // === showSubclassesForClass === describe('showSubclassesForClass', () => { const classIndex = 'barbarian' const classUrl = `/api/2014/classes/${classIndex}` it('returns subclasses for a specific class', async () => { const classData = classFactory.build({ index: classIndex, url: classUrl }) await ClassModel.insertMany([classData]) const subclassesData = subclassFactory.buildList(2, { class: { index: classIndex, name: classData.name, url: classUrl } }) await SubclassModel.insertMany(subclassesData) await SubclassModel.insertMany(subclassFactory.buildList(1)) const request = createRequest({ params: { index: classIndex } }) const response = createResponse() await ClassController.showSubclassesForClass(request, response, mockNext) expect(response.statusCode).toBe(200) const responseData = JSON.parse(response._getData()) expect(responseData).toHaveProperty('count', 2) expect(responseData).toHaveProperty('results') expect(responseData.results).toHaveLength(2) expect(mockNext).not.toHaveBeenCalled() }) it('returns 404 if class index is invalid', async () => { const request = createRequest({ params: { index: 'nonexistent' } }) const response = createResponse() await ClassController.showSubclassesForClass(request, response, mockNext) expect(response.statusCode).toBe(404) expect(JSON.parse(response._getData()).error).toEqual('Not found') }) it('handles subclass find database errors', async () => { const classData = classFactory.build({ index: classIndex, url: classUrl }) await ClassModel.insertMany([classData]) const request = createRequest({ params: { index: classIndex } }) const response = createResponse() const error = new Error('Subclass find failed') vi.spyOn(SubclassModel, 'find').mockImplementationOnce( () => ({ select: vi.fn().mockReturnThis(), sort: vi.fn().mockRejectedValueOnce(error) }) as any ) await ClassController.showSubclassesForClass(request, response, mockNext) expect(mockNext).toHaveBeenCalledWith(error) }) }) // === showSpellsForClass === describe('showSpellsForClass', () => { const classIndex = 'wizard' const classUrl = `/api/2014/classes/${classIndex}` it('returns spells for a specific class', async () => { const classData = classFactory.build({ index: classIndex, url: classUrl }) await ClassModel.insertMany([classData]) const spellsData = spellFactory.buildList(3, { classes: [{ index: classIndex, name: classData.name, url: classUrl }] }) await SpellModel.insertMany(spellsData) await SpellModel.insertMany(spellFactory.buildList(2)) const request = createRequest({ params: { index: classIndex } }) const response = createResponse() await ClassController.showSpellsForClass(request, response, mockNext) expect(response.statusCode).toBe(200) const responseData = JSON.parse(response._getData()) expect(responseData.count).toBe(3) expect(responseData.results).toHaveLength(3) expect(mockNext).not.toHaveBeenCalled() }) it('returns spells filtered by a single level', async () => { const classData = classFactory.build({ index: classIndex, url: classUrl }) await ClassModel.insertMany([classData]) const spellsLevel1 = spellFactory.buildList(2, { level: 1, classes: [{ index: classIndex, name: classData.name, url: classUrl }] }) const spellsLevel2 = spellFactory.buildList(1, { level: 2, classes: [{ index: classIndex, name: classData.name, url: classUrl }] }) await SpellModel.insertMany([...spellsLevel1, ...spellsLevel2]) const request = createRequest({ query: { level: '1' }, params: { index: classIndex } }) const response = createResponse() await ClassController.showSpellsForClass(request, response, mockNext) expect(response.statusCode).toBe(200) const responseData = JSON.parse(response._getData()) expect(responseData.count).toBe(2) expect(responseData.results).toHaveLength(2) responseData.results.forEach((spell: any) => { expect(spell.level).toBe(1) }) expect(mockNext).not.toHaveBeenCalled() }) it('returns spells filtered by multiple levels', async () => { const classData = classFactory.build({ index: classIndex, url: classUrl }) await ClassModel.insertMany([classData]) const spellsLevel1 = spellFactory.buildList(2, { level: 1, classes: [{ index: classIndex, name: classData.name, url: classUrl }] }) const spellsLevel2 = spellFactory.buildList(1, { level: 2, classes: [{ index: classIndex, name: classData.name, url: classUrl }] }) const spellsLevel3 = spellFactory.buildList(1, { level: 3, classes: [{ index: classIndex, name: classData.name, url: classUrl }] }) await SpellModel.insertMany([...spellsLevel1, ...spellsLevel2, ...spellsLevel3]) const request = createRequest({ query: { level: ['1', '3'] }, params: { index: classIndex } }) const response = createResponse() await ClassController.showSpellsForClass(request, response, mockNext) expect(response.statusCode).toBe(200) const responseData = JSON.parse(response._getData()) expect(responseData.count).toBe(3) // 2 level 1 + 1 level 3 expect(responseData.results).toHaveLength(3) responseData.results.forEach((spell: any) => { expect([1, 3]).toContain(spell.level) }) expect(mockNext).not.toHaveBeenCalled() }) it('returns an empty list if no spells match the level filter', async () => { const classData = classFactory.build({ index: classIndex, url: classUrl }) await ClassModel.insertMany([classData]) const spellsLevel1 = spellFactory.buildList(2, { level: 1, classes: [{ index: classIndex, name: classData.name, url: classUrl }] }) await SpellModel.insertMany(spellsLevel1) const request = createRequest({ query: { level: '5' }, params: { index: classIndex } }) const response = createResponse() await ClassController.showSpellsForClass(request, response, mockNext) expect(response.statusCode).toBe(200) const responseData = JSON.parse(response._getData()) expect(responseData.count).toBe(0) expect(responseData.results).toHaveLength(0) expect(mockNext).not.toHaveBeenCalled() }) it('returns 404 if class index is invalid, regardless of level query', async () => { const request = createRequest({ query: { level: '1' }, params: { index: 'nonexistent' } }) const response = createResponse() await ClassController.showSpellsForClass(request, response, mockNext) expect(response.statusCode).toBe(404) expect(JSON.parse(response._getData())).toEqual({ error: 'Not found' }) expect(mockNext).not.toHaveBeenCalled() }) it('returns 400 for invalid level parameter (non-numeric string)', async () => { const classData = classFactory.build({ index: classIndex, url: classUrl }) await ClassModel.insertMany([classData]) const request = createRequest({ query: { level: 'abc' }, params: { index: classIndex } }) const response = createResponse() await ClassController.showSpellsForClass(request, response, mockNext) expect(response.statusCode).toBe(400) const responseData = JSON.parse(response._getData()) expect(responseData.error).toBe('Invalid query parameters') expect(responseData.details).toBeInstanceOf(Array) expect(responseData.details[0].path).toEqual(['level']) expect(responseData.details[0].message).toContain('Invalid') expect(mockNext).not.toHaveBeenCalled() }) it('returns 400 for invalid level parameter (in an array)', async () => { const classData = classFactory.build({ index: classIndex, url: classUrl }) await ClassModel.insertMany([classData]) const request = createRequest({ query: { level: ['1', 'abc'] }, params: { index: classIndex } }) const response = createResponse() await ClassController.showSpellsForClass(request, response, mockNext) expect(response.statusCode).toBe(400) const responseData = JSON.parse(response._getData()) expect(responseData.error).toBe('Invalid query parameters') expect(responseData.details).toBeInstanceOf(Array) expect(responseData.details[0].path).toEqual(['level', 1]) expect(responseData.details[0].message).toContain('Invalid') expect(mockNext).not.toHaveBeenCalled() }) it('handles database errors gracefully', async () => { const classData = classFactory.build({ index: classIndex, url: classUrl }) await ClassModel.insertMany([classData]) const request = createRequest({ params: { index: classIndex } }) const response = createResponse() const error = new Error('Spell find failed') vi.spyOn(SpellModel, 'find').mockImplementationOnce( () => ({ select: vi.fn().mockReturnThis(), sort: vi.fn().mockRejectedValueOnce(error) }) as any ) await ClassController.showSpellsForClass(request, response, mockNext) expect(mockNext).toHaveBeenCalledWith(error) }) }) // === showFeaturesForClass === describe('showFeaturesForClass', () => { const classIndex = 'fighter' const classUrl = `/api/2014/classes/${classIndex}` it('returns features for a specific class', async () => { const classData = classFactory.build({ index: classIndex, url: classUrl }) await ClassModel.insertMany([classData]) const featuresData = featureFactory.buildList(4, { class: { index: classIndex, name: classData.name, url: classUrl } }) await FeatureModel.insertMany(featuresData) await FeatureModel.insertMany(featureFactory.buildList(2)) const request = createRequest({ params: { index: classIndex } }) const response = createResponse() await ClassController.showFeaturesForClass(request, response, mockNext) expect(response.statusCode).toBe(200) const responseData = JSON.parse(response._getData()) expect(responseData).toHaveProperty('count', 4) expect(responseData.results).toHaveLength(4) expect(mockNext).not.toHaveBeenCalled() }) it('returns 404 if class index is invalid', async () => { const request = createRequest({ params: { index: 'nonexistent' } }) const response = createResponse() await ClassController.showFeaturesForClass(request, response, mockNext) expect(response.statusCode).toBe(404) expect(JSON.parse(response._getData())).toEqual({ error: 'Not found' }) expect(mockNext).not.toHaveBeenCalled() }) it('handles feature find database errors', async () => { const classData = classFactory.build({ index: classIndex, url: classUrl }) await ClassModel.insertMany([classData]) const request = createRequest({ params: { index: classIndex } }) const response = createResponse() const error = new Error('Feature find failed') vi.spyOn(FeatureModel, 'find').mockImplementationOnce( () => ({ select: vi.fn().mockReturnThis(), sort: vi.fn().mockRejectedValueOnce(error) }) as any ) await ClassController.showFeaturesForClass(request, response, mockNext) expect(mockNext).toHaveBeenCalledWith(error) }) }) // === showProficienciesForClass === describe('showProficienciesForClass', () => { const classIndex = 'rogue' const classUrl = `/api/2014/classes/${classIndex}` it('returns proficiencies for a specific class', async () => { const classData = classFactory.build({ index: classIndex, url: classUrl }) await ClassModel.insertMany([classData]) const profsData = proficiencyFactory.buildList(5, { classes: [{ index: classIndex, name: classData.name, url: classUrl }] }) await ProficiencyModel.insertMany(profsData) await ProficiencyModel.insertMany(proficiencyFactory.buildList(2)) const request = createRequest({ params: { index: classIndex } }) const response = createResponse() await ClassController.showProficienciesForClass(request, response, mockNext) expect(response.statusCode).toBe(200) const responseData = JSON.parse(response._getData()) expect(responseData).toHaveProperty('count', 5) expect(responseData.results).toHaveLength(5) expect(mockNext).not.toHaveBeenCalled() }) it('returns 404 if class index is invalid', async () => { const request = createRequest({ params: { index: 'nonexistent' } }) const response = createResponse() await ClassController.showProficienciesForClass(request, response, mockNext) expect(response.statusCode).toBe(404) expect(JSON.parse(response._getData())).toEqual({ error: 'Not found' }) expect(mockNext).not.toHaveBeenCalled() }) it('handles proficiency find database errors', async () => { const classData = classFactory.build({ index: classIndex, url: classUrl }) await ClassModel.insertMany([classData]) const request = createRequest({ params: { index: classIndex } }) const response = createResponse() const error = new Error('Proficiency find failed') vi.spyOn(ProficiencyModel, 'find').mockImplementationOnce( () => ({ select: vi.fn().mockReturnThis(), sort: vi.fn().mockRejectedValueOnce(error) }) as any ) await ClassController.showProficienciesForClass(request, response, mockNext) expect(mockNext).toHaveBeenCalledWith(error) }) }) }) ================================================ FILE: src/tests/controllers/api/2014/conditionController.test.ts ================================================ import { createRequest, createResponse } from 'node-mocks-http' import { describe, expect, it, vi } from 'vitest' import ConditionController from '@/controllers/api/2014/conditionController' import ConditionModel from '@/models/2014/condition' import { conditionFactory } from '@/tests/factories/2014/condition.factory' import { mockNext as defaultMockNext } from '@/tests/support' import { generateUniqueDbUri, setupIsolatedDatabase, setupModelCleanup, teardownIsolatedDatabase } from '@/tests/support/db' const mockNext = vi.fn(defaultMockNext) // Generate URI for this test file const dbUri = generateUniqueDbUri('condition') // Setup hooks using helpers setupIsolatedDatabase(dbUri) teardownIsolatedDatabase() setupModelCleanup(ConditionModel) describe('ConditionController', () => { describe('index', () => { it('returns a list of conditions', async () => { const conditionsData = conditionFactory.buildList(3) const conditionDocs = conditionsData.map((data) => new ConditionModel(data)) await ConditionModel.insertMany(conditionDocs) const request = createRequest({ query: {} }) const response = createResponse() await ConditionController.index(request, response, mockNext) expect(response.statusCode).toBe(200) const responseData = JSON.parse(response._getData()) expect(responseData.count).toBe(3) expect(responseData.results).toHaveLength(3) expect(responseData.results).toEqual( expect.arrayContaining([ expect.objectContaining({ index: conditionsData[0].index, name: conditionsData[0].name }), expect.objectContaining({ index: conditionsData[1].index, name: conditionsData[1].name }), expect.objectContaining({ index: conditionsData[2].index, name: conditionsData[2].name }) ]) ) expect(mockNext).not.toHaveBeenCalled() }) it('returns an empty list when no conditions exist', async () => { const request = createRequest({ query: {} }) const response = createResponse() await ConditionController.index(request, response, mockNext) expect(response.statusCode).toBe(200) const responseData = JSON.parse(response._getData()) expect(responseData.count).toBe(0) expect(responseData.results).toEqual([]) expect(mockNext).not.toHaveBeenCalled() }) }) describe('show', () => { it('returns a single condition when found', async () => { const conditionData = conditionFactory.build({ index: 'blinded', name: 'Blinded' }) await ConditionModel.insertMany([conditionData]) const request = createRequest({ params: { index: 'blinded' } }) const response = createResponse() await ConditionController.show(request, response, mockNext) expect(response.statusCode).toBe(200) const responseData = JSON.parse(response._getData()) expect(responseData.index).toBe('blinded') expect(responseData.name).toBe('Blinded') expect(responseData.desc).toEqual(conditionData.desc) expect(mockNext).not.toHaveBeenCalled() }) it('calls next() when the condition is not found', async () => { const request = createRequest({ params: { index: 'nonexistent' } }) const response = createResponse() await ConditionController.show(request, response, mockNext) expect(response.statusCode).toBe(200) expect(response._getData()).toBe('') expect(mockNext).toHaveBeenCalledOnce() expect(mockNext).toHaveBeenCalledWith() }) }) }) ================================================ FILE: src/tests/controllers/api/2014/damageTypeController.test.ts ================================================ import { createRequest, createResponse } from 'node-mocks-http' import { describe, expect, it, vi } from 'vitest' import DamageTypeController from '@/controllers/api/2014/damageTypeController' import DamageTypeModel from '@/models/2014/damageType' import { damageTypeFactory } from '@/tests/factories/2014/damageType.factory' import { mockNext as defaultMockNext } from '@/tests/support' import { generateUniqueDbUri, setupIsolatedDatabase, setupModelCleanup, teardownIsolatedDatabase } from '@/tests/support/db' const mockNext = vi.fn(defaultMockNext) // Generate URI for this test file const dbUri = generateUniqueDbUri('damageType') // Setup hooks using helpers setupIsolatedDatabase(dbUri) teardownIsolatedDatabase() setupModelCleanup(DamageTypeModel) describe('DamageTypeController', () => { describe('index', () => { it('returns a list of damage types', async () => { const damageTypesData = damageTypeFactory.buildList(3) const damageTypeDocs = damageTypesData.map((data) => new DamageTypeModel(data)) await DamageTypeModel.insertMany(damageTypeDocs) const request = createRequest({ query: {} }) const response = createResponse() await DamageTypeController.index(request, response, mockNext) expect(response.statusCode).toBe(200) const responseData = JSON.parse(response._getData()) expect(responseData.count).toBe(3) expect(responseData.results).toHaveLength(3) expect(responseData.results).toEqual( expect.arrayContaining([ expect.objectContaining({ index: damageTypesData[0].index, name: damageTypesData[0].name }), expect.objectContaining({ index: damageTypesData[1].index, name: damageTypesData[1].name }), expect.objectContaining({ index: damageTypesData[2].index, name: damageTypesData[2].name }) ]) ) expect(mockNext).not.toHaveBeenCalled() }) it('returns an empty list when no damage types exist', async () => { const request = createRequest({ query: {} }) const response = createResponse() await DamageTypeController.index(request, response, mockNext) expect(response.statusCode).toBe(200) const responseData = JSON.parse(response._getData()) expect(responseData.count).toBe(0) expect(responseData.results).toEqual([]) expect(mockNext).not.toHaveBeenCalled() }) }) describe('show', () => { it('returns a single damage type when found', async () => { const damageTypeData = damageTypeFactory.build({ index: 'acid', name: 'Acid' }) await DamageTypeModel.insertMany([damageTypeData]) const request = createRequest({ params: { index: 'acid' } }) const response = createResponse() await DamageTypeController.show(request, response, mockNext) expect(response.statusCode).toBe(200) const responseData = JSON.parse(response._getData()) expect(responseData.index).toBe('acid') expect(responseData.name).toBe('Acid') expect(responseData.desc).toEqual(damageTypeData.desc) expect(mockNext).not.toHaveBeenCalled() }) it('calls next() when the damage type is not found', async () => { const request = createRequest({ params: { index: 'nonexistent' } }) const response = createResponse() await DamageTypeController.show(request, response, mockNext) expect(response.statusCode).toBe(200) expect(response._getData()).toBe('') expect(mockNext).toHaveBeenCalledOnce() expect(mockNext).toHaveBeenCalledWith() }) }) }) ================================================ FILE: src/tests/controllers/api/2014/equipmentCategoryController.test.ts ================================================ import { createRequest, createResponse } from 'node-mocks-http' import { describe, expect, it, vi } from 'vitest' import EquipmentCategoryController from '@/controllers/api/2014/equipmentCategoryController' import EquipmentCategoryModel from '@/models/2014/equipmentCategory' import { equipmentCategoryFactory } from '@/tests/factories/2014/equipmentCategory.factory' import { mockNext as defaultMockNext } from '@/tests/support' import { generateUniqueDbUri, setupIsolatedDatabase, setupModelCleanup, teardownIsolatedDatabase } from '@/tests/support/db' const mockNext = vi.fn(defaultMockNext) // Generate URI for this test file const dbUri = generateUniqueDbUri('equipmentcategory') // Setup hooks using helpers setupIsolatedDatabase(dbUri) teardownIsolatedDatabase() setupModelCleanup(EquipmentCategoryModel) describe('EquipmentCategoryController', () => { describe('index', () => { it('returns a list of equipment categories', async () => { const categoriesData = equipmentCategoryFactory.buildList(3) const categoryDocs = categoriesData.map((data) => new EquipmentCategoryModel(data)) await EquipmentCategoryModel.insertMany(categoryDocs) const request = createRequest({ query: {} }) const response = createResponse() await EquipmentCategoryController.index(request, response, mockNext) expect(response.statusCode).toBe(200) const responseData = JSON.parse(response._getData()) expect(responseData.count).toBe(3) expect(responseData.results).toHaveLength(3) expect(responseData.results).toEqual( expect.arrayContaining([ expect.objectContaining({ index: categoriesData[0].index, name: categoriesData[0].name }), expect.objectContaining({ index: categoriesData[1].index, name: categoriesData[1].name }), expect.objectContaining({ index: categoriesData[2].index, name: categoriesData[2].name }) ]) ) expect(mockNext).not.toHaveBeenCalled() }) it('returns an empty list when no equipment categories exist', async () => { const request = createRequest({ query: {} }) const response = createResponse() await EquipmentCategoryController.index(request, response, mockNext) expect(response.statusCode).toBe(200) const responseData = JSON.parse(response._getData()) expect(responseData.count).toBe(0) expect(responseData.results).toEqual([]) expect(mockNext).not.toHaveBeenCalled() }) }) describe('show', () => { it('returns a single equipment category when found', async () => { const categoryData = equipmentCategoryFactory.build({ index: 'armor', name: 'Armor' }) await EquipmentCategoryModel.insertMany([categoryData]) const request = createRequest({ params: { index: 'armor' } }) const response = createResponse() await EquipmentCategoryController.show(request, response, mockNext) expect(response.statusCode).toBe(200) const responseData = JSON.parse(response._getData()) expect(responseData.index).toBe('armor') expect(responseData.name).toBe('Armor') expect(responseData.equipment).toHaveLength(categoryData.equipment?.length ?? 0) expect(mockNext).not.toHaveBeenCalled() }) it('calls next() when the equipment category is not found', async () => { const request = createRequest({ params: { index: 'nonexistent' } }) const response = createResponse() await EquipmentCategoryController.show(request, response, mockNext) expect(response.statusCode).toBe(200) expect(response._getData()).toBe('') expect(mockNext).toHaveBeenCalledOnce() expect(mockNext).toHaveBeenCalledWith() }) }) }) ================================================ FILE: src/tests/controllers/api/2014/equipmentController.test.ts ================================================ import { createRequest, createResponse } from 'node-mocks-http' import { describe, expect, it, vi } from 'vitest' import EquipmentController from '@/controllers/api/2014/equipmentController' import EquipmentModel from '@/models/2014/equipment' import { equipmentFactory } from '@/tests/factories/2014/equipment.factory' import { mockNext as defaultMockNext } from '@/tests/support' import { generateUniqueDbUri, setupIsolatedDatabase, setupModelCleanup, teardownIsolatedDatabase } from '@/tests/support/db' const mockNext = vi.fn(defaultMockNext) // Generate URI for this test file const dbUri = generateUniqueDbUri('equipment') // Setup hooks using helpers setupIsolatedDatabase(dbUri) teardownIsolatedDatabase() setupModelCleanup(EquipmentModel) describe('EquipmentController', () => { describe('index', () => { it('returns a list of equipment', async () => { const equipmentData = equipmentFactory.buildList(3) const equipmentDocs = equipmentData.map((data) => new EquipmentModel(data)) await EquipmentModel.insertMany(equipmentDocs) const request = createRequest({ query: {} }) const response = createResponse() await EquipmentController.index(request, response, mockNext) expect(response.statusCode).toBe(200) const responseData = JSON.parse(response._getData()) expect(responseData.count).toBe(3) expect(responseData.results).toHaveLength(3) expect(responseData.results).toEqual( expect.arrayContaining([ expect.objectContaining({ index: equipmentData[0].index, name: equipmentData[0].name }), expect.objectContaining({ index: equipmentData[1].index, name: equipmentData[1].name }), expect.objectContaining({ index: equipmentData[2].index, name: equipmentData[2].name }) ]) ) expect(mockNext).not.toHaveBeenCalled() }) it('returns an empty list when no equipment exists', async () => { const request = createRequest({ query: {} }) const response = createResponse() await EquipmentController.index(request, response, mockNext) expect(response.statusCode).toBe(200) const responseData = JSON.parse(response._getData()) expect(responseData.count).toBe(0) expect(responseData.results).toEqual([]) expect(mockNext).not.toHaveBeenCalled() }) }) describe('show', () => { it('returns a single piece of equipment when found', async () => { const equipmentData = equipmentFactory.build({ index: 'club', name: 'Club' }) await EquipmentModel.insertMany([equipmentData]) const request = createRequest({ params: { index: 'club' } }) const response = createResponse() await EquipmentController.show(request, response, mockNext) expect(response.statusCode).toBe(200) const responseData = JSON.parse(response._getData()) expect(responseData.index).toBe('club') expect(responseData.name).toBe('Club') expect(responseData.desc).toEqual(equipmentData.desc) expect(responseData.cost.quantity).toEqual(equipmentData.cost.quantity) expect(responseData.equipment_category.index).toEqual(equipmentData.equipment_category.index) expect(mockNext).not.toHaveBeenCalled() }) it('calls next() when the equipment is not found', async () => { const request = createRequest({ params: { index: 'nonexistent' } }) const response = createResponse() await EquipmentController.show(request, response, mockNext) expect(response.statusCode).toBe(200) expect(response._getData()).toBe('') expect(mockNext).toHaveBeenCalledOnce() expect(mockNext).toHaveBeenCalledWith() }) }) }) ================================================ FILE: src/tests/controllers/api/2014/featController.test.ts ================================================ import { createRequest, createResponse } from 'node-mocks-http' import { describe, expect, it, vi } from 'vitest' import FeatController from '@/controllers/api/2014/featController' import FeatModel from '@/models/2014/feat' // Use Model suffix import { featFactory } from '@/tests/factories/2014/feat.factory' // Import factory import { mockNext as defaultMockNext } from '@/tests/support' import { generateUniqueDbUri, setupIsolatedDatabase, setupModelCleanup, teardownIsolatedDatabase } from '@/tests/support/db' const mockNext = vi.fn(defaultMockNext) // Generate URI for this test file const dbUri = generateUniqueDbUri('feat') // Setup hooks using helpers setupIsolatedDatabase(dbUri) teardownIsolatedDatabase() setupModelCleanup(FeatModel) describe('FeatController', () => { describe('index', () => { it('returns a list of feats', async () => { // Arrange const featsData = featFactory.buildList(3) await FeatModel.insertMany(featsData) const request = createRequest({ query: {} }) const response = createResponse() // Act await FeatController.index(request, response, mockNext) // Assert expect(response.statusCode).toBe(200) const responseData = JSON.parse(response._getData()) expect(responseData.count).toBe(3) expect(responseData.results).toHaveLength(3) expect(responseData.results).toEqual( expect.arrayContaining([ // Index action returns index, name, url expect.objectContaining({ index: featsData[0].index, name: featsData[0].name, url: featsData[0].url }), expect.objectContaining({ index: featsData[1].index, name: featsData[1].name, url: featsData[1].url }), expect.objectContaining({ index: featsData[2].index, name: featsData[2].name, url: featsData[2].url }) ]) ) expect(mockNext).not.toHaveBeenCalled() }) it('handles database errors during find', async () => { // Arrange const request = createRequest({ query: {} }) const response = createResponse() const error = new Error('Database find failed') vi.spyOn(FeatModel, 'find').mockImplementationOnce(() => { const query = { select: vi.fn().mockReturnThis(), sort: vi.fn().mockReturnThis(), exec: vi.fn().mockRejectedValueOnce(error) } as any return query }) // Act await FeatController.index(request, response, mockNext) // Assert expect(response.statusCode).toBe(200) // Controller passes error to next() expect(response._getData()).toBe('') expect(mockNext).toHaveBeenCalledOnce() expect(mockNext).toHaveBeenCalledWith(error) }) it('returns an empty list when no feats exist', async () => { const request = createRequest({ query: {} }) const response = createResponse() await FeatController.index(request, response, mockNext) expect(response.statusCode).toBe(200) const responseData = JSON.parse(response._getData()) expect(responseData.count).toBe(0) expect(responseData.results).toEqual([]) expect(mockNext).not.toHaveBeenCalled() }) }) describe('show', () => { it('returns a single feat when found', async () => { // Arrange const featData = featFactory.build({ index: 'tough', name: 'Tough' }) await FeatModel.insertMany([featData]) const request = createRequest({ params: { index: 'tough' } }) const response = createResponse() // Act await FeatController.show(request, response, mockNext) // Assert expect(response.statusCode).toBe(200) const responseData = JSON.parse(response._getData()) // Check specific fields returned by show expect(responseData).toMatchObject({ index: featData.index, name: featData.name, prerequisites: expect.any(Array), // Check structure if needed desc: featData.desc, url: featData.url }) expect(mockNext).not.toHaveBeenCalled() }) it('calls next() when the feat is not found', async () => { // Arrange const request = createRequest({ params: { index: 'nonexistent' } }) const response = createResponse() // Act await FeatController.show(request, response, mockNext) // Assert expect(response.statusCode).toBe(200) // Passes to next() expect(response._getData()).toBe('') expect(mockNext).toHaveBeenCalledOnce() expect(mockNext).toHaveBeenCalledWith() }) it('handles database errors during findOne', async () => { // Arrange const request = createRequest({ params: { index: 'tough' } }) const response = createResponse() const error = new Error('Database findOne failed') vi.spyOn(FeatModel, 'findOne').mockRejectedValueOnce(error) // Act await FeatController.show(request, response, mockNext) // Assert expect(response.statusCode).toBe(200) // Passes to next() expect(response._getData()).toBe('') expect(mockNext).toHaveBeenCalledOnce() expect(mockNext).toHaveBeenCalledWith(error) }) }) }) ================================================ FILE: src/tests/controllers/api/2014/featureController.test.ts ================================================ import { createRequest, createResponse } from 'node-mocks-http' import { describe, expect, it, vi } from 'vitest' import FeatureController from '@/controllers/api/2014/featureController' import FeatureModel from '@/models/2014/feature' import { featureFactory } from '@/tests/factories/2014/feature.factory' import { mockNext as defaultMockNext } from '@/tests/support' import { generateUniqueDbUri, setupIsolatedDatabase, setupModelCleanup, teardownIsolatedDatabase } from '@/tests/support/db' const mockNext = vi.fn(defaultMockNext) // Generate URI for this test file const dbUri = generateUniqueDbUri('feature') // Setup hooks using helpers setupIsolatedDatabase(dbUri) teardownIsolatedDatabase() setupModelCleanup(FeatureModel) describe('FeatureController', () => { describe('index', () => { it('returns a list of features', async () => { const featuresData = featureFactory.buildList(3) const featureDocs = featuresData.map((data) => new FeatureModel(data)) await FeatureModel.insertMany(featureDocs) const request = createRequest({ query: {} }) const response = createResponse() await FeatureController.index(request, response, mockNext) expect(response.statusCode).toBe(200) const responseData = JSON.parse(response._getData()) expect(responseData.count).toBe(3) expect(responseData.results).toHaveLength(3) expect(responseData.results).toEqual( expect.arrayContaining([ expect.objectContaining({ index: featuresData[0].index, name: featuresData[0].name }), expect.objectContaining({ index: featuresData[1].index, name: featuresData[1].name }), expect.objectContaining({ index: featuresData[2].index, name: featuresData[2].name }) ]) ) expect(mockNext).not.toHaveBeenCalled() }) it('returns an empty list when no features exist', async () => { const request = createRequest({ query: {} }) const response = createResponse() await FeatureController.index(request, response, mockNext) expect(response.statusCode).toBe(200) const responseData = JSON.parse(response._getData()) expect(responseData.count).toBe(0) expect(responseData.results).toEqual([]) expect(mockNext).not.toHaveBeenCalled() }) }) describe('show', () => { it('returns a single feature when found', async () => { const featureData = featureFactory.build({ index: 'action-surge', name: 'Action Surge' }) await FeatureModel.insertMany([featureData]) const request = createRequest({ params: { index: 'action-surge' } }) const response = createResponse() await FeatureController.show(request, response, mockNext) expect(response.statusCode).toBe(200) const responseData = JSON.parse(response._getData()) expect(responseData.index).toBe('action-surge') expect(responseData.name).toBe('Action Surge') expect(responseData.desc).toEqual(featureData.desc) expect(responseData.level).toEqual(featureData.level) expect(responseData.class.index).toEqual(featureData.class.index) expect(mockNext).not.toHaveBeenCalled() }) it('calls next() when the feature is not found', async () => { const request = createRequest({ params: { index: 'nonexistent' } }) const response = createResponse() await FeatureController.show(request, response, mockNext) expect(response.statusCode).toBe(200) expect(response._getData()).toBe('') expect(mockNext).toHaveBeenCalledOnce() expect(mockNext).toHaveBeenCalledWith() }) }) }) ================================================ FILE: src/tests/controllers/api/2014/languageController.test.ts ================================================ import { createRequest, createResponse } from 'node-mocks-http' import { describe, expect, it, vi } from 'vitest' import LanguageController from '@/controllers/api/2014/languageController' import LanguageModel from '@/models/2014/language' import { languageFactory } from '@/tests/factories/2014/language.factory' import { mockNext as defaultMockNext } from '@/tests/support' import { generateUniqueDbUri, setupIsolatedDatabase, setupModelCleanup, teardownIsolatedDatabase } from '@/tests/support/db' const mockNext = vi.fn(defaultMockNext) // Generate URI for this test file const dbUri = generateUniqueDbUri('language') // Setup hooks using helpers setupIsolatedDatabase(dbUri) teardownIsolatedDatabase() setupModelCleanup(LanguageModel) describe('LanguageController', () => { describe('index', () => { it('returns a list of languages', async () => { const languagesData = languageFactory.buildList(3) const languageDocs = languagesData.map((data) => new LanguageModel(data)) await LanguageModel.insertMany(languageDocs) const request = createRequest({ query: {} }) const response = createResponse() await LanguageController.index(request, response, mockNext) expect(response.statusCode).toBe(200) const responseData = JSON.parse(response._getData()) expect(responseData.count).toBe(3) expect(responseData.results).toHaveLength(3) expect(responseData.results).toEqual( expect.arrayContaining([ expect.objectContaining({ index: languagesData[0].index, name: languagesData[0].name }), expect.objectContaining({ index: languagesData[1].index, name: languagesData[1].name }), expect.objectContaining({ index: languagesData[2].index, name: languagesData[2].name }) ]) ) expect(mockNext).not.toHaveBeenCalled() }) it('returns an empty list when no languages exist', async () => { const request = createRequest({ query: {} }) const response = createResponse() await LanguageController.index(request, response, mockNext) expect(response.statusCode).toBe(200) const responseData = JSON.parse(response._getData()) expect(responseData.count).toBe(0) expect(responseData.results).toEqual([]) expect(mockNext).not.toHaveBeenCalled() }) }) describe('show', () => { it('returns a single language when found', async () => { const languageData = languageFactory.build({ index: 'common', name: 'Common' }) await LanguageModel.insertMany([languageData]) const request = createRequest({ params: { index: 'common' } }) const response = createResponse() await LanguageController.show(request, response, mockNext) expect(response.statusCode).toBe(200) const responseData = JSON.parse(response._getData()) expect(responseData.index).toBe('common') expect(responseData.name).toBe('Common') expect(responseData.script).toEqual(languageData.script) expect(responseData.type).toEqual(languageData.type) expect(mockNext).not.toHaveBeenCalled() }) it('calls next() when the language is not found', async () => { const request = createRequest({ params: { index: 'nonexistent' } }) const response = createResponse() await LanguageController.show(request, response, mockNext) expect(response.statusCode).toBe(200) expect(response._getData()).toBe('') expect(mockNext).toHaveBeenCalledOnce() expect(mockNext).toHaveBeenCalledWith() }) }) }) ================================================ FILE: src/tests/controllers/api/2014/magicItemController.test.ts ================================================ import { createRequest, createResponse } from 'node-mocks-http' import { describe, expect, it, vi } from 'vitest' import * as MagicItemController from '@/controllers/api/2014/magicItemController' import MagicItemModel from '@/models/2014/magicItem' import { magicItemFactory } from '@/tests/factories/2014/magicItem.factory' import { mockNext as defaultMockNext } from '@/tests/support' // Assuming mockNext is here based on spell test import { generateUniqueDbUri, setupIsolatedDatabase, setupModelCleanup, teardownIsolatedDatabase } from '@/tests/support/db' const mockNext = vi.fn(defaultMockNext) // Generate URI for this test file const dbUri = generateUniqueDbUri('magicitem') // Setup hooks using helpers setupIsolatedDatabase(dbUri) teardownIsolatedDatabase() setupModelCleanup(MagicItemModel) describe('MagicItemController', () => { describe('index', () => { it('returns a list of magic items', async () => { // Arrange const magicItemsData = magicItemFactory.buildList(3) // Mongoose documents need _id, factory generates data matching the schema without it // Insert directly, letting Mongoose handle _id await MagicItemModel.insertMany(magicItemsData) const request = createRequest({ query: {} }) const response = createResponse() // Act await MagicItemController.index(request, response, mockNext) // Assert expect(response.statusCode).toBe(200) const responseData = JSON.parse(response._getData()) expect(responseData.count).toBe(3) expect(responseData.results).toHaveLength(3) // Check for essential properties returned by the index endpoint expect(responseData.results).toEqual( expect.arrayContaining([ expect.objectContaining({ index: magicItemsData[0].index, name: magicItemsData[0].name, url: magicItemsData[0].url }), expect.objectContaining({ index: magicItemsData[1].index, name: magicItemsData[1].name, url: magicItemsData[1].url }), expect.objectContaining({ index: magicItemsData[2].index, name: magicItemsData[2].name, url: magicItemsData[2].url }) ]) ) expect(mockNext).not.toHaveBeenCalled() }) it('handles database errors during find', async () => { // Arrange const request = createRequest({ query: {} }) const response = createResponse() const error = new Error('Database find failed') // Mock the find method to throw an error vi.spyOn(MagicItemModel, 'find').mockImplementationOnce(() => { const query = { select: vi.fn().mockReturnThis(), sort: vi.fn().mockRejectedValueOnce(error) // Add other methods chained in the controller if necessary } as any return query }) // Act await MagicItemController.index(request, response, mockNext) // Assert // Status code might not be set if error is passed to next() expect(response.statusCode).toBe(200) // Or check if response was sent expect(response._getData()).toBe('') // No data sent on error passed to next() expect(mockNext).toHaveBeenCalledOnce() expect(mockNext).toHaveBeenCalledWith(error) }) it('returns an empty list when no magic items exist', async () => { // Arrange const request = createRequest({ query: {} }) const response = createResponse() // Act await MagicItemController.index(request, response, mockNext) // Assert expect(response.statusCode).toBe(200) const responseData = JSON.parse(response._getData()) expect(responseData.count).toBe(0) expect(responseData.results).toEqual([]) expect(mockNext).not.toHaveBeenCalled() }) // TODO: Add tests for query parameters (e.g., name) if applicable }) describe('show', () => { it('returns a single magic item when found', async () => { // Arrange const itemData = magicItemFactory.build({ index: 'cloak-of-protection', name: 'Cloak of Protection' }) await MagicItemModel.insertMany([itemData]) // Insert as array const request = createRequest({ params: { index: 'cloak-of-protection' } }) const response = createResponse() // Act await MagicItemController.show(request, response, mockNext) // Assert expect(response.statusCode).toBe(200) const responseData = JSON.parse(response._getData()) // Check against the original data, excluding potential virtuals or _id expect(responseData).toMatchObject({ index: itemData.index, name: itemData.name, desc: itemData.desc, url: itemData.url // Add other fields as necessary }) expect(mockNext).not.toHaveBeenCalled() }) it('calls next() when the magic item is not found', async () => { // Arrange const request = createRequest({ params: { index: 'nonexistent-item' } }) const response = createResponse() // Act await MagicItemController.show(request, response, mockNext) // Assert expect(response.statusCode).toBe(200) // Controller doesn't set 404, passes to next() expect(response._getData()).toBe('') // No data sent expect(mockNext).toHaveBeenCalledOnce() expect(mockNext).toHaveBeenCalledWith() // Called with no arguments for default 404 handling }) it('handles database errors during findOne', async () => { // Arrange const request = createRequest({ params: { index: 'any-index' } }) const response = createResponse() const error = new Error('Database findOne failed') // Mock findOne to throw an error vi.spyOn(MagicItemModel, 'findOne').mockRejectedValueOnce(error) // Act await MagicItemController.show(request, response, mockNext) // Assert expect(response.statusCode).toBe(200) expect(response._getData()).toBe('') expect(mockNext).toHaveBeenCalledOnce() expect(mockNext).toHaveBeenCalledWith(error) }) }) }) ================================================ FILE: src/tests/controllers/api/2014/magicSchoolController.test.ts ================================================ import { createRequest, createResponse } from 'node-mocks-http' import { describe, expect, it, vi } from 'vitest' import MagicSchoolController from '@/controllers/api/2014/magicSchoolController' import MagicSchoolModel from '@/models/2014/magicSchool' // Use Model suffix import { magicSchoolFactory } from '@/tests/factories/2014/magicSchool.factory' // Updated path import { mockNext as defaultMockNext } from '@/tests/support' // Assuming support helper location import { generateUniqueDbUri, setupIsolatedDatabase, setupModelCleanup, teardownIsolatedDatabase } from '@/tests/support/db' const mockNext = vi.fn(defaultMockNext) // Generate URI for this test file const dbUri = generateUniqueDbUri('magicschool') // Setup hooks using helpers setupIsolatedDatabase(dbUri) teardownIsolatedDatabase() setupModelCleanup(MagicSchoolModel) describe('MagicSchoolController', () => { describe('index', () => { it('returns a list of magic schools', async () => { // Arrange const schoolsData = magicSchoolFactory.buildList(3) await MagicSchoolModel.insertMany(schoolsData) const request = createRequest({ query: {} }) const response = createResponse() // Act await MagicSchoolController.index(request, response, mockNext) // Assert expect(response.statusCode).toBe(200) const responseData = JSON.parse(response._getData()) expect(responseData.count).toBe(3) expect(responseData.results).toHaveLength(3) expect(responseData.results).toEqual( expect.arrayContaining([ expect.objectContaining({ index: schoolsData[0].index, name: schoolsData[0].name }), expect.objectContaining({ index: schoolsData[1].index, name: schoolsData[1].name }), expect.objectContaining({ index: schoolsData[2].index, name: schoolsData[2].name }) ]) ) expect(mockNext).not.toHaveBeenCalled() }) it('returns an empty list when no magic schools exist', async () => { const request = createRequest({ query: {} }) const response = createResponse() await MagicSchoolController.index(request, response, mockNext) expect(response.statusCode).toBe(200) const responseData = JSON.parse(response._getData()) expect(responseData.count).toBe(0) expect(responseData.results).toEqual([]) expect(mockNext).not.toHaveBeenCalled() }) // Add error handling test if necessary }) describe('show', () => { it('returns a single magic school when found', async () => { // Arrange const schoolData = magicSchoolFactory.build({ index: 'evocation', name: 'Evocation' }) await MagicSchoolModel.insertMany([schoolData]) const request = createRequest({ params: { index: 'evocation' } }) const response = createResponse() // Act await MagicSchoolController.show(request, response, mockNext) // Assert expect(response.statusCode).toBe(200) const responseData = JSON.parse(response._getData()) expect(responseData.index).toBe('evocation') expect(responseData.name).toBe('Evocation') expect(responseData.desc).toEqual(schoolData.desc) expect(mockNext).not.toHaveBeenCalled() }) it('calls next() when the magic school is not found', async () => { const request = createRequest({ params: { index: 'nonexistent' } }) const response = createResponse() await MagicSchoolController.show(request, response, mockNext) expect(response.statusCode).toBe(200) // Controller passes to next() expect(response._getData()).toBe('') expect(mockNext).toHaveBeenCalledOnce() expect(mockNext).toHaveBeenCalledWith() // Default 404 handling }) // Add error handling test if necessary }) }) ================================================ FILE: src/tests/controllers/api/2014/monsterController.test.ts ================================================ import { createRequest, createResponse } from 'node-mocks-http' import { describe, expect, it, vi } from 'vitest' import * as MonsterController from '@/controllers/api/2014/monsterController' import MonsterModel from '@/models/2014/monster' import { monsterFactory } from '@/tests/factories/2014/monster.factory' import { mockNext as defaultMockNext } from '@/tests/support' // DB Helper Imports import { generateUniqueDbUri, setupIsolatedDatabase, setupModelCleanup, teardownIsolatedDatabase } from '@/tests/support/db' // Remove redis mock - Integration tests will hit the real DB // vi.mock('@/util', ...) const mockNext = vi.fn(defaultMockNext) // Setup DB isolation const dbUri = generateUniqueDbUri('monster') setupIsolatedDatabase(dbUri) teardownIsolatedDatabase() setupModelCleanup(MonsterModel) // Removed createMockQuery helper describe('MonsterController', () => { describe('index', () => { it('returns a list of monsters with default query', async () => { // Arrange: Seed the database const monstersData = monsterFactory.buildList(3) await MonsterModel.insertMany(monstersData) const request = createRequest({ query: {}, originalUrl: '/api/monsters' }) const response = createResponse() // Act await MonsterController.index(request, response, mockNext) // Assert: Check response based on seeded data expect(response.statusCode).toBe(200) const responseData = JSON.parse(response._getData()) expect(responseData.count).toBe(3) expect(responseData.results).toHaveLength(3) expect(responseData.results).toEqual( expect.arrayContaining([ expect.objectContaining({ index: monstersData[0].index, name: monstersData[0].name }), expect.objectContaining({ index: monstersData[1].index, name: monstersData[1].name }), expect.objectContaining({ index: monstersData[2].index, name: monstersData[2].name }) ]) ) expect(mockNext).not.toHaveBeenCalled() }) // Keep challenge_rating tests, but they now hit the DB describe('with challenge_rating query', () => { const crTestCases = [ { input: '5', expectedCount: 1, seedCRs: [5, 1, 2] }, { input: '1,10,0.25', expectedCount: 2, seedCRs: [1, 10, 5] }, { input: ['2', '4'], expectedCount: 2, seedCRs: [2, 4, 5] }, { input: 'abc,3,def,0.5', expectedCount: 2, seedCRs: [3, 0.5, 1] }, { input: ['1', 'xyz', '5'], expectedCount: 2, seedCRs: [1, 5, 10] }, { input: 'invalid, nope', expectedCount: 3, seedCRs: [1, 2, 3] }, // Expect all when filter is invalid { input: '', expectedCount: 3, seedCRs: [1, 2, 3] } // Expect all when filter is empty ] it.each(crTestCases)( 'handles challenge rating: $input', async ({ input, expectedCount, seedCRs }) => { // Arrange: Seed specific CRs const monstersToSeed = seedCRs.map((cr) => monsterFactory.build({ challenge_rating: cr })) await MonsterModel.insertMany(monstersToSeed) const request = createRequest({ query: { challenge_rating: input }, originalUrl: '/api/monsters' }) const response = createResponse() // Act await MonsterController.index(request, response, mockNext) // Assert expect(response.statusCode).toBe(200) const responseData = JSON.parse(response._getData()) expect(responseData.count).toBe(expectedCount) expect(responseData.results).toHaveLength(expectedCount) // Optional: Add more specific checks on returned indices if needed expect(mockNext).not.toHaveBeenCalled() } ) }) // No need for explicit DB error mocking now, handled by helpers/real errors // describe('when something goes wrong', ...) // Redis tests are removed as we aren't mocking redis here anymore // describe('when data is in Redis cache', ...) }) describe('show', () => { it('returns a monster object', async () => { // Arrange const monsterData = monsterFactory.build({ index: 'goblin' }) await MonsterModel.insertMany([monsterData]) const request = createRequest({ params: { index: 'goblin' } }) const response = createResponse() // Act await MonsterController.show(request, response, mockNext) // Assert expect(response.statusCode).toBe(200) const responseData = JSON.parse(response._getData()) expect(responseData.index).toBe('goblin') // Add more detailed checks as needed expect(responseData).toHaveProperty('challenge_rating', monsterData.challenge_rating) expect(mockNext).not.toHaveBeenCalled() }) it('calls next() when the record does not exist', async () => { // Arrange const request = createRequest({ params: { index: 'non-existent' } }) const response = createResponse() // Act await MonsterController.show(request, response, mockNext) // Assert expect(response.statusCode).toBe(200) // Controller passes to next expect(response._getData()).toBe('') expect(mockNext).toHaveBeenCalledOnce() expect(mockNext).toHaveBeenCalledWith() // Default 404 handling }) // No need for explicit DB error mocking // describe('when something goes wrong', ...) }) }) ================================================ FILE: src/tests/controllers/api/2014/proficiencyController.test.ts ================================================ import { createRequest, createResponse } from 'node-mocks-http' import { describe, expect, it, vi } from 'vitest' import ProficiencyController from '@/controllers/api/2014/proficiencyController' import ProficiencyModel from '@/models/2014/proficiency' import { proficiencyFactory } from '@/tests/factories/2014/proficiency.factory' import { mockNext as defaultMockNext } from '@/tests/support' import { generateUniqueDbUri, setupIsolatedDatabase, setupModelCleanup, teardownIsolatedDatabase } from '@/tests/support/db' const mockNext = vi.fn(defaultMockNext) // Generate URI for this test file const dbUri = generateUniqueDbUri('proficiency') // Setup hooks using helpers setupIsolatedDatabase(dbUri) teardownIsolatedDatabase() setupModelCleanup(ProficiencyModel) describe('ProficiencyController', () => { describe('index', () => { it('returns a list of proficiencies', async () => { // Arrange const proficienciesData = proficiencyFactory.buildList(3) await ProficiencyModel.insertMany(proficienciesData) const request = createRequest({ query: {} }) const response = createResponse() // Act await ProficiencyController.index(request, response, mockNext) // Assert expect(response.statusCode).toBe(200) const responseData = JSON.parse(response._getData()) expect(responseData.count).toBe(3) expect(responseData.results).toHaveLength(3) expect(responseData.results).toEqual( expect.arrayContaining([ expect.objectContaining({ index: proficienciesData[0].index, name: proficienciesData[0].name }), expect.objectContaining({ index: proficienciesData[1].index, name: proficienciesData[1].name }), expect.objectContaining({ index: proficienciesData[2].index, name: proficienciesData[2].name }) ]) ) expect(mockNext).not.toHaveBeenCalled() }) it('returns an empty list when no proficiencies exist', async () => { const request = createRequest({ query: {} }) const response = createResponse() await ProficiencyController.index(request, response, mockNext) expect(response.statusCode).toBe(200) const responseData = JSON.parse(response._getData()) expect(responseData.count).toBe(0) expect(responseData.results).toEqual([]) expect(mockNext).not.toHaveBeenCalled() }) }) describe('show', () => { it('returns a single proficiency when found', async () => { // Arrange const proficiencyData = proficiencyFactory.build({ index: 'saving-throw-str', name: 'Saving Throw: STR' }) await ProficiencyModel.insertMany([proficiencyData]) const request = createRequest({ params: { index: 'saving-throw-str' } }) const response = createResponse() // Act await ProficiencyController.show(request, response, mockNext) // Assert expect(response.statusCode).toBe(200) const responseData = JSON.parse(response._getData()) expect(responseData.index).toBe('saving-throw-str') expect(responseData.name).toBe('Saving Throw: STR') // Add more specific assertions based on the model structure expect(responseData.type).toEqual(proficiencyData.type) // Check optional fields only if they exist on the source data if (proficiencyData.classes) { expect(responseData.classes).toEqual( expect.arrayContaining( proficiencyData.classes.map((c) => expect.objectContaining({ index: c.index })) ) ) } else { expect(responseData.classes).toEqual([]) // Or appropriate default } if (proficiencyData.races) { expect(responseData.races).toEqual( expect.arrayContaining( proficiencyData.races.map((r) => expect.objectContaining({ index: r.index })) ) ) } else { expect(responseData.races).toEqual([]) // Or appropriate default } expect(responseData.reference.index).toEqual(proficiencyData.reference.index) expect(mockNext).not.toHaveBeenCalled() }) it('calls next() when the proficiency is not found', async () => { const request = createRequest({ params: { index: 'nonexistent' } }) const response = createResponse() await ProficiencyController.show(request, response, mockNext) expect(response.statusCode).toBe(200) // Passes to next() expect(response._getData()).toBe('') expect(mockNext).toHaveBeenCalledOnce() expect(mockNext).toHaveBeenCalledWith() // Default 404 handling }) }) }) ================================================ FILE: src/tests/controllers/api/2014/raceController.test.ts ================================================ import { createRequest, createResponse } from 'node-mocks-http' import { describe, expect, it, vi } from 'vitest' import * as RaceController from '@/controllers/api/2014/raceController' import ProficiencyModel from '@/models/2014/proficiency' import RaceModel from '@/models/2014/race' import SubraceModel from '@/models/2014/subrace' import TraitModel from '@/models/2014/trait' import { proficiencyFactory } from '@/tests/factories/2014/proficiency.factory' import { raceFactory } from '@/tests/factories/2014/race.factory' import { subraceFactory } from '@/tests/factories/2014/subrace.factory' import { traitFactory } from '@/tests/factories/2014/trait.factory' import { mockNext as defaultMockNext } from '@/tests/support' import { generateUniqueDbUri, setupIsolatedDatabase, setupModelCleanup, teardownIsolatedDatabase } from '@/tests/support/db' const mockNext = vi.fn(defaultMockNext) // Setup DB isolation const dbUri = generateUniqueDbUri('race') setupIsolatedDatabase(dbUri) teardownIsolatedDatabase() // Cleanup all relevant models setupModelCleanup(RaceModel) setupModelCleanup(SubraceModel) setupModelCleanup(TraitModel) setupModelCleanup(ProficiencyModel) describe('RaceController', () => { describe('index', () => { it('returns a list of races', async () => { // Arrange: Seed DB const racesData = raceFactory.buildList(3) await RaceModel.insertMany(racesData) const request = createRequest({ query: {} }) const response = createResponse() // Act await RaceController.index(request, response, mockNext) // Assert: Check response based on seeded data expect(response.statusCode).toBe(200) const responseData = JSON.parse(response._getData()) expect(responseData.count).toBe(3) expect(responseData.results).toHaveLength(3) expect(responseData.results).toEqual( expect.arrayContaining([ expect.objectContaining({ index: racesData[0].index, name: racesData[0].name }), expect.objectContaining({ index: racesData[1].index, name: racesData[1].name }), expect.objectContaining({ index: racesData[2].index, name: racesData[2].name }) ]) ) expect(mockNext).not.toHaveBeenCalled() }) // describe('when something goes wrong', ...) }) describe('show', () => { it('returns a single race', async () => { // Arrange const raceData = raceFactory.build({ index: 'human' }) await RaceModel.insertMany([raceData]) const request = createRequest({ params: { index: 'human' } }) const response = createResponse() // Act await RaceController.show(request, response, mockNext) // Assert expect(response.statusCode).toBe(200) const responseData = JSON.parse(response._getData()) expect(responseData.index).toEqual(raceData.index) // Add more specific checks expect(responseData.name).toEqual(raceData.name) expect(responseData.speed).toEqual(raceData.speed) expect(mockNext).not.toHaveBeenCalled() }) it('calls next() when the record does not exist', async () => { // Arrange const request = createRequest({ params: { index: 'non-existent' } }) const response = createResponse() // Act await RaceController.show(request, response, mockNext) // Assert expect(mockNext).toHaveBeenCalledWith() expect(response._getData()).toBe('') }) // describe('when something goes wrong', ...) }) describe('showSubracesForRace', () => { const raceIndex = 'dwarf' const raceUrl = `/api/2014/races/${raceIndex}` it('returns a list of subraces for the race', async () => { // Arrange const raceRef = { index: raceIndex, name: 'Dwarf', url: raceUrl } const subracesData = subraceFactory.buildList(2, { race: raceRef }) await SubraceModel.insertMany(subracesData) // Seed an unrelated subrace await SubraceModel.insertMany(subraceFactory.buildList(1)) const request = createRequest({ params: { index: raceIndex } }) const response = createResponse() // Act await RaceController.showSubracesForRace(request, response, mockNext) // Assert expect(response.statusCode).toBe(200) const responseData = JSON.parse(response._getData()) expect(responseData.count).toBe(2) expect(responseData.results).toHaveLength(2) expect(responseData.results.map((r: any) => r.index)).toEqual( expect.arrayContaining([subracesData[0].index, subracesData[1].index]) ) expect(mockNext).not.toHaveBeenCalled() }) // describe('when something goes wrong', ...) }) describe('showTraitsForRace', () => { const raceIndex = 'elf' const raceUrl = `/api/2014/races/${raceIndex}` it('returns a list of traits for the race', async () => { // Arrange const raceRef = { index: raceIndex, name: 'Elf', url: raceUrl } const traitsData = traitFactory.buildList(3, { races: [raceRef] }) await TraitModel.insertMany(traitsData) // Seed unrelated traits await TraitModel.insertMany(traitFactory.buildList(2)) const request = createRequest({ params: { index: raceIndex } }) const response = createResponse() // Act await RaceController.showTraitsForRace(request, response, mockNext) // Assert expect(response.statusCode).toBe(200) const responseData = JSON.parse(response._getData()) expect(responseData.count).toBe(3) expect(responseData.results).toHaveLength(3) expect(responseData.results.map((t: any) => t.index)).toEqual( expect.arrayContaining([traitsData[0].index, traitsData[1].index, traitsData[2].index]) ) expect(mockNext).not.toHaveBeenCalled() }) // describe('when something goes wrong', ...) }) describe('showProficienciesForRace', () => { const raceIndex = 'halfling' const raceUrl = `/api/2014/races/${raceIndex}` it('returns a list of proficiencies for the race', async () => { // Arrange const raceRef = { index: raceIndex, name: 'Halfling', url: raceUrl } const proficienciesData = proficiencyFactory.buildList(4, { races: [raceRef] }) await ProficiencyModel.insertMany(proficienciesData) // Seed unrelated proficiencies await ProficiencyModel.insertMany(proficiencyFactory.buildList(2)) const request = createRequest({ params: { index: raceIndex } }) const response = createResponse() // Act await RaceController.showProficienciesForRace(request, response, mockNext) // Assert expect(response.statusCode).toBe(200) const responseData = JSON.parse(response._getData()) expect(responseData.count).toBe(4) expect(responseData.results).toHaveLength(4) expect(responseData.results.map((p: any) => p.index)).toEqual( expect.arrayContaining([ proficienciesData[0].index, proficienciesData[1].index, proficienciesData[2].index, proficienciesData[3].index ]) ) expect(mockNext).not.toHaveBeenCalled() }) // describe('when something goes wrong', ...) }) }) ================================================ FILE: src/tests/controllers/api/2014/ruleSectionController.test.ts ================================================ import { createRequest, createResponse } from 'node-mocks-http' import { describe, expect, it, vi } from 'vitest' import * as RuleSectionController from '@/controllers/api/2014/ruleSectionController' import RuleSectionModel from '@/models/2014/ruleSection' // Use Model suffix import { ruleSectionFactory } from '@/tests/factories/2014/ruleSection.factory' // Updated path import { mockNext as defaultMockNext } from '@/tests/support' // Assuming support helper location import { generateUniqueDbUri, setupIsolatedDatabase, setupModelCleanup, teardownIsolatedDatabase } from '@/tests/support/db' const mockNext = vi.fn(defaultMockNext) // Generate URI for this test file const dbUri = generateUniqueDbUri('rulesection') // Setup hooks using helpers setupIsolatedDatabase(dbUri) teardownIsolatedDatabase() setupModelCleanup(RuleSectionModel) describe('RuleSectionController', () => { describe('index', () => { it('returns a list of rule sections', async () => { // Arrange const ruleSectionsData = ruleSectionFactory.buildList(3) await RuleSectionModel.insertMany(ruleSectionsData) const request = createRequest({ query: {} }) const response = createResponse() // Act await RuleSectionController.index(request, response, mockNext) // Assert expect(response.statusCode).toBe(200) const responseData = JSON.parse(response._getData()) expect(responseData.count).toBe(3) expect(responseData.results).toHaveLength(3) expect(responseData.results).toEqual( expect.arrayContaining([ expect.objectContaining({ index: ruleSectionsData[0].index, name: ruleSectionsData[0].name }), expect.objectContaining({ index: ruleSectionsData[1].index, name: ruleSectionsData[1].name }), expect.objectContaining({ index: ruleSectionsData[2].index, name: ruleSectionsData[2].name }) ]) ) expect(mockNext).not.toHaveBeenCalled() }) it('returns an empty list when no rule sections exist', async () => { const request = createRequest({ query: {} }) const response = createResponse() await RuleSectionController.index(request, response, mockNext) expect(response.statusCode).toBe(200) const responseData = JSON.parse(response._getData()) expect(responseData.count).toBe(0) expect(responseData.results).toEqual([]) expect(mockNext).not.toHaveBeenCalled() }) }) describe('show', () => { it('returns a single rule section when found', async () => { // Arrange const ruleSectionData = ruleSectionFactory.build({ index: 'adventuring', name: 'Adventuring' }) await RuleSectionModel.insertMany([ruleSectionData]) const request = createRequest({ params: { index: 'adventuring' } }) const response = createResponse() // Act await RuleSectionController.show(request, response, mockNext) // Assert expect(response.statusCode).toBe(200) const responseData = JSON.parse(response._getData()) expect(responseData.index).toBe('adventuring') expect(responseData.name).toBe('Adventuring') expect(responseData.desc).toEqual(ruleSectionData.desc) expect(mockNext).not.toHaveBeenCalled() }) it('calls next() when the rule section is not found', async () => { const request = createRequest({ params: { index: 'nonexistent' } }) const response = createResponse() await RuleSectionController.show(request, response, mockNext) expect(response.statusCode).toBe(200) // Passes to next() expect(response._getData()).toBe('') expect(mockNext).toHaveBeenCalledOnce() expect(mockNext).toHaveBeenCalledWith() // Default 404 handling }) }) }) ================================================ FILE: src/tests/controllers/api/2014/rulesController.test.ts ================================================ import { createRequest, createResponse } from 'node-mocks-http' import { describe, expect, it, vi } from 'vitest' // Import specific functions from the correct controller file import * as RuleController from '@/controllers/api/2014/ruleController' import RuleModel from '@/models/2014/rule' // Use Model suffix import { ruleFactory } from '@/tests/factories/2014/rule.factory' // Updated path import { mockNext as defaultMockNext } from '@/tests/support' import { generateUniqueDbUri, setupIsolatedDatabase, setupModelCleanup, teardownIsolatedDatabase } from '@/tests/support/db' const mockNext = vi.fn(defaultMockNext) // Generate URI for this test file const dbUri = generateUniqueDbUri('rule') // Setup hooks using helpers setupIsolatedDatabase(dbUri) teardownIsolatedDatabase() setupModelCleanup(RuleModel) describe('RuleController', () => { // Updated describe block name describe('index', () => { it('returns a list of rules', async () => { // Arrange const rulesData = ruleFactory.buildList(3) const ruleDocs = rulesData.map((data) => new RuleModel(data)) await RuleModel.insertMany(ruleDocs) const request = createRequest({ query: {} }) const response = createResponse() // Act await RuleController.index(request, response, mockNext) // Assert expect(response.statusCode).toBe(200) const responseData = JSON.parse(response._getData()) expect(responseData.count).toBe(3) expect(responseData.results).toHaveLength(3) expect(responseData.results).toEqual( expect.arrayContaining([ expect.objectContaining({ index: rulesData[0].index, name: rulesData[0].name }), expect.objectContaining({ index: rulesData[1].index, name: rulesData[1].name }), expect.objectContaining({ index: rulesData[2].index, name: rulesData[2].name }) ]) ) expect(mockNext).not.toHaveBeenCalled() }) it('returns an empty list when no rules exist', async () => { // Arrange const request = createRequest({ query: {} }) const response = createResponse() // Act await RuleController.index(request, response, mockNext) // Assert expect(response.statusCode).toBe(200) const responseData = JSON.parse(response._getData()) expect(responseData.count).toBe(0) expect(responseData.results).toEqual([]) expect(mockNext).not.toHaveBeenCalled() }) }) describe('show', () => { it('returns a single rule when found', async () => { // Arrange const ruleData = ruleFactory.build({ index: 'combat', name: 'Combat' }) await RuleModel.insertMany([ruleData]) // Use insertMany workaround const request = createRequest({ params: { index: 'combat' } }) const response = createResponse() // Act await RuleController.show(request, response, mockNext) // Assert expect(response.statusCode).toBe(200) const responseData = JSON.parse(response._getData()) expect(responseData.index).toBe('combat') expect(responseData.name).toBe('Combat') expect(responseData.desc).toEqual(ruleData.desc) expect(responseData.subsections).toHaveLength(ruleData.subsections.length) expect(mockNext).not.toHaveBeenCalled() }) it('calls next() when the rule is not found', async () => { // Arrange const request = createRequest({ params: { index: 'nonexistent' } }) const response = createResponse() // Act await RuleController.show(request, response, mockNext) // Assert expect(response.statusCode).toBe(200) expect(response._getData()).toBe('') expect(mockNext).toHaveBeenCalledOnce() expect(mockNext).toHaveBeenCalledWith() }) }) }) ================================================ FILE: src/tests/controllers/api/2014/skillController.test.ts ================================================ import { createRequest, createResponse } from 'node-mocks-http' import { describe, expect, it, vi } from 'vitest' import SkillController from '@/controllers/api/2014/skillController' import SkillModel from '@/models/2014/skill' // Use Model suffix import { skillFactory } from '@/tests/factories/2014/skill.factory' // Updated path import { mockNext as defaultMockNext } from '@/tests/support' // Assuming support helper location import { generateUniqueDbUri, setupIsolatedDatabase, setupModelCleanup, teardownIsolatedDatabase } from '@/tests/support/db' const mockNext = vi.fn(defaultMockNext) // Generate URI for this test file const dbUri = generateUniqueDbUri('skill') // Setup hooks using helpers setupIsolatedDatabase(dbUri) teardownIsolatedDatabase() setupModelCleanup(SkillModel) describe('SkillController', () => { describe('index', () => { it('returns a list of skills', async () => { // Arrange const skillsData = skillFactory.buildList(3) await SkillModel.insertMany(skillsData) const request = createRequest({ query: {} }) const response = createResponse() // Act await SkillController.index(request, response, mockNext) // Assert expect(response.statusCode).toBe(200) const responseData = JSON.parse(response._getData()) expect(responseData.count).toBe(3) expect(responseData.results).toHaveLength(3) expect(responseData.results).toEqual( expect.arrayContaining([ expect.objectContaining({ index: skillsData[0].index, name: skillsData[0].name }), expect.objectContaining({ index: skillsData[1].index, name: skillsData[1].name }), expect.objectContaining({ index: skillsData[2].index, name: skillsData[2].name }) ]) ) expect(mockNext).not.toHaveBeenCalled() }) it('returns an empty list when no skills exist', async () => { const request = createRequest({ query: {} }) const response = createResponse() await SkillController.index(request, response, mockNext) expect(response.statusCode).toBe(200) const responseData = JSON.parse(response._getData()) expect(responseData.count).toBe(0) expect(responseData.results).toEqual([]) expect(mockNext).not.toHaveBeenCalled() }) }) describe('show', () => { it('returns a single skill when found', async () => { // Arrange const skillData = skillFactory.build({ index: 'athletics', name: 'Athletics' }) await SkillModel.insertMany([skillData]) const request = createRequest({ params: { index: 'athletics' } }) const response = createResponse() // Act await SkillController.show(request, response, mockNext) // Assert expect(response.statusCode).toBe(200) const responseData = JSON.parse(response._getData()) expect(responseData.index).toBe('athletics') expect(responseData.name).toBe('Athletics') expect(responseData.desc).toEqual(skillData.desc) // Check nested object expect(responseData.ability_score.index).toEqual(skillData.ability_score.index) expect(mockNext).not.toHaveBeenCalled() }) it('calls next() when the skill is not found', async () => { const request = createRequest({ params: { index: 'nonexistent' } }) const response = createResponse() await SkillController.show(request, response, mockNext) expect(response.statusCode).toBe(200) // Passes to next() expect(response._getData()).toBe('') expect(mockNext).toHaveBeenCalledOnce() expect(mockNext).toHaveBeenCalledWith() // Default 404 handling }) }) }) ================================================ FILE: src/tests/controllers/api/2014/spellController.test.ts ================================================ import { createRequest, createResponse } from 'node-mocks-http' import { describe, expect, it, vi } from 'vitest' import * as SpellController from '@/controllers/api/2014/spellController' import SpellModel from '@/models/2014/spell' import { spellFactory } from '@/tests/factories/2014/spell.factory' import { mockNext as defaultMockNext } from '@/tests/support' // Import the DB helper functions import { generateUniqueDbUri, setupIsolatedDatabase, setupModelCleanup, teardownIsolatedDatabase } from '@/tests/support/db' const mockNext = vi.fn(defaultMockNext) // Generate URI for this test file const dbUri = generateUniqueDbUri('spell') // Setup hooks using helpers setupIsolatedDatabase(dbUri) teardownIsolatedDatabase() setupModelCleanup(SpellModel) describe('SpellController', () => { describe('index', () => { it('returns a list of spells', async () => { // Arrange const spellsData = spellFactory.buildList(3) // Use insertMany directly await SpellModel.insertMany(spellsData) const request = createRequest({ query: {} }) const response = createResponse() // Act await SpellController.index(request, response, mockNext) // Assert expect(response.statusCode).toBe(200) const responseData = JSON.parse(response._getData()) expect(responseData.count).toBe(3) expect(responseData.results).toHaveLength(3) expect(responseData.results).toEqual( expect.arrayContaining([ expect.objectContaining({ index: spellsData[0].index, name: spellsData[0].name }), expect.objectContaining({ index: spellsData[1].index, name: spellsData[1].name }), expect.objectContaining({ index: spellsData[2].index, name: spellsData[2].name }) ]) ) expect(mockNext).not.toHaveBeenCalled() }) // Filters test remains the same it('filters spells by level', async () => { const spellsData = [ spellFactory.build({ level: 1, name: 'Spell A' }), spellFactory.build({ level: 2, name: 'Spell B' }), spellFactory.build({ level: 1, name: 'Spell C' }) ] await SpellModel.insertMany(spellsData) const request = createRequest({ query: { level: '1' } }) const response = createResponse() await SpellController.index(request, response, mockNext) expect(response.statusCode).toBe(200) const responseData = JSON.parse(response._getData()) expect(responseData.count).toBe(2) expect(responseData.results).toHaveLength(2) expect(responseData.results.map((r: any) => r.index)).toEqual( expect.arrayContaining([spellsData[0].index, spellsData[2].index]) ) expect(mockNext).not.toHaveBeenCalled() }) it('returns an empty list when no spells exist', async () => { // Arrange const request = createRequest({ query: {} }) const response = createResponse() // Act await SpellController.index(request, response, mockNext) // Assert expect(response.statusCode).toBe(200) const responseData = JSON.parse(response._getData()) expect(responseData.count).toBe(0) expect(responseData.results).toEqual([]) expect(mockNext).not.toHaveBeenCalled() }) }) describe('show', () => { it('returns a single spell when found', async () => { // Arrange const spellData = spellFactory.build({ index: 'fireball', name: 'Fireball' }) await SpellModel.insertMany([spellData]) const request = createRequest({ params: { index: 'fireball' } }) const response = createResponse() // Act await SpellController.show(request, response, mockNext) // Assert expect(response.statusCode).toBe(200) const responseData = JSON.parse(response._getData()) expect(responseData.index).toBe('fireball') expect(responseData.name).toBe('Fireball') expect(responseData.desc).toEqual(spellData.desc) expect(responseData.level).toEqual(spellData.level) expect(mockNext).not.toHaveBeenCalled() }) it('calls next() when the spell is not found', async () => { // Arrange const request = createRequest({ params: { index: 'nonexistent' } }) const response = createResponse() // Act await SpellController.show(request, response, mockNext) // Assert expect(response.statusCode).toBe(200) expect(response._getData()).toBe('') expect(mockNext).toHaveBeenCalledOnce() expect(mockNext).toHaveBeenCalledWith() }) }) }) ================================================ FILE: src/tests/controllers/api/2014/subclassController.test.ts ================================================ import { createRequest, createResponse } from 'node-mocks-http' import { describe, expect, it, vi } from 'vitest' import * as SubclassController from '@/controllers/api/2014/subclassController' import FeatureModel from '@/models/2014/feature' import LevelModel from '@/models/2014/level' import SubclassModel from '@/models/2014/subclass' import { apiReferenceFactory } from '@/tests/factories/2014/common.factory' import { featureFactory } from '@/tests/factories/2014/feature.factory' import { levelFactory } from '@/tests/factories/2014/level.factory' import { subclassFactory } from '@/tests/factories/2014/subclass.factory' import { mockNext as defaultMockNext } from '@/tests/support' import { generateUniqueDbUri, setupIsolatedDatabase, setupModelCleanup, teardownIsolatedDatabase } from '@/tests/support/db' const mockNext = vi.fn(defaultMockNext) // Generate URI for this test file const dbUri = generateUniqueDbUri('subclass') // Setup hooks using helpers setupIsolatedDatabase(dbUri) teardownIsolatedDatabase() // Setup cleanup for all relevant models setupModelCleanup(SubclassModel) setupModelCleanup(LevelModel) setupModelCleanup(FeatureModel) describe('SubclassController', () => { describe('index', () => { it('returns a list of subclasses', async () => { // Arrange const subclassesData = subclassFactory.buildList(3) await SubclassModel.insertMany(subclassesData) const request = createRequest({ query: {} }) const response = createResponse() // Act await SubclassController.index(request, response, mockNext) // Assert expect(response.statusCode).toBe(200) const responseData = JSON.parse(response._getData()) expect(responseData.count).toBe(3) expect(responseData.results).toHaveLength(3) expect(responseData.results).toEqual( expect.arrayContaining([ // Index returns index, name, url expect.objectContaining({ index: subclassesData[0].index, name: subclassesData[0].name, url: subclassesData[0].url }), expect.objectContaining({ index: subclassesData[1].index, name: subclassesData[1].name, url: subclassesData[1].url }), expect.objectContaining({ index: subclassesData[2].index, name: subclassesData[2].name, url: subclassesData[2].url }) ]) ) expect(mockNext).not.toHaveBeenCalled() }) it('handles database errors during find', async () => { // Arrange const request = createRequest({ query: {} }) const response = createResponse() const error = new Error('Database find failed') vi.spyOn(SubclassModel, 'find').mockImplementationOnce(() => { const query = { select: vi.fn().mockReturnThis(), sort: vi.fn().mockReturnThis(), exec: vi.fn().mockRejectedValueOnce(error) } as any return query }) // Act await SubclassController.index(request, response, mockNext) // Assert expect(response.statusCode).toBe(200) expect(response._getData()).toBe('') expect(mockNext).toHaveBeenCalledOnce() expect(mockNext).toHaveBeenCalledWith(error) }) it('returns an empty list when no subclasses exist', async () => { const request = createRequest({ query: {} }) const response = createResponse() await SubclassController.index(request, response, mockNext) expect(response.statusCode).toBe(200) const responseData = JSON.parse(response._getData()) expect(responseData.count).toBe(0) expect(responseData.results).toEqual([]) expect(mockNext).not.toHaveBeenCalled() }) }) describe('show', () => { it('returns a single subclass when found', async () => { // Arrange const subclassData = subclassFactory.build({ index: 'berserker' }) await SubclassModel.insertMany([subclassData]) const request = createRequest({ params: { index: 'berserker' } }) const response = createResponse() // Act await SubclassController.show(request, response, mockNext) // Assert expect(response.statusCode).toBe(200) const responseData = JSON.parse(response._getData()) expect(responseData).toMatchObject({ index: subclassData.index, name: subclassData.name, url: subclassData.url }) expect(mockNext).not.toHaveBeenCalled() }) it('calls next() when the subclass is not found', async () => { const request = createRequest({ params: { index: 'nonexistent' } }) const response = createResponse() await SubclassController.show(request, response, mockNext) expect(response.statusCode).toBe(200) expect(response._getData()).toBe('') expect(mockNext).toHaveBeenCalledOnce() expect(mockNext).toHaveBeenCalledWith() }) it('handles database errors during findOne', async () => { const request = createRequest({ params: { index: 'berserker' } }) const response = createResponse() const error = new Error('Database findOne failed') vi.spyOn(SubclassModel, 'findOne').mockRejectedValueOnce(error) await SubclassController.show(request, response, mockNext) expect(response.statusCode).toBe(200) expect(response._getData()).toBe('') expect(mockNext).toHaveBeenCalledOnce() expect(mockNext).toHaveBeenCalledWith(error) }) }) describe('showLevelsForSubclass', () => { const subclassIndex = 'berserker' const subclassUrl = `/api/2014/subclasses/${subclassIndex}` it('returns levels for a specific subclass', async () => { // Arrange const subclassData = subclassFactory.build({ index: subclassIndex, url: subclassUrl }) await SubclassModel.insertMany([subclassData]) // Create levels specifically linked to this subclass const levelsData = levelFactory.buildList(3, { subclass: { index: subclassIndex, name: subclassData.name, url: subclassUrl } }) await LevelModel.insertMany(levelsData) // Add some unrelated levels to ensure filtering works await LevelModel.insertMany(levelFactory.buildList(2)) const request = createRequest({ params: { index: subclassIndex } }) const response = createResponse() // Act await SubclassController.showLevelsForSubclass(request, response, mockNext) // Assert expect(response.statusCode).toBe(200) const responseData = JSON.parse(response._getData()) expect(responseData).toBeInstanceOf(Array) expect(responseData).toHaveLength(3) // Only the 3 levels for this subclass // Check if the returned levels match the ones created for the subclass expect(responseData).toEqual( expect.arrayContaining([ expect.objectContaining({ index: levelsData[0].index, level: levelsData[0].level, url: levelsData[0].url }), expect.objectContaining({ index: levelsData[1].index, level: levelsData[1].level, url: levelsData[1].url }), expect.objectContaining({ index: levelsData[2].index, level: levelsData[2].level, url: levelsData[2].url }) ]) ) expect(mockNext).not.toHaveBeenCalled() }) it('returns an empty array if subclass exists but has no levels', async () => { const subclassData = subclassFactory.build({ index: subclassIndex }) await SubclassModel.insertMany([subclassData]) // No levels inserted for this subclass const request = createRequest({ params: { index: subclassIndex } }) const response = createResponse() await SubclassController.showLevelsForSubclass(request, response, mockNext) expect(response.statusCode).toBe(200) expect(JSON.parse(response._getData())).toEqual([]) expect(mockNext).not.toHaveBeenCalled() }) it('returns an empty array for a non-existent subclass index', async () => { const request = createRequest({ params: { index: 'nonexistent-subclass' } }) const response = createResponse() await SubclassController.showLevelsForSubclass(request, response, mockNext) expect(response.statusCode).toBe(200) expect(JSON.parse(response._getData())).toEqual([]) expect(mockNext).not.toHaveBeenCalled() }) it('handles database errors during level find', async () => { const request = createRequest({ params: { index: subclassIndex } }) const response = createResponse() const error = new Error('Level find failed') vi.spyOn(LevelModel, 'find').mockImplementationOnce( () => ({ sort: vi.fn().mockRejectedValueOnce(error) }) as any ) await SubclassController.showLevelsForSubclass(request, response, mockNext) expect(mockNext).toHaveBeenCalledOnce() expect(mockNext).toHaveBeenCalledWith(error) }) }) describe('showLevelForSubclass', () => { const subclassIndex = 'berserker' const targetLevel = 5 const levelUrl = `/api/2014/subclasses/${subclassIndex}/levels/${targetLevel}` it('returns a specific level for a subclass', async () => { // Arrange const subclassData = subclassFactory.build({ index: subclassIndex }) await SubclassModel.insertMany([subclassData]) const levelData = levelFactory.build({ level: targetLevel, subclass: { index: subclassIndex, name: subclassData.name, url: `/api/2014/subclasses/${subclassIndex}` }, url: levelUrl }) await LevelModel.insertMany([levelData]) const request = createRequest({ params: { index: subclassIndex, level: String(targetLevel) } }) const response = createResponse() // Act await SubclassController.showLevelForSubclass(request, response, mockNext) // Assert expect(response.statusCode).toBe(200) const responseData = JSON.parse(response._getData()) expect(responseData).toMatchObject({ index: levelData.index, level: levelData.level, url: levelData.url }) expect(mockNext).not.toHaveBeenCalled() }) it('calls next() when the specific level is not found for the subclass', async () => { const request = createRequest({ params: { index: subclassIndex, level: '10' } }) const response = createResponse() await SubclassController.showLevelForSubclass(request, response, mockNext) expect(response.statusCode).toBe(200) expect(response._getData()).toBe('') expect(mockNext).toHaveBeenCalledOnce() expect(mockNext).toHaveBeenCalledWith() }) it('handles database errors during level findOne', async () => { const request = createRequest({ params: { index: subclassIndex, level: String(targetLevel) } }) const response = createResponse() const error = new Error('Level findOne failed') vi.spyOn(LevelModel, 'findOne').mockRejectedValueOnce(error) await SubclassController.showLevelForSubclass(request, response, mockNext) expect(response.statusCode).toBe(200) expect(response._getData()).toBe('') expect(mockNext).toHaveBeenCalledOnce() expect(mockNext).toHaveBeenCalledWith(error) }) it('returns 400 for invalid level parameter (non-numeric)', async () => { const request = createRequest({ params: { index: subclassIndex, level: 'invalid' } }) const response = createResponse() await SubclassController.showLevelForSubclass(request, response, mockNext) expect(response.statusCode).toBe(400) expect(JSON.parse(response._getData()).error).toContain('Invalid path parameters') expect(mockNext).not.toHaveBeenCalled() // Should not call next on validation error }) it('returns 400 for invalid level parameter (out of range)', async () => { const request = createRequest({ params: { index: subclassIndex, level: '0' } }) // Level 0 is invalid const response = createResponse() await SubclassController.showLevelForSubclass(request, response, mockNext) expect(response.statusCode).toBe(400) expect(JSON.parse(response._getData()).error).toContain('Invalid path parameters') expect(mockNext).not.toHaveBeenCalled() }) }) describe('showFeaturesForSubclass', () => { const subclassIndex = 'berserker' const subclassUrl = `/api/2014/subclasses/${subclassIndex}` it('returns features for a specific subclass', async () => { // Arrange const subclassData = subclassFactory.build({ index: subclassIndex, url: subclassUrl }) await SubclassModel.insertMany([subclassData]) // Create features linked to this subclass const subclassRef = apiReferenceFactory.build({ index: subclassIndex, name: subclassData.name, url: subclassUrl }) const featuresData = featureFactory.buildList(2, { subclass: subclassRef }) await FeatureModel.insertMany(featuresData) // Add unrelated features await FeatureModel.insertMany(featureFactory.buildList(1)) const request = createRequest({ params: { index: subclassIndex } }) const response = createResponse() // Act await SubclassController.showFeaturesForSubclass(request, response, mockNext) // Assert expect(response.statusCode).toBe(200) const responseData = JSON.parse(response._getData()) expect(responseData).toHaveProperty('count', 2) expect(responseData).toHaveProperty('results') expect(responseData.results).toBeInstanceOf(Array) expect(responseData.results).toHaveLength(2) // Only the 2 features for this subclass expect(responseData.results).toEqual( expect.arrayContaining([ expect.objectContaining({ index: featuresData[0].index, name: featuresData[0].name, url: featuresData[0].url }), expect.objectContaining({ index: featuresData[1].index, name: featuresData[1].name, url: featuresData[1].url }) ]) ) expect(mockNext).not.toHaveBeenCalled() }) it('returns an empty list if subclass has no features', async () => { const subclassData = subclassFactory.build({ index: subclassIndex }) await SubclassModel.insertMany([subclassData]) // No features inserted const request = createRequest({ params: { index: subclassIndex } }) const response = createResponse() await SubclassController.showFeaturesForSubclass(request, response, mockNext) expect(response.statusCode).toBe(200) expect(JSON.parse(response._getData())).toEqual({ count: 0, results: [] }) expect(mockNext).not.toHaveBeenCalled() }) it('handles database errors during feature find', async () => { const request = createRequest({ params: { index: subclassIndex } }) const response = createResponse() const error = new Error('Feature find failed') vi.spyOn(FeatureModel, 'find').mockImplementationOnce(() => { const query = { select: vi.fn().mockReturnThis(), sort: vi.fn().mockRejectedValueOnce(error) } as any return query }) await SubclassController.showFeaturesForSubclass(request, response, mockNext) expect(response.statusCode).toBe(200) expect(response._getData()).toBe('') expect(mockNext).toHaveBeenCalledOnce() expect(mockNext).toHaveBeenCalledWith(error) }) }) describe('showFeaturesForSubclassAndLevel', () => { const subclassIndex = 'berserker' const targetLevel = 3 const subclassUrl = `/api/2014/subclasses/${subclassIndex}` it('returns features for a specific subclass and level', async () => { // Arrange const subclassData = subclassFactory.build({ index: subclassIndex, url: subclassUrl }) await SubclassModel.insertMany([subclassData]) const subclassRef = apiReferenceFactory.build({ index: subclassIndex, name: subclassData.name, url: subclassUrl }) // Features at the target level const featuresDataLevel = featureFactory.buildList(2, { subclass: subclassRef, level: targetLevel }) // Features at a different level const featuresDataOtherLevel = featureFactory.buildList(1, { subclass: subclassRef, level: targetLevel + 1 }) await FeatureModel.insertMany([...featuresDataLevel, ...featuresDataOtherLevel]) const request = createRequest({ params: { index: subclassIndex, level: String(targetLevel) } }) const response = createResponse() // Act await SubclassController.showFeaturesForSubclassAndLevel(request, response, mockNext) // Assert expect(response.statusCode).toBe(200) const responseData = JSON.parse(response._getData()) expect(responseData).toHaveProperty('count', 2) expect(responseData).toHaveProperty('results') expect(responseData.results).toBeInstanceOf(Array) expect(responseData.results).toHaveLength(2) // Only features at target level expect(responseData.results).toEqual( expect.arrayContaining([ expect.objectContaining({ index: featuresDataLevel[0].index, name: featuresDataLevel[0].name, url: featuresDataLevel[0].url }), expect.objectContaining({ index: featuresDataLevel[1].index, name: featuresDataLevel[1].name, url: featuresDataLevel[1].url }) ]) ) expect(mockNext).not.toHaveBeenCalled() }) it('returns an empty list if no features for the specific level', async () => { const subclassData = subclassFactory.build({ index: subclassIndex }) await SubclassModel.insertMany([subclassData]) // No features at target level inserted const request = createRequest({ params: { index: subclassIndex, level: String(targetLevel) } }) const response = createResponse() await SubclassController.showFeaturesForSubclassAndLevel(request, response, mockNext) expect(response.statusCode).toBe(200) expect(JSON.parse(response._getData())).toEqual({ count: 0, results: [] }) expect(mockNext).not.toHaveBeenCalled() }) it('handles database errors during feature find for level', async () => { const request = createRequest({ params: { index: subclassIndex, level: String(targetLevel) } }) const response = createResponse() const error = new Error('Feature find failed') vi.spyOn(FeatureModel, 'find').mockImplementationOnce(() => { const query = { select: vi.fn().mockReturnThis(), sort: vi.fn().mockRejectedValueOnce(error) } as any return query }) await SubclassController.showFeaturesForSubclassAndLevel(request, response, mockNext) expect(response.statusCode).toBe(200) expect(response._getData()).toBe('') expect(mockNext).toHaveBeenCalledOnce() expect(mockNext).toHaveBeenCalledWith(error) }) it('returns 400 for invalid level parameter (non-numeric)', async () => { const request = createRequest({ params: { index: subclassIndex, level: 'invalid' } }) const response = createResponse() await SubclassController.showFeaturesForSubclassAndLevel(request, response, mockNext) expect(response.statusCode).toBe(400) expect(JSON.parse(response._getData()).error).toContain('Invalid path parameters') expect(mockNext).not.toHaveBeenCalled() }) it('returns 400 for invalid level parameter (out of range)', async () => { const request = createRequest({ params: { index: subclassIndex, level: '21' } }) const response = createResponse() await SubclassController.showFeaturesForSubclassAndLevel(request, response, mockNext) expect(response.statusCode).toBe(400) expect(JSON.parse(response._getData()).error).toContain('Invalid path parameters') expect(mockNext).not.toHaveBeenCalled() }) }) }) ================================================ FILE: src/tests/controllers/api/2014/subraceController.test.ts ================================================ import { createRequest, createResponse } from 'node-mocks-http' import { describe, expect, it, vi } from 'vitest' import * as SubraceController from '@/controllers/api/2014/subraceController' import ProficiencyModel from '@/models/2014/proficiency' import SubraceModel from '@/models/2014/subrace' import TraitModel from '@/models/2014/trait' import { proficiencyFactory } from '@/tests/factories/2014/proficiency.factory' import { subraceFactory } from '@/tests/factories/2014/subrace.factory' import { traitFactory } from '@/tests/factories/2014/trait.factory' import { mockNext as defaultMockNext } from '@/tests/support' import { generateUniqueDbUri, setupIsolatedDatabase, setupModelCleanup, teardownIsolatedDatabase } from '@/tests/support/db' const mockNext = vi.fn(defaultMockNext) // Setup DB isolation const dbUri = generateUniqueDbUri('subrace') setupIsolatedDatabase(dbUri) teardownIsolatedDatabase() // Cleanup all relevant models setupModelCleanup(SubraceModel) setupModelCleanup(TraitModel) setupModelCleanup(ProficiencyModel) describe('SubraceController', () => { describe('index', () => { it('returns a list of subraces', async () => { // Arrange: Seed DB const subracesData = subraceFactory.buildList(3) await SubraceModel.insertMany(subracesData) const request = createRequest({ query: {} }) const response = createResponse() // Act await SubraceController.index(request, response, mockNext) // Assert: Check response based on seeded data expect(response.statusCode).toBe(200) const responseData = JSON.parse(response._getData()) expect(responseData.count).toBe(3) expect(responseData.results).toHaveLength(3) expect(responseData.results).toEqual( expect.arrayContaining([ expect.objectContaining({ index: subracesData[0].index, name: subracesData[0].name }), expect.objectContaining({ index: subracesData[1].index, name: subracesData[1].name }), expect.objectContaining({ index: subracesData[2].index, name: subracesData[2].name }) ]) ) expect(mockNext).not.toHaveBeenCalled() }) // describe('when something goes wrong', ...) }) describe('show', () => { it('returns a single subrace', async () => { // Arrange const subraceData = subraceFactory.build({ index: 'high-elf' }) await SubraceModel.insertMany([subraceData]) const request = createRequest({ params: { index: 'high-elf' } }) const response = createResponse() // Act await SubraceController.show(request, response, mockNext) // Assert expect(response.statusCode).toBe(200) const responseData = JSON.parse(response._getData()) expect(responseData.index).toBe(subraceData.index) expect(responseData.name).toBe(subraceData.name) // Add more checks as needed expect(responseData.race.index).toBe(subraceData.race.index) expect(mockNext).not.toHaveBeenCalled() }) it('calls next() when the record does not exist', async () => { // Arrange const request = createRequest({ params: { index: 'non-existent' } }) const response = createResponse() // Act await SubraceController.show(request, response, mockNext) // Assert expect(mockNext).toHaveBeenCalledWith() expect(response._getData()).toBe('') }) // describe('when something goes wrong', ...) }) describe('showTraitsForSubrace', () => { const subraceIndex = 'rock-gnome' const subraceUrl = `/api/2014/subraces/${subraceIndex}` it('returns a list of traits for the subrace', async () => { // Arrange const subraceRef = { index: subraceIndex, name: 'Rock Gnome', url: subraceUrl } const traitsData = traitFactory.buildList(2, { subraces: [subraceRef] }) await TraitModel.insertMany(traitsData) // Seed unrelated traits await TraitModel.insertMany(traitFactory.buildList(1)) const request = createRequest({ params: { index: subraceIndex } }) const response = createResponse() // Act await SubraceController.showTraitsForSubrace(request, response, mockNext) // Assert expect(response.statusCode).toBe(200) const responseData = JSON.parse(response._getData()) expect(responseData.count).toBe(2) expect(responseData.results).toHaveLength(2) expect(responseData.results.map((t: any) => t.index)).toEqual( expect.arrayContaining([traitsData[0].index, traitsData[1].index]) ) expect(mockNext).not.toHaveBeenCalled() }) // describe('when something goes wrong', ...) }) describe('showProficienciesForSubrace', () => { const subraceIndex = 'hill-dwarf' const subraceUrl = `/api/2014/subraces/${subraceIndex}` it('returns a list of proficiencies for the subrace', async () => { // Arrange const subraceRef = { index: subraceIndex, name: 'Hill Dwarf', url: subraceUrl } // The Proficiency model links via `races` which includes subraces conceptually const proficienciesData = proficiencyFactory.buildList(3, { races: [subraceRef] }) await ProficiencyModel.insertMany(proficienciesData) // Seed unrelated proficiencies await ProficiencyModel.insertMany(proficiencyFactory.buildList(2)) const request = createRequest({ params: { index: subraceIndex } }) const response = createResponse() // Act await SubraceController.showProficienciesForSubrace(request, response, mockNext) // Assert expect(response.statusCode).toBe(200) const responseData = JSON.parse(response._getData()) expect(responseData.count).toBe(3) expect(responseData.results).toHaveLength(3) expect(responseData.results.map((p: any) => p.index)).toEqual( expect.arrayContaining([ proficienciesData[0].index, proficienciesData[1].index, proficienciesData[2].index ]) ) expect(mockNext).not.toHaveBeenCalled() }) // describe('when something goes wrong', ...) }) }) ================================================ FILE: src/tests/controllers/api/2014/traitController.test.ts ================================================ import { createRequest, createResponse } from 'node-mocks-http' import { describe, expect, it, vi } from 'vitest' import TraitController from '@/controllers/api/2014/traitController' import TraitModel from '@/models/2014/trait' import { traitFactory } from '@/tests/factories/2014/trait.factory' import { mockNext as defaultMockNext } from '@/tests/support' import { generateUniqueDbUri, setupIsolatedDatabase, setupModelCleanup, teardownIsolatedDatabase } from '@/tests/support/db' const mockNext = vi.fn(defaultMockNext) // Generate URI for this test file const dbUri = generateUniqueDbUri('trait') // Setup hooks using helpers setupIsolatedDatabase(dbUri) teardownIsolatedDatabase() setupModelCleanup(TraitModel) describe('TraitController', () => { describe('index', () => { it('returns a list of traits', async () => { // Arrange const traitsData = traitFactory.buildList(3) await TraitModel.insertMany(traitsData) const request = createRequest({ query: {} }) const response = createResponse() // Act await TraitController.index(request, response, mockNext) // Assert expect(response.statusCode).toBe(200) const responseData = JSON.parse(response._getData()) expect(responseData.count).toBe(3) expect(responseData.results).toHaveLength(3) expect(responseData.results).toEqual( expect.arrayContaining([ expect.objectContaining({ index: traitsData[0].index, name: traitsData[0].name }), expect.objectContaining({ index: traitsData[1].index, name: traitsData[1].name }), expect.objectContaining({ index: traitsData[2].index, name: traitsData[2].name }) ]) ) expect(mockNext).not.toHaveBeenCalled() }) it('returns an empty list when no traits exist', async () => { const request = createRequest({ query: {} }) const response = createResponse() await TraitController.index(request, response, mockNext) expect(response.statusCode).toBe(200) const responseData = JSON.parse(response._getData()) expect(responseData.count).toBe(0) expect(responseData.results).toEqual([]) expect(mockNext).not.toHaveBeenCalled() }) }) describe('show', () => { it('returns a single trait when found', async () => { // Arrange const traitData = traitFactory.build({ index: 'darkvision', name: 'Darkvision' }) await TraitModel.insertMany([traitData]) const request = createRequest({ params: { index: 'darkvision' } }) const response = createResponse() // Act await TraitController.show(request, response, mockNext) // Assert expect(response.statusCode).toBe(200) const responseData = JSON.parse(response._getData()) expect(responseData.index).toBe('darkvision') expect(responseData.name).toBe('Darkvision') expect(responseData.desc).toEqual(traitData.desc) // Add checks for potentially optional fields like races, subraces, proficiencies if needed expect(mockNext).not.toHaveBeenCalled() }) it('calls next() when the trait is not found', async () => { const request = createRequest({ params: { index: 'nonexistent' } }) const response = createResponse() await TraitController.show(request, response, mockNext) expect(response.statusCode).toBe(200) // Passes to next() expect(response._getData()).toBe('') expect(mockNext).toHaveBeenCalledOnce() expect(mockNext).toHaveBeenCalledWith() // Default 404 handling }) }) }) ================================================ FILE: src/tests/controllers/api/2014/weaponPropertyController.test.ts ================================================ import { createRequest, createResponse } from 'node-mocks-http' import { describe, expect, it, vi } from 'vitest' import WeaponPropertyController from '@/controllers/api/2014/weaponPropertyController' import WeaponPropertyModel from '@/models/2014/weaponProperty' import { weaponPropertyFactory } from '@/tests/factories/2014/weaponProperty.factory' import { mockNext as defaultMockNext } from '@/tests/support' import { generateUniqueDbUri, setupIsolatedDatabase, setupModelCleanup, teardownIsolatedDatabase } from '@/tests/support/db' const mockNext = vi.fn(defaultMockNext) // Setup DB isolation const dbUri = generateUniqueDbUri('weaponproperty') setupIsolatedDatabase(dbUri) teardownIsolatedDatabase() setupModelCleanup(WeaponPropertyModel) describe('WeaponPropertyController', () => { describe('index', () => { it('returns a list of weapon properties', async () => { // Arrange: Seed DB const propertiesData = weaponPropertyFactory.buildList(3) await WeaponPropertyModel.insertMany(propertiesData) const request = createRequest({ query: {} }) const response = createResponse() // Act await WeaponPropertyController.index(request, response, mockNext) // Assert: Check response based on seeded data expect(response.statusCode).toBe(200) const responseData = JSON.parse(response._getData()) expect(responseData.count).toBe(3) expect(responseData.results).toHaveLength(3) expect(responseData.results).toEqual( expect.arrayContaining([ expect.objectContaining({ index: propertiesData[0].index, name: propertiesData[0].name }), expect.objectContaining({ index: propertiesData[1].index, name: propertiesData[1].name }), expect.objectContaining({ index: propertiesData[2].index, name: propertiesData[2].name }) ]) ) expect(mockNext).not.toHaveBeenCalled() }) // describe('when something goes wrong', ...) }) describe('show', () => { it('returns a single weapon property', async () => { // Arrange const propertyData = weaponPropertyFactory.build({ index: 'versatile' }) await WeaponPropertyModel.insertMany([propertyData]) const request = createRequest({ params: { index: 'versatile' } }) const response = createResponse() // Act await WeaponPropertyController.show(request, response, mockNext) // Assert expect(response.statusCode).toBe(200) const responseData = JSON.parse(response._getData()) expect(responseData.index).toBe(propertyData.index) expect(responseData.name).toBe(propertyData.name) expect(responseData.desc).toEqual(propertyData.desc) expect(mockNext).not.toHaveBeenCalled() }) it('calls next() when the record does not exist', async () => { // Arrange const request = createRequest({ params: { index: 'non-existent' } }) const response = createResponse() // Act await WeaponPropertyController.show(request, response, mockNext) // Assert expect(mockNext).toHaveBeenCalledWith() expect(response._getData()).toBe('') }) // describe('when something goes wrong', ...) }) }) ================================================ FILE: src/tests/controllers/api/2024/BackgroundController.test.ts ================================================ import { createRequest, createResponse } from 'node-mocks-http' import { describe, expect, it, vi } from 'vitest' import BackgroundController from '@/controllers/api/2024/backgroundController' import BackgroundModel from '@/models/2024/background' import { backgroundFactory } from '@/tests/factories/2024/background.factory' import { mockNext as defaultMockNext } from '@/tests/support' import { generateUniqueDbUri, setupIsolatedDatabase, setupModelCleanup, teardownIsolatedDatabase } from '@/tests/support/db' const mockNext = vi.fn(defaultMockNext) const dbUri = generateUniqueDbUri('background') setupIsolatedDatabase(dbUri) teardownIsolatedDatabase() setupModelCleanup(BackgroundModel) describe('BackgroundController', () => { describe('index', () => { it('returns a list of backgrounds', async () => { const backgroundsData = backgroundFactory.buildList(3) await BackgroundModel.insertMany(backgroundsData) const request = createRequest({ query: {} }) const response = createResponse() await BackgroundController.index(request, response, mockNext) expect(response.statusCode).toBe(200) const responseData = JSON.parse(response._getData()) expect(responseData.count).toBe(3) expect(responseData.results).toHaveLength(3) expect(mockNext).not.toHaveBeenCalled() }) it('filters by name', async () => { const backgroundsData = [ backgroundFactory.build({ name: 'Acolyte' }), backgroundFactory.build({ name: 'Criminal' }) ] await BackgroundModel.insertMany(backgroundsData) const request = createRequest({ query: { name: 'Acolyte' } }) const response = createResponse() await BackgroundController.index(request, response, mockNext) expect(response.statusCode).toBe(200) const responseData = JSON.parse(response._getData()) expect(responseData.count).toBe(1) expect(responseData.results[0].name).toBe('Acolyte') }) }) describe('show', () => { it('returns a single background when found', async () => { const backgroundData = backgroundFactory.build({ index: 'acolyte', name: 'Acolyte' }) await BackgroundModel.insertMany([backgroundData]) const request = createRequest({ params: { index: 'acolyte' } }) const response = createResponse() await BackgroundController.show(request, response, mockNext) expect(response.statusCode).toBe(200) const responseData = JSON.parse(response._getData()) expect(responseData.index).toBe('acolyte') expect(responseData.name).toBe('Acolyte') expect(mockNext).not.toHaveBeenCalled() }) it('calls next() when the background is not found', async () => { const request = createRequest({ params: { index: 'nonexistent' } }) const response = createResponse() await BackgroundController.show(request, response, mockNext) expect(response._getData()).toBe('') expect(mockNext).toHaveBeenCalledOnce() }) }) }) ================================================ FILE: src/tests/controllers/api/2024/FeatController.test.ts ================================================ import { createRequest, createResponse } from 'node-mocks-http' import { describe, expect, it, vi } from 'vitest' import FeatController from '@/controllers/api/2024/featController' import FeatModel from '@/models/2024/feat' import { featFactory } from '@/tests/factories/2024/feat.factory' import { mockNext as defaultMockNext } from '@/tests/support' import { generateUniqueDbUri, setupIsolatedDatabase, setupModelCleanup, teardownIsolatedDatabase } from '@/tests/support/db' const mockNext = vi.fn(defaultMockNext) const dbUri = generateUniqueDbUri('feat') setupIsolatedDatabase(dbUri) teardownIsolatedDatabase() setupModelCleanup(FeatModel) describe('FeatController', () => { describe('index', () => { it('returns a list of feats', async () => { const featsData = featFactory.buildList(3) await FeatModel.insertMany(featsData) const request = createRequest({ query: {} }) const response = createResponse() await FeatController.index(request, response, mockNext) expect(response.statusCode).toBe(200) const responseData = JSON.parse(response._getData()) expect(responseData.count).toBe(3) expect(responseData.results).toHaveLength(3) expect(mockNext).not.toHaveBeenCalled() }) it('filters by name', async () => { const featsData = [featFactory.build({ name: 'Alert' }), featFactory.build({ name: 'Lucky' })] await FeatModel.insertMany(featsData) const request = createRequest({ query: { name: 'Alert' } }) const response = createResponse() await FeatController.index(request, response, mockNext) expect(response.statusCode).toBe(200) const responseData = JSON.parse(response._getData()) expect(responseData.count).toBe(1) expect(responseData.results[0].name).toBe('Alert') }) }) describe('show', () => { it('returns a single feat when found', async () => { const featData = featFactory.build({ index: 'alert', name: 'Alert' }) await FeatModel.insertMany([featData]) const request = createRequest({ params: { index: 'alert' } }) const response = createResponse() await FeatController.show(request, response, mockNext) expect(response.statusCode).toBe(200) const responseData = JSON.parse(response._getData()) expect(responseData.index).toBe('alert') expect(responseData.name).toBe('Alert') expect(mockNext).not.toHaveBeenCalled() }) it('calls next() when the feat is not found', async () => { const request = createRequest({ params: { index: 'nonexistent' } }) const response = createResponse() await FeatController.show(request, response, mockNext) expect(response._getData()).toBe('') expect(mockNext).toHaveBeenCalledOnce() }) }) }) ================================================ FILE: src/tests/controllers/api/2024/MagicItemController.test.ts ================================================ import { createRequest, createResponse } from 'node-mocks-http' import { describe, expect, it, vi } from 'vitest' import MagicItemController from '@/controllers/api/2024/magicItemController' import MagicItemModel from '@/models/2024/magicItem' import { magicItemFactory } from '@/tests/factories/2024/magicItem.factory' import { mockNext as defaultMockNext } from '@/tests/support' import { generateUniqueDbUri, setupIsolatedDatabase, setupModelCleanup, teardownIsolatedDatabase } from '@/tests/support/db' const mockNext = vi.fn(defaultMockNext) const dbUri = generateUniqueDbUri('magic-item') setupIsolatedDatabase(dbUri) teardownIsolatedDatabase() setupModelCleanup(MagicItemModel) describe('MagicItemController', () => { describe('index', () => { it('returns a list of magic items', async () => { const itemsData = magicItemFactory.buildList(3) await MagicItemModel.insertMany(itemsData) const request = createRequest({ query: {} }) const response = createResponse() await MagicItemController.index(request, response, mockNext) expect(response.statusCode).toBe(200) const responseData = JSON.parse(response._getData()) expect(responseData.count).toBe(3) expect(responseData.results).toHaveLength(3) expect(mockNext).not.toHaveBeenCalled() }) it('filters by name', async () => { const itemsData = [ magicItemFactory.build({ name: 'Bag of Holding' }), magicItemFactory.build({ name: 'Cloak of Elvenkind' }) ] await MagicItemModel.insertMany(itemsData) const request = createRequest({ query: { name: 'Bag' } }) const response = createResponse() await MagicItemController.index(request, response, mockNext) expect(response.statusCode).toBe(200) const responseData = JSON.parse(response._getData()) expect(responseData.count).toBe(1) expect(responseData.results[0].name).toBe('Bag of Holding') }) }) describe('show', () => { it('returns a single magic item when found', async () => { const itemData = magicItemFactory.build({ index: 'bag-of-holding', name: 'Bag of Holding' }) await MagicItemModel.insertMany([itemData]) const request = createRequest({ params: { index: 'bag-of-holding' } }) const response = createResponse() await MagicItemController.show(request, response, mockNext) expect(response.statusCode).toBe(200) const responseData = JSON.parse(response._getData()) expect(responseData.index).toBe('bag-of-holding') expect(responseData.name).toBe('Bag of Holding') expect(mockNext).not.toHaveBeenCalled() }) it('calls next() when the magic item is not found', async () => { const request = createRequest({ params: { index: 'nonexistent' } }) const response = createResponse() await MagicItemController.show(request, response, mockNext) expect(response._getData()).toBe('') expect(mockNext).toHaveBeenCalledOnce() }) }) }) ================================================ FILE: src/tests/controllers/api/2024/ProficiencyController.test.ts ================================================ import { createRequest, createResponse } from 'node-mocks-http' import { describe, expect, it, vi } from 'vitest' import ProficiencyController from '@/controllers/api/2024/proficiencyController' import ProficiencyModel from '@/models/2024/proficiency' import { proficiencyFactory } from '@/tests/factories/2024/proficiency.factory' import { mockNext as defaultMockNext } from '@/tests/support' import { generateUniqueDbUri, setupIsolatedDatabase, setupModelCleanup, teardownIsolatedDatabase } from '@/tests/support/db' const mockNext = vi.fn(defaultMockNext) const dbUri = generateUniqueDbUri('proficiency') setupIsolatedDatabase(dbUri) teardownIsolatedDatabase() setupModelCleanup(ProficiencyModel) describe('ProficiencyController', () => { describe('index', () => { it('returns a list of proficiencies', async () => { const proficienciesData = proficiencyFactory.buildList(3) await ProficiencyModel.insertMany(proficienciesData) const request = createRequest({ query: {} }) const response = createResponse() await ProficiencyController.index(request, response, mockNext) expect(response.statusCode).toBe(200) const responseData = JSON.parse(response._getData()) expect(responseData.count).toBe(3) expect(responseData.results).toHaveLength(3) expect(mockNext).not.toHaveBeenCalled() }) it('filters by name', async () => { const proficienciesData = [ proficiencyFactory.build({ name: 'Skill: Arcana' }), proficiencyFactory.build({ name: 'Skill: History' }) ] await ProficiencyModel.insertMany(proficienciesData) const request = createRequest({ query: { name: 'Arcana' } }) const response = createResponse() await ProficiencyController.index(request, response, mockNext) expect(response.statusCode).toBe(200) const responseData = JSON.parse(response._getData()) expect(responseData.count).toBe(1) expect(responseData.results[0].name).toBe('Skill: Arcana') }) }) describe('show', () => { it('returns a single proficiency when found', async () => { const proficiencyData = proficiencyFactory.build({ index: 'skill-arcana', name: 'Skill: Arcana' }) await ProficiencyModel.insertMany([proficiencyData]) const request = createRequest({ params: { index: 'skill-arcana' } }) const response = createResponse() await ProficiencyController.show(request, response, mockNext) expect(response.statusCode).toBe(200) const responseData = JSON.parse(response._getData()) expect(responseData.index).toBe('skill-arcana') expect(responseData.name).toBe('Skill: Arcana') expect(mockNext).not.toHaveBeenCalled() }) it('calls next() when the proficiency is not found', async () => { const request = createRequest({ params: { index: 'nonexistent' } }) const response = createResponse() await ProficiencyController.show(request, response, mockNext) expect(response._getData()).toBe('') expect(mockNext).toHaveBeenCalledOnce() }) }) }) ================================================ FILE: src/tests/controllers/api/2024/SubclassController.test.ts ================================================ import { createRequest, createResponse } from 'node-mocks-http' import { describe, expect, it, vi } from 'vitest' import SubclassController from '@/controllers/api/2024/subclassController' import SubclassModel from '@/models/2024/subclass' import { subclassFactory } from '@/tests/factories/2024/subclass.factory' import { mockNext as defaultMockNext } from '@/tests/support' import { generateUniqueDbUri, setupIsolatedDatabase, setupModelCleanup, teardownIsolatedDatabase } from '@/tests/support/db' const mockNext = vi.fn(defaultMockNext) const dbUri = generateUniqueDbUri('subclass') setupIsolatedDatabase(dbUri) teardownIsolatedDatabase() setupModelCleanup(SubclassModel) describe('SubclassController', () => { describe('index', () => { it('returns a list of subclasses', async () => { const subclassesData = subclassFactory.buildList(3) await SubclassModel.insertMany(subclassesData) const request = createRequest({ query: {} }) const response = createResponse() await SubclassController.index(request, response, mockNext) expect(response.statusCode).toBe(200) const responseData = JSON.parse(response._getData()) expect(responseData.count).toBe(3) expect(responseData.results).toHaveLength(3) expect(mockNext).not.toHaveBeenCalled() }) it('filters by name', async () => { const subclassesData = [ subclassFactory.build({ name: 'Path of the Berserker' }), subclassFactory.build({ name: 'Path of the Totem Warrior' }) ] await SubclassModel.insertMany(subclassesData) const request = createRequest({ query: { name: 'Berserker' } }) const response = createResponse() await SubclassController.index(request, response, mockNext) expect(response.statusCode).toBe(200) const responseData = JSON.parse(response._getData()) expect(responseData.count).toBe(1) expect(responseData.results[0].name).toBe('Path of the Berserker') }) }) describe('show', () => { it('returns a single subclass when found', async () => { const subclassData = subclassFactory.build({ index: 'berserker', name: 'Path of the Berserker' }) await SubclassModel.insertMany([subclassData]) const request = createRequest({ params: { index: 'berserker' } }) const response = createResponse() await SubclassController.show(request, response, mockNext) expect(response.statusCode).toBe(200) const responseData = JSON.parse(response._getData()) expect(responseData.index).toBe('berserker') expect(responseData.name).toBe('Path of the Berserker') expect(responseData.features).toHaveLength(2) expect(mockNext).not.toHaveBeenCalled() }) it('calls next() when the subclass is not found', async () => { const request = createRequest({ params: { index: 'nonexistent' } }) const response = createResponse() await SubclassController.show(request, response, mockNext) expect(response._getData()).toBe('') expect(mockNext).toHaveBeenCalledOnce() }) }) }) ================================================ FILE: src/tests/controllers/api/2024/abilityScoreController.test.ts ================================================ import { createRequest, createResponse } from 'node-mocks-http' import { describe, expect, it, vi } from 'vitest' import AbilityScoreController from '@/controllers/api/2024/abilityScoreController' import AbilityScoreModel from '@/models/2024/abilityScore' import { abilityScoreFactory } from '@/tests/factories/2024/abilityScore.factory' import { mockNext as defaultMockNext } from '@/tests/support' import { generateUniqueDbUri, setupIsolatedDatabase, setupModelCleanup, teardownIsolatedDatabase } from '@/tests/support/db' const mockNext = vi.fn(defaultMockNext) // Generate URI for this test file const dbUri = generateUniqueDbUri('abilityscore') // Setup hooks using helpers setupIsolatedDatabase(dbUri) teardownIsolatedDatabase() setupModelCleanup(AbilityScoreModel) describe('AbilityScoreController', () => { describe('index', () => { it('returns a list of ability scores', async () => { // Arrange: Seed the database const abilityScoresData = abilityScoreFactory.buildList(3) await AbilityScoreModel.insertMany(abilityScoresData) const request = createRequest() const response = createResponse() // Act await AbilityScoreController.index(request, response, mockNext) // Assert expect(response.statusCode).toBe(200) const responseData = JSON.parse(response._getData()) expect(responseData.count).toBe(3) expect(responseData.results.length).toBe(3) // Check if the returned data loosely matches the seeded data (checking name/index) expect(responseData.results).toEqual( expect.arrayContaining([ expect.objectContaining({ index: abilityScoresData[0].index, name: abilityScoresData[0].name }), expect.objectContaining({ index: abilityScoresData[1].index, name: abilityScoresData[1].name }), expect.objectContaining({ index: abilityScoresData[2].index, name: abilityScoresData[2].name }) ]) ) expect(mockNext).not.toHaveBeenCalled() }) // Skipping the explicit 'find error' mock test for now. }) describe('show', () => { it('returns a single ability score when found', async () => { // Arrange: Seed the database const abilityScoreData = abilityScoreFactory.build({ index: 'cha', name: 'CHA' }) await AbilityScoreModel.insertMany([abilityScoreData]) const request = createRequest({ params: { index: 'cha' } }) const response = createResponse() // Act await AbilityScoreController.show(request, response, mockNext) // Assert expect(response.statusCode).toBe(200) const responseData = JSON.parse(response._getData()) expect(responseData.index).toBe('cha') expect(responseData.name).toBe('CHA') expect(mockNext).not.toHaveBeenCalled() }) it('calls next with an error if the ability score is not found', async () => { // Arrange: Database is empty (guaranteed by setupModelCleanup) const request = createRequest({ params: { index: 'nonexistent' } }) const response = createResponse() // Act await AbilityScoreController.show(request, response, mockNext) // Assert expect(response.statusCode).toBe(200) // Default node-mocks-http status expect(response._getData()).toBe('') // No data written before error expect(mockNext).toHaveBeenCalledOnce() expect(mockNext).toHaveBeenCalledWith() // Expect next() called with no arguments }) // Skipping the explicit 'findOne error' mock test for similar reasons as above. }) }) ================================================ FILE: src/tests/controllers/api/2024/alignmentController.test.ts ================================================ import { createRequest, createResponse } from 'node-mocks-http' import { describe, expect, it, vi } from 'vitest' import AlignmentController from '@/controllers/api/2024/alignmentController' import AlignmentModel from '@/models/2024/alignment' import { alignmentFactory } from '@/tests/factories/2024/alignment.factory' import { mockNext as defaultMockNext } from '@/tests/support' import { generateUniqueDbUri, setupIsolatedDatabase, setupModelCleanup, teardownIsolatedDatabase } from '@/tests/support/db' const mockNext = vi.fn(defaultMockNext) const dbUri = generateUniqueDbUri('alignment') setupIsolatedDatabase(dbUri) teardownIsolatedDatabase() setupModelCleanup(AlignmentModel) describe('AlignmentController', () => { describe('index', () => { it('returns a list of alignments', async () => { const alignmentsData = alignmentFactory.buildList(3) await AlignmentModel.insertMany(alignmentsData) const request = createRequest() const response = createResponse() await AlignmentController.index(request, response, mockNext) expect(response.statusCode).toBe(200) const responseData = JSON.parse(response._getData()) expect(responseData.count).toBe(3) expect(responseData.results.length).toBe(3) expect(responseData.results).toEqual( expect.arrayContaining([ expect.objectContaining({ index: alignmentsData[0].index, name: alignmentsData[0].name }), expect.objectContaining({ index: alignmentsData[1].index, name: alignmentsData[1].name }), expect.objectContaining({ index: alignmentsData[2].index, name: alignmentsData[2].name }) ]) ) expect(mockNext).not.toHaveBeenCalled() }) }) describe('show', () => { it('returns a single alignment when found', async () => { const alignmentData = alignmentFactory.build({ index: 'lawful-good', name: 'Lawful Good' }) await AlignmentModel.insertMany([alignmentData]) const request = createRequest({ params: { index: 'lawful-good' } }) const response = createResponse() await AlignmentController.show(request, response, mockNext) expect(response.statusCode).toBe(200) const responseData = JSON.parse(response._getData()) expect(responseData.index).toBe('lawful-good') expect(responseData.name).toBe('Lawful Good') expect(mockNext).not.toHaveBeenCalled() }) it('calls next with an error if the alignment is not found', async () => { const request = createRequest({ params: { index: 'nonexistent' } }) const response = createResponse() await AlignmentController.show(request, response, mockNext) expect(response.statusCode).toBe(200) expect(response._getData()).toBe('') expect(mockNext).toHaveBeenCalledOnce() expect(mockNext).toHaveBeenCalledWith() }) }) }) ================================================ FILE: src/tests/controllers/api/2024/conditionController.test.ts ================================================ import { createRequest, createResponse } from 'node-mocks-http' import { describe, expect, it, vi } from 'vitest' import ConditionController from '@/controllers/api/2024/conditionController' import ConditionModel from '@/models/2024/condition' import { conditionFactory } from '@/tests/factories/2024/condition.factory' import { mockNext as defaultMockNext } from '@/tests/support' import { generateUniqueDbUri, setupIsolatedDatabase, setupModelCleanup, teardownIsolatedDatabase } from '@/tests/support/db' const mockNext = vi.fn(defaultMockNext) const dbUri = generateUniqueDbUri('condition') setupIsolatedDatabase(dbUri) teardownIsolatedDatabase() setupModelCleanup(ConditionModel) describe('ConditionController', () => { describe('index', () => { it('returns a list of conditions', async () => { const conditionsData = conditionFactory.buildList(3) await ConditionModel.insertMany(conditionsData) const request = createRequest() const response = createResponse() await ConditionController.index(request, response, mockNext) expect(response.statusCode).toBe(200) const responseData = JSON.parse(response._getData()) expect(responseData.count).toBe(3) expect(responseData.results.length).toBe(3) expect(responseData.results).toEqual( expect.arrayContaining([ expect.objectContaining({ index: conditionsData[0].index, name: conditionsData[0].name }), expect.objectContaining({ index: conditionsData[1].index, name: conditionsData[1].name }), expect.objectContaining({ index: conditionsData[2].index, name: conditionsData[2].name }) ]) ) expect(mockNext).not.toHaveBeenCalled() }) }) describe('show', () => { it('returns a single condition when found', async () => { const conditionData = conditionFactory.build({ index: 'poisoned', name: 'Poisoned' }) await ConditionModel.insertMany([conditionData]) const request = createRequest({ params: { index: 'poisoned' } }) const response = createResponse() await ConditionController.show(request, response, mockNext) expect(response.statusCode).toBe(200) const responseData = JSON.parse(response._getData()) expect(responseData.index).toBe('poisoned') expect(responseData.name).toBe('Poisoned') expect(mockNext).not.toHaveBeenCalled() }) it('calls next with an error if the condition is not found', async () => { const request = createRequest({ params: { index: 'nonexistent' } }) const response = createResponse() await ConditionController.show(request, response, mockNext) expect(response.statusCode).toBe(200) expect(response._getData()).toBe('') expect(mockNext).toHaveBeenCalledOnce() expect(mockNext).toHaveBeenCalledWith() }) }) }) ================================================ FILE: src/tests/controllers/api/2024/damageTypeController.test.ts ================================================ import { createRequest, createResponse } from 'node-mocks-http' import { describe, expect, it, vi } from 'vitest' import DamageTypeController from '@/controllers/api/2024/damageTypeController' import DamageTypeModel from '@/models/2024/damageType' import { damageTypeFactory } from '@/tests/factories/2024/damageType.factory' import { mockNext as defaultMockNext } from '@/tests/support' import { generateUniqueDbUri, setupIsolatedDatabase, setupModelCleanup, teardownIsolatedDatabase } from '@/tests/support/db' const mockNext = vi.fn(defaultMockNext) const dbUri = generateUniqueDbUri('damagetype') setupIsolatedDatabase(dbUri) teardownIsolatedDatabase() setupModelCleanup(DamageTypeModel) describe('DamageTypeController', () => { describe('index', () => { it('returns a list of damage types', async () => { const damageTypesData = damageTypeFactory.buildList(3) await DamageTypeModel.insertMany(damageTypesData) const request = createRequest() const response = createResponse() await DamageTypeController.index(request, response, mockNext) expect(response.statusCode).toBe(200) const responseData = JSON.parse(response._getData()) expect(responseData.count).toBe(3) expect(responseData.results.length).toBe(3) expect(responseData.results).toEqual( expect.arrayContaining([ expect.objectContaining({ index: damageTypesData[0].index, name: damageTypesData[0].name }), expect.objectContaining({ index: damageTypesData[1].index, name: damageTypesData[1].name }), expect.objectContaining({ index: damageTypesData[2].index, name: damageTypesData[2].name }) ]) ) expect(mockNext).not.toHaveBeenCalled() }) }) describe('show', () => { it('returns a single damage type when found', async () => { const damageTypeData = damageTypeFactory.build({ index: 'fire', name: 'Fire' }) await DamageTypeModel.insertMany([damageTypeData]) const request = createRequest({ params: { index: 'fire' } }) const response = createResponse() await DamageTypeController.show(request, response, mockNext) expect(response.statusCode).toBe(200) const responseData = JSON.parse(response._getData()) expect(responseData.index).toBe('fire') expect(responseData.name).toBe('Fire') expect(mockNext).not.toHaveBeenCalled() }) it('calls next with an error if the damage type is not found', async () => { const request = createRequest({ params: { index: 'nonexistent' } }) const response = createResponse() await DamageTypeController.show(request, response, mockNext) expect(response.statusCode).toBe(200) expect(response._getData()).toBe('') expect(mockNext).toHaveBeenCalledOnce() expect(mockNext).toHaveBeenCalledWith() }) }) }) ================================================ FILE: src/tests/controllers/api/2024/equipmentCategoryController.test.ts ================================================ import { createRequest, createResponse } from 'node-mocks-http' import { describe, expect, it, vi } from 'vitest' import EquipmentCategoryController from '@/controllers/api/2024/equipmentCategoryController' import EquipmentCategoryModel from '@/models/2024/equipmentCategory' import { equipmentCategoryFactory } from '@/tests/factories/2024/equipmentCategory.factory' import { mockNext as defaultMockNext } from '@/tests/support' import { generateUniqueDbUri, setupIsolatedDatabase, setupModelCleanup, teardownIsolatedDatabase } from '@/tests/support/db' const mockNext = vi.fn(defaultMockNext) // Generate URI for this test file const dbUri = generateUniqueDbUri('equipmentcategory') // Setup hooks using helpers setupIsolatedDatabase(dbUri) teardownIsolatedDatabase() setupModelCleanup(EquipmentCategoryModel) describe('EquipmentCategoryController', () => { describe('index', () => { it('returns a list of equipment categories', async () => { const categoriesData = equipmentCategoryFactory.buildList(3) const categoryDocs = categoriesData.map((data) => new EquipmentCategoryModel(data)) await EquipmentCategoryModel.insertMany(categoryDocs) const request = createRequest({ query: {} }) const response = createResponse() await EquipmentCategoryController.index(request, response, mockNext) expect(response.statusCode).toBe(200) const responseData = JSON.parse(response._getData()) expect(responseData.count).toBe(3) expect(responseData.results).toHaveLength(3) expect(responseData.results).toEqual( expect.arrayContaining([ expect.objectContaining({ index: categoriesData[0].index, name: categoriesData[0].name }), expect.objectContaining({ index: categoriesData[1].index, name: categoriesData[1].name }), expect.objectContaining({ index: categoriesData[2].index, name: categoriesData[2].name }) ]) ) expect(mockNext).not.toHaveBeenCalled() }) it('returns an empty list when no equipment categories exist', async () => { const request = createRequest({ query: {} }) const response = createResponse() await EquipmentCategoryController.index(request, response, mockNext) expect(response.statusCode).toBe(200) const responseData = JSON.parse(response._getData()) expect(responseData.count).toBe(0) expect(responseData.results).toEqual([]) expect(mockNext).not.toHaveBeenCalled() }) }) describe('show', () => { it('returns a single equipment category when found', async () => { const categoryData = equipmentCategoryFactory.build({ index: 'armor', name: 'Armor' }) await EquipmentCategoryModel.insertMany([categoryData]) const request = createRequest({ params: { index: 'armor' } }) const response = createResponse() await EquipmentCategoryController.show(request, response, mockNext) expect(response.statusCode).toBe(200) const responseData = JSON.parse(response._getData()) expect(responseData.index).toBe('armor') expect(responseData.name).toBe('Armor') expect(responseData.equipment).toHaveLength(categoryData.equipment?.length ?? 0) expect(mockNext).not.toHaveBeenCalled() }) it('calls next() when the equipment category is not found', async () => { const request = createRequest({ params: { index: 'nonexistent' } }) const response = createResponse() await EquipmentCategoryController.show(request, response, mockNext) expect(response.statusCode).toBe(200) expect(response._getData()).toBe('') expect(mockNext).toHaveBeenCalledOnce() expect(mockNext).toHaveBeenCalledWith() }) }) }) ================================================ FILE: src/tests/controllers/api/2024/equipmentController.test.ts ================================================ import { createRequest, createResponse } from 'node-mocks-http' import { describe, expect, it, vi } from 'vitest' import EquipmentController from '@/controllers/api/2024/equipmentController' import EquipmentModel from '@/models/2024/equipment' import { equipmentFactory, weaponFactory } from '@/tests/factories/2024/equipment.factory' import { mockNext as defaultMockNext } from '@/tests/support' import { generateUniqueDbUri, setupIsolatedDatabase, setupModelCleanup, teardownIsolatedDatabase } from '@/tests/support/db' const mockNext = vi.fn(defaultMockNext) // Generate URI for this test file const dbUri = generateUniqueDbUri('equipment') // Setup hooks using helpers setupIsolatedDatabase(dbUri) teardownIsolatedDatabase() setupModelCleanup(EquipmentModel) describe('EquipmentController', () => { describe('index', () => { it('returns a list of equipment', async () => { // Arrange: Seed the database with different equipment types const equipmentData = equipmentFactory.buildList(2) const weaponData = weaponFactory.build() await EquipmentModel.insertMany([...equipmentData, weaponData]) const request = createRequest({ query: {} }) const response = createResponse() // Act await EquipmentController.index(request, response, mockNext) // Assert expect(response.statusCode).toBe(200) const responseData = JSON.parse(response._getData()) expect(responseData.count).toBe(3) expect(responseData.results).toHaveLength(3) expect(responseData.results).toEqual( expect.arrayContaining([ expect.objectContaining({ index: equipmentData[0].index, name: equipmentData[0].name }), expect.objectContaining({ index: equipmentData[1].index, name: equipmentData[1].name }), expect.objectContaining({ index: weaponData.index, name: weaponData.name }) ]) ) expect(mockNext).not.toHaveBeenCalled() }) it('returns an empty list when no equipment exists', async () => { const request = createRequest({ query: {} }) const response = createResponse() await EquipmentController.index(request, response, mockNext) expect(response.statusCode).toBe(200) const responseData = JSON.parse(response._getData()) expect(responseData.count).toBe(0) expect(responseData.results).toEqual([]) expect(mockNext).not.toHaveBeenCalled() }) }) describe('show', () => { it('returns a single equipment item when found', async () => { const equipmentData = weaponFactory.build({ index: 'longsword', name: 'Longsword' }) await EquipmentModel.insertMany([equipmentData]) const request = createRequest({ params: { index: 'longsword' } }) const response = createResponse() await EquipmentController.show(request, response, mockNext) expect(response.statusCode).toBe(200) const responseData = JSON.parse(response._getData()) expect(responseData.index).toBe('longsword') expect(responseData.name).toBe('Longsword') expect(responseData.damage).toBeDefined() expect(mockNext).not.toHaveBeenCalled() }) it('calls next() when the equipment is not found', async () => { const request = createRequest({ params: { index: 'nonexistent' } }) const response = createResponse() await EquipmentController.show(request, response, mockNext) expect(response.statusCode).toBe(200) // Default status expect(response._getData()).toBe('') // No data written expect(mockNext).toHaveBeenCalledOnce() expect(mockNext).toHaveBeenCalledWith() }) }) }) ================================================ FILE: src/tests/controllers/api/2024/languageController.test.ts ================================================ import { createRequest, createResponse } from 'node-mocks-http' import { describe, expect, it, vi } from 'vitest' import LanguageController from '@/controllers/api/2024/languageController' import LanguageModel from '@/models/2024/language' import { languageFactory } from '@/tests/factories/2024/language.factory' import { mockNext as defaultMockNext } from '@/tests/support' import { generateUniqueDbUri, setupIsolatedDatabase, setupModelCleanup, teardownIsolatedDatabase } from '@/tests/support/db' const mockNext = vi.fn(defaultMockNext) const dbUri = generateUniqueDbUri('language') setupIsolatedDatabase(dbUri) teardownIsolatedDatabase() setupModelCleanup(LanguageModel) describe('LanguageController', () => { describe('index', () => { it('returns a list of languages', async () => { const languagesData = languageFactory.buildList(3) await LanguageModel.insertMany(languagesData) const request = createRequest() const response = createResponse() await LanguageController.index(request, response, mockNext) expect(response.statusCode).toBe(200) const responseData = JSON.parse(response._getData()) expect(responseData.count).toBe(3) expect(responseData.results.length).toBe(3) expect(responseData.results).toEqual( expect.arrayContaining([ expect.objectContaining({ index: languagesData[0].index, name: languagesData[0].name }), expect.objectContaining({ index: languagesData[1].index, name: languagesData[1].name }), expect.objectContaining({ index: languagesData[2].index, name: languagesData[2].name }) ]) ) expect(mockNext).not.toHaveBeenCalled() }) }) describe('show', () => { it('returns a single language when found', async () => { const languageData = languageFactory.build({ index: 'common', name: 'Common' }) await LanguageModel.insertMany([languageData]) const request = createRequest({ params: { index: 'common' } }) const response = createResponse() await LanguageController.show(request, response, mockNext) expect(response.statusCode).toBe(200) const responseData = JSON.parse(response._getData()) expect(responseData.index).toBe('common') expect(responseData.name).toBe('Common') expect(mockNext).not.toHaveBeenCalled() }) it('calls next with an error if the language is not found', async () => { const request = createRequest({ params: { index: 'nonexistent' } }) const response = createResponse() await LanguageController.show(request, response, mockNext) expect(response.statusCode).toBe(200) expect(response._getData()).toBe('') expect(mockNext).toHaveBeenCalledOnce() expect(mockNext).toHaveBeenCalledWith() }) }) }) ================================================ FILE: src/tests/controllers/api/2024/magicSchoolController.test.ts ================================================ import { createRequest, createResponse } from 'node-mocks-http' import { describe, expect, it, vi } from 'vitest' import MagicSchoolController from '@/controllers/api/2024/magicSchoolController' import MagicSchoolModel from '@/models/2024/magicSchool' import { magicSchoolFactory } from '@/tests/factories/2024/magicSchool.factory' import { mockNext as defaultMockNext } from '@/tests/support' import { generateUniqueDbUri, setupIsolatedDatabase, setupModelCleanup, teardownIsolatedDatabase } from '@/tests/support/db' const mockNext = vi.fn(defaultMockNext) const dbUri = generateUniqueDbUri('magicschool') setupIsolatedDatabase(dbUri) teardownIsolatedDatabase() setupModelCleanup(MagicSchoolModel) describe('MagicSchoolController', () => { describe('index', () => { it('returns a list of magic schools', async () => { const magicSchoolsData = magicSchoolFactory.buildList(3) await MagicSchoolModel.insertMany(magicSchoolsData) const request = createRequest() const response = createResponse() await MagicSchoolController.index(request, response, mockNext) expect(response.statusCode).toBe(200) const responseData = JSON.parse(response._getData()) expect(responseData.count).toBe(3) expect(responseData.results.length).toBe(3) expect(responseData.results).toEqual( expect.arrayContaining([ expect.objectContaining({ index: magicSchoolsData[0].index, name: magicSchoolsData[0].name }), expect.objectContaining({ index: magicSchoolsData[1].index, name: magicSchoolsData[1].name }), expect.objectContaining({ index: magicSchoolsData[2].index, name: magicSchoolsData[2].name }) ]) ) expect(mockNext).not.toHaveBeenCalled() }) }) describe('show', () => { it('returns a single magic school when found', async () => { const magicSchoolData = magicSchoolFactory.build({ index: 'evocation', name: 'Evocation' }) await MagicSchoolModel.insertMany([magicSchoolData]) const request = createRequest({ params: { index: 'evocation' } }) const response = createResponse() await MagicSchoolController.show(request, response, mockNext) expect(response.statusCode).toBe(200) const responseData = JSON.parse(response._getData()) expect(responseData.index).toBe('evocation') expect(responseData.name).toBe('Evocation') expect(mockNext).not.toHaveBeenCalled() }) it('calls next with an error if the magic school is not found', async () => { const request = createRequest({ params: { index: 'nonexistent' } }) const response = createResponse() await MagicSchoolController.show(request, response, mockNext) expect(response.statusCode).toBe(200) expect(response._getData()).toBe('') expect(mockNext).toHaveBeenCalledOnce() expect(mockNext).toHaveBeenCalledWith() }) }) }) ================================================ FILE: src/tests/controllers/api/2024/skillController.test.ts ================================================ import { createRequest, createResponse } from 'node-mocks-http' import { describe, expect, it, vi } from 'vitest' import SkillController from '@/controllers/api/2024/skillController' import SkillModel from '@/models/2024/skill' // Use Model suffix import { skillFactory } from '@/tests/factories/2024/skill.factory' // Updated path import { mockNext as defaultMockNext } from '@/tests/support' // Assuming support helper location import { generateUniqueDbUri, setupIsolatedDatabase, setupModelCleanup, teardownIsolatedDatabase } from '@/tests/support/db' const mockNext = vi.fn(defaultMockNext) // Generate URI for this test file const dbUri = generateUniqueDbUri('skill') // Setup hooks using helpers setupIsolatedDatabase(dbUri) teardownIsolatedDatabase() setupModelCleanup(SkillModel) describe('SkillController', () => { describe('index', () => { it('returns a list of skills', async () => { // Arrange const skillsData = skillFactory.buildList(3) await SkillModel.insertMany(skillsData) const request = createRequest({ query: {} }) const response = createResponse() // Act await SkillController.index(request, response, mockNext) // Assert expect(response.statusCode).toBe(200) const responseData = JSON.parse(response._getData()) expect(responseData.count).toBe(3) expect(responseData.results).toHaveLength(3) expect(responseData.results).toEqual( expect.arrayContaining([ expect.objectContaining({ index: skillsData[0].index, name: skillsData[0].name }), expect.objectContaining({ index: skillsData[1].index, name: skillsData[1].name }), expect.objectContaining({ index: skillsData[2].index, name: skillsData[2].name }) ]) ) expect(mockNext).not.toHaveBeenCalled() }) it('returns an empty list when no skills exist', async () => { const request = createRequest({ query: {} }) const response = createResponse() await SkillController.index(request, response, mockNext) expect(response.statusCode).toBe(200) const responseData = JSON.parse(response._getData()) expect(responseData.count).toBe(0) expect(responseData.results).toEqual([]) expect(mockNext).not.toHaveBeenCalled() }) }) describe('show', () => { it('returns a single skill when found', async () => { // Arrange const skillData = skillFactory.build({ index: 'athletics', name: 'Athletics' }) await SkillModel.insertMany([skillData]) const request = createRequest({ params: { index: 'athletics' } }) const response = createResponse() // Act await SkillController.show(request, response, mockNext) // Assert expect(response.statusCode).toBe(200) const responseData = JSON.parse(response._getData()) expect(responseData.index).toBe('athletics') expect(responseData.name).toBe('Athletics') expect(responseData.description).toEqual(skillData.description) // Check nested object expect(responseData.ability_score.index).toEqual(skillData.ability_score.index) expect(mockNext).not.toHaveBeenCalled() }) it('calls next() when the skill is not found', async () => { const request = createRequest({ params: { index: 'nonexistent' } }) const response = createResponse() await SkillController.show(request, response, mockNext) expect(response.statusCode).toBe(200) // Passes to next() expect(response._getData()).toBe('') expect(mockNext).toHaveBeenCalledOnce() expect(mockNext).toHaveBeenCalledWith() // Default 404 handling }) }) }) ================================================ FILE: src/tests/controllers/api/2024/speciesController.test.ts ================================================ import { createRequest, createResponse } from 'node-mocks-http' import { describe, expect, it, vi } from 'vitest' import * as SpeciesController from '@/controllers/api/2024/speciesController' import Species2024Model from '@/models/2024/species' import Subspecies2024Model from '@/models/2024/subspecies' import Trait2024Model from '@/models/2024/trait' import { speciesFactory } from '@/tests/factories/2024/species.factory' import { subspeciesFactory } from '@/tests/factories/2024/subspecies.factory' import { traitFactory } from '@/tests/factories/2024/trait.factory' import { mockNext as defaultMockNext } from '@/tests/support' import { generateUniqueDbUri, setupIsolatedDatabase, setupModelCleanup, teardownIsolatedDatabase } from '@/tests/support/db' const mockNext = vi.fn(defaultMockNext) const dbUri = generateUniqueDbUri('species2024') setupIsolatedDatabase(dbUri) teardownIsolatedDatabase() setupModelCleanup(Species2024Model) setupModelCleanup(Subspecies2024Model) setupModelCleanup(Trait2024Model) describe('SpeciesController (2024)', () => { describe('index', () => { it('returns a list of species', async () => { const speciesData = speciesFactory.buildList(3) await Species2024Model.insertMany(speciesData) const request = createRequest({ query: {} }) const response = createResponse() await SpeciesController.index(request, response, mockNext) expect(response.statusCode).toBe(200) const responseData = JSON.parse(response._getData()) expect(responseData.count).toBe(3) expect(responseData.results).toHaveLength(3) expect(mockNext).not.toHaveBeenCalled() }) it('returns an empty list when no species exist', async () => { const request = createRequest({ query: {} }) const response = createResponse() await SpeciesController.index(request, response, mockNext) expect(response.statusCode).toBe(200) const responseData = JSON.parse(response._getData()) expect(responseData.count).toBe(0) expect(responseData.results).toEqual([]) expect(mockNext).not.toHaveBeenCalled() }) }) describe('show', () => { it('returns a single species when found', async () => { const speciesData = speciesFactory.build({ index: 'elf', name: 'Elf' }) await Species2024Model.insertMany([speciesData]) const request = createRequest({ params: { index: 'elf' } }) const response = createResponse() await SpeciesController.show(request, response, mockNext) expect(response.statusCode).toBe(200) const responseData = JSON.parse(response._getData()) expect(responseData.index).toBe('elf') expect(responseData.name).toBe('Elf') expect(mockNext).not.toHaveBeenCalled() }) it('calls next() when the species is not found', async () => { const request = createRequest({ params: { index: 'nonexistent' } }) const response = createResponse() await SpeciesController.show(request, response, mockNext) expect(response.statusCode).toBe(200) expect(response._getData()).toBe('') expect(mockNext).toHaveBeenCalledOnce() expect(mockNext).toHaveBeenCalledWith() }) }) describe('showSubspeciesForSpecies', () => { it('returns subspecies matching the species url', async () => { const species = speciesFactory.build({ index: 'elf', name: 'Elf' }) const matchingSubspecies = subspeciesFactory.buildList(2, { species: { index: 'elf', name: 'Elf', url: '/api/2024/species/elf' } }) const otherSubspecies = subspeciesFactory.build({ species: { index: 'dwarf', name: 'Dwarf', url: '/api/2024/species/dwarf' } }) await Species2024Model.insertMany([species]) await Subspecies2024Model.insertMany([...matchingSubspecies, otherSubspecies]) const request = createRequest({ params: { index: 'elf' } }) const response = createResponse() await SpeciesController.showSubspeciesForSpecies(request, response, mockNext) expect(response.statusCode).toBe(200) const responseData = JSON.parse(response._getData()) expect(responseData.count).toBe(2) expect(responseData.results).toHaveLength(2) expect(mockNext).not.toHaveBeenCalled() }) it('returns empty list when no matching subspecies exist', async () => { const request = createRequest({ params: { index: 'human' } }) const response = createResponse() await SpeciesController.showSubspeciesForSpecies(request, response, mockNext) expect(response.statusCode).toBe(200) const responseData = JSON.parse(response._getData()) expect(responseData.count).toBe(0) expect(responseData.results).toEqual([]) }) }) describe('showTraitsForSpecies', () => { it('returns traits matching the species url', async () => { const matchingTraits = traitFactory.buildList(2, { species: [{ index: 'elf', name: 'Elf', url: '/api/2024/species/elf' }] }) const otherTrait = traitFactory.build({ species: [{ index: 'dwarf', name: 'Dwarf', url: '/api/2024/species/dwarf' }] }) await Trait2024Model.insertMany([...matchingTraits, otherTrait]) const request = createRequest({ params: { index: 'elf' } }) const response = createResponse() await SpeciesController.showTraitsForSpecies(request, response, mockNext) expect(response.statusCode).toBe(200) const responseData = JSON.parse(response._getData()) expect(responseData.count).toBe(2) expect(responseData.results).toHaveLength(2) expect(mockNext).not.toHaveBeenCalled() }) it('returns empty list when no matching traits exist', async () => { const request = createRequest({ params: { index: 'human' } }) const response = createResponse() await SpeciesController.showTraitsForSpecies(request, response, mockNext) expect(response.statusCode).toBe(200) const responseData = JSON.parse(response._getData()) expect(responseData.count).toBe(0) expect(responseData.results).toEqual([]) }) }) }) ================================================ FILE: src/tests/controllers/api/2024/subspeciesController.test.ts ================================================ import { createRequest, createResponse } from 'node-mocks-http' import { describe, expect, it, vi } from 'vitest' import * as SubspeciesController from '@/controllers/api/2024/subspeciesController' import Subspecies2024Model from '@/models/2024/subspecies' import Trait2024Model from '@/models/2024/trait' import { subspeciesFactory } from '@/tests/factories/2024/subspecies.factory' import { traitFactory } from '@/tests/factories/2024/trait.factory' import { mockNext as defaultMockNext } from '@/tests/support' import { generateUniqueDbUri, setupIsolatedDatabase, setupModelCleanup, teardownIsolatedDatabase } from '@/tests/support/db' const mockNext = vi.fn(defaultMockNext) const dbUri = generateUniqueDbUri('subspecies2024') setupIsolatedDatabase(dbUri) teardownIsolatedDatabase() setupModelCleanup(Subspecies2024Model) setupModelCleanup(Trait2024Model) describe('SubspeciesController (2024)', () => { describe('index', () => { it('returns a list of subspecies', async () => { const subspeciesData = subspeciesFactory.buildList(3) await Subspecies2024Model.insertMany(subspeciesData) const request = createRequest({ query: {} }) const response = createResponse() await SubspeciesController.index(request, response, mockNext) expect(response.statusCode).toBe(200) const responseData = JSON.parse(response._getData()) expect(responseData.count).toBe(3) expect(responseData.results).toHaveLength(3) expect(mockNext).not.toHaveBeenCalled() }) it('returns an empty list when no subspecies exist', async () => { const request = createRequest({ query: {} }) const response = createResponse() await SubspeciesController.index(request, response, mockNext) expect(response.statusCode).toBe(200) const responseData = JSON.parse(response._getData()) expect(responseData.count).toBe(0) expect(responseData.results).toEqual([]) expect(mockNext).not.toHaveBeenCalled() }) }) describe('show', () => { it('returns a single subspecies when found', async () => { const subspeciesData = subspeciesFactory.build({ index: 'high-elf', name: 'High Elf' }) await Subspecies2024Model.insertMany([subspeciesData]) const request = createRequest({ params: { index: 'high-elf' } }) const response = createResponse() await SubspeciesController.show(request, response, mockNext) expect(response.statusCode).toBe(200) const responseData = JSON.parse(response._getData()) expect(responseData.index).toBe('high-elf') expect(responseData.name).toBe('High Elf') expect(mockNext).not.toHaveBeenCalled() }) it('calls next() when the subspecies is not found', async () => { const request = createRequest({ params: { index: 'nonexistent' } }) const response = createResponse() await SubspeciesController.show(request, response, mockNext) expect(response.statusCode).toBe(200) expect(response._getData()).toBe('') expect(mockNext).toHaveBeenCalledOnce() expect(mockNext).toHaveBeenCalledWith() }) }) describe('showTraitsForSubspecies', () => { it('returns traits matching the subspecies url', async () => { const matchingTraits = traitFactory.buildList(2, { subspecies: [ { index: 'high-elf', name: 'High Elf', url: '/api/2024/subspecies/high-elf' } ] }) const otherTrait = traitFactory.build({ subspecies: [ { index: 'wood-elf', name: 'Wood Elf', url: '/api/2024/subspecies/wood-elf' } ] }) await Trait2024Model.insertMany([...matchingTraits, otherTrait]) const request = createRequest({ params: { index: 'high-elf' } }) const response = createResponse() await SubspeciesController.showTraitsForSubspecies(request, response, mockNext) expect(response.statusCode).toBe(200) const responseData = JSON.parse(response._getData()) expect(responseData.count).toBe(2) expect(responseData.results).toHaveLength(2) expect(mockNext).not.toHaveBeenCalled() }) it('returns empty list when no matching traits exist', async () => { const request = createRequest({ params: { index: 'nonexistent-subspecies' } }) const response = createResponse() await SubspeciesController.showTraitsForSubspecies(request, response, mockNext) expect(response.statusCode).toBe(200) const responseData = JSON.parse(response._getData()) expect(responseData.count).toBe(0) expect(responseData.results).toEqual([]) }) }) }) ================================================ FILE: src/tests/controllers/api/2024/traitController.test.ts ================================================ import { createRequest, createResponse } from 'node-mocks-http' import { describe, expect, it, vi } from 'vitest' import TraitController from '@/controllers/api/2024/traitController' import Trait2024Model from '@/models/2024/trait' import { traitFactory } from '@/tests/factories/2024/trait.factory' import { mockNext as defaultMockNext } from '@/tests/support' import { generateUniqueDbUri, setupIsolatedDatabase, setupModelCleanup, teardownIsolatedDatabase } from '@/tests/support/db' const mockNext = vi.fn(defaultMockNext) const dbUri = generateUniqueDbUri('trait2024') setupIsolatedDatabase(dbUri) teardownIsolatedDatabase() setupModelCleanup(Trait2024Model) describe('TraitController (2024)', () => { describe('index', () => { it('returns a list of traits', async () => { const traitsData = traitFactory.buildList(3) await Trait2024Model.insertMany(traitsData) const request = createRequest({ query: {} }) const response = createResponse() await TraitController.index(request, response, mockNext) expect(response.statusCode).toBe(200) const responseData = JSON.parse(response._getData()) expect(responseData.count).toBe(3) expect(responseData.results).toHaveLength(3) expect(mockNext).not.toHaveBeenCalled() }) it('returns an empty list when no traits exist', async () => { const request = createRequest({ query: {} }) const response = createResponse() await TraitController.index(request, response, mockNext) expect(response.statusCode).toBe(200) const responseData = JSON.parse(response._getData()) expect(responseData.count).toBe(0) expect(responseData.results).toEqual([]) expect(mockNext).not.toHaveBeenCalled() }) }) describe('show', () => { it('returns a single trait when found', async () => { const traitData = traitFactory.build({ index: 'darkvision', name: 'Darkvision' }) await Trait2024Model.insertMany([traitData]) const request = createRequest({ params: { index: 'darkvision' } }) const response = createResponse() await TraitController.show(request, response, mockNext) expect(response.statusCode).toBe(200) const responseData = JSON.parse(response._getData()) expect(responseData.index).toBe('darkvision') expect(responseData.name).toBe('Darkvision') expect(responseData.description).toEqual(traitData.description) expect(mockNext).not.toHaveBeenCalled() }) it('calls next() when the trait is not found', async () => { const request = createRequest({ params: { index: 'nonexistent' } }) const response = createResponse() await TraitController.show(request, response, mockNext) expect(response.statusCode).toBe(200) expect(response._getData()).toBe('') expect(mockNext).toHaveBeenCalledOnce() expect(mockNext).toHaveBeenCalledWith() }) }) }) ================================================ FILE: src/tests/controllers/api/2024/weaponMasteryPropertyController.test.ts ================================================ import { createRequest, createResponse } from 'node-mocks-http' import { describe, expect, it, vi } from 'vitest' import WeaponMasteryPropertyController from '@/controllers/api/2024/weaponMasteryPropertyController' import WeaponMasteryPropertyModel from '@/models/2024/weaponMasteryProperty' import { weaponMasteryPropertyFactory } from '@/tests/factories/2024/weaponMasteryProperty.factory' import { mockNext as defaultMockNext } from '@/tests/support' import { generateUniqueDbUri, setupIsolatedDatabase, setupModelCleanup, teardownIsolatedDatabase } from '@/tests/support/db' const mockNext = vi.fn(defaultMockNext) const dbUri = generateUniqueDbUri('weaponmasteryproperty') setupIsolatedDatabase(dbUri) teardownIsolatedDatabase() setupModelCleanup(WeaponMasteryPropertyModel) describe('WeaponMasteryPropertyController', () => { describe('index', () => { it('returns a list of weapon mastery properties', async () => { const weaponMasteryPropertiesData = weaponMasteryPropertyFactory.buildList(3) await WeaponMasteryPropertyModel.insertMany(weaponMasteryPropertiesData) const request = createRequest() const response = createResponse() await WeaponMasteryPropertyController.index(request, response, mockNext) expect(response.statusCode).toBe(200) const responseData = JSON.parse(response._getData()) expect(responseData.count).toBe(3) expect(responseData.results.length).toBe(3) expect(responseData.results).toEqual( expect.arrayContaining([ expect.objectContaining({ index: weaponMasteryPropertiesData[0].index, name: weaponMasteryPropertiesData[0].name }), expect.objectContaining({ index: weaponMasteryPropertiesData[1].index, name: weaponMasteryPropertiesData[1].name }), expect.objectContaining({ index: weaponMasteryPropertiesData[2].index, name: weaponMasteryPropertiesData[2].name }) ]) ) expect(mockNext).not.toHaveBeenCalled() }) }) describe('show', () => { it('returns a single weapon mastery property when found', async () => { const weaponMasteryPropertyData = weaponMasteryPropertyFactory.build({ index: 'cleave', name: 'Cleave' }) await WeaponMasteryPropertyModel.insertMany([weaponMasteryPropertyData]) const request = createRequest({ params: { index: 'cleave' } }) const response = createResponse() await WeaponMasteryPropertyController.show(request, response, mockNext) expect(response.statusCode).toBe(200) const responseData = JSON.parse(response._getData()) expect(responseData.index).toBe('cleave') expect(responseData.name).toBe('Cleave') expect(mockNext).not.toHaveBeenCalled() }) it('calls next with an error if the weapon mastery property is not found', async () => { const request = createRequest({ params: { index: 'nonexistent' } }) const response = createResponse() await WeaponMasteryPropertyController.show(request, response, mockNext) expect(response.statusCode).toBe(200) expect(response._getData()).toBe('') expect(mockNext).toHaveBeenCalledOnce() expect(mockNext).toHaveBeenCalledWith() }) }) }) ================================================ FILE: src/tests/controllers/api/2024/weaponPropertyController.test.ts ================================================ import { createRequest, createResponse } from 'node-mocks-http' import { describe, expect, it, vi } from 'vitest' import WeaponPropertyController from '@/controllers/api/2024/weaponPropertyController' import WeaponPropertyModel from '@/models/2024/weaponProperty' import { weaponPropertyFactory } from '@/tests/factories/2024/weaponProperty.factory' import { mockNext as defaultMockNext } from '@/tests/support' import { generateUniqueDbUri, setupIsolatedDatabase, setupModelCleanup, teardownIsolatedDatabase } from '@/tests/support/db' const mockNext = vi.fn(defaultMockNext) const dbUri = generateUniqueDbUri('weaponproperty') setupIsolatedDatabase(dbUri) teardownIsolatedDatabase() setupModelCleanup(WeaponPropertyModel) describe('WeaponPropertyController', () => { describe('index', () => { it('returns a list of weapon properties', async () => { const weaponPropertiesData = weaponPropertyFactory.buildList(3) await WeaponPropertyModel.insertMany(weaponPropertiesData) const request = createRequest() const response = createResponse() await WeaponPropertyController.index(request, response, mockNext) expect(response.statusCode).toBe(200) const responseData = JSON.parse(response._getData()) expect(responseData.count).toBe(3) expect(responseData.results.length).toBe(3) expect(responseData.results).toEqual( expect.arrayContaining([ expect.objectContaining({ index: weaponPropertiesData[0].index, name: weaponPropertiesData[0].name }), expect.objectContaining({ index: weaponPropertiesData[1].index, name: weaponPropertiesData[1].name }), expect.objectContaining({ index: weaponPropertiesData[2].index, name: weaponPropertiesData[2].name }) ]) ) expect(mockNext).not.toHaveBeenCalled() }) }) describe('show', () => { it('returns a single weapon property when found', async () => { const weaponPropertyData = weaponPropertyFactory.build({ index: 'versatile', name: 'Versatile' }) await WeaponPropertyModel.insertMany([weaponPropertyData]) const request = createRequest({ params: { index: 'versatile' } }) const response = createResponse() await WeaponPropertyController.show(request, response, mockNext) expect(response.statusCode).toBe(200) const responseData = JSON.parse(response._getData()) expect(responseData.index).toBe('versatile') expect(responseData.name).toBe('Versatile') expect(mockNext).not.toHaveBeenCalled() }) it('calls next with an error if the weapon property is not found', async () => { const request = createRequest({ params: { index: 'nonexistent' } }) const response = createResponse() await WeaponPropertyController.show(request, response, mockNext) expect(response.statusCode).toBe(200) expect(response._getData()).toBe('') expect(mockNext).toHaveBeenCalledOnce() expect(mockNext).toHaveBeenCalledWith() }) }) }) ================================================ FILE: src/tests/controllers/api/v2014Controller.test.ts ================================================ import { createRequest, createResponse } from 'node-mocks-http' import { describe, expect, it, vi } from 'vitest' import * as ApiController from '@/controllers/api/v2014Controller' import CollectionModel from '@/models/2014/collection' import { collectionFactory } from '@/tests/factories/2014/collection.factory' import { mockNext as defaultMockNext } from '@/tests/support' import { generateUniqueDbUri, setupIsolatedDatabase, setupModelCleanup, teardownIsolatedDatabase } from '@/tests/support/db' const mockNext = vi.fn(defaultMockNext) // Generate URI for this test file const dbUri = generateUniqueDbUri('v2014') // Setup hooks using helpers setupIsolatedDatabase(dbUri) teardownIsolatedDatabase() setupModelCleanup(CollectionModel) describe('v2014 API Controller', () => { describe('index', () => { it('returns the map of available API routes', async () => { // Arrange: Seed data within the test const collectionsData = collectionFactory.buildList(3) await CollectionModel.insertMany(collectionsData) const request = createRequest() const response = createResponse() const expectedResponse = collectionsData.reduce( (acc, col) => { acc[col.index] = `/api/2014/${col.index}` return acc }, {} as Record ) await ApiController.index(request, response, mockNext) // Assert const actualResponse = JSON.parse(response._getData()) expect(response.statusCode).toBe(200) expect(actualResponse).toEqual(expectedResponse) expect(mockNext).not.toHaveBeenCalled() }) it('handles database errors during find', async () => { // Arrange const request = createRequest() const response = createResponse() const error = new Error('Database find failed') vi.spyOn(CollectionModel, 'find').mockImplementationOnce(() => { const query = { select: vi.fn().mockReturnThis(), sort: vi.fn().mockReturnThis(), exec: vi.fn().mockRejectedValueOnce(error) } as any return query }) // Act await ApiController.index(request, response, mockNext) // Assert expect(response.statusCode).toBe(200) expect(response._getData()).toBe('') expect(mockNext).toHaveBeenCalledOnce() expect(mockNext).toHaveBeenCalledWith(error) }) it('returns an empty object when no collections exist', async () => { // Arrange: Cleanup is handled by setupModelCleanup const request = createRequest() const response = createResponse() // Act await ApiController.index(request, response, mockNext) // Assert expect(response.statusCode).toBe(200) expect(JSON.parse(response._getData())).toEqual({}) expect(mockNext).not.toHaveBeenCalled() }) }) }) ================================================ FILE: src/tests/controllers/api/v2024Controller.test.ts ================================================ import { createRequest, createResponse } from 'node-mocks-http' import { describe, expect, it, vi } from 'vitest' import * as ApiController from '@/controllers/api/v2024Controller' import CollectionModel from '@/models/2024/collection' import { collectionFactory } from '@/tests/factories/2024/collection.factory' import { mockNext as defaultMockNext } from '@/tests/support' import { generateUniqueDbUri, setupIsolatedDatabase, setupModelCleanup, teardownIsolatedDatabase } from '@/tests/support/db' const mockNext = vi.fn(defaultMockNext) // Generate URI for this test file const dbUri = generateUniqueDbUri('v2024') // Setup hooks using helpers setupIsolatedDatabase(dbUri) teardownIsolatedDatabase() setupModelCleanup(CollectionModel) describe('v2024 API Controller', () => { describe('index', () => { it('returns the map of available API routes', async () => { const collectionsData = collectionFactory.buildList(3) await CollectionModel.insertMany(collectionsData) const request = createRequest() const response = createResponse() const expectedResponse = collectionsData.reduce( (acc, col) => { acc[col.index] = `/api/2024/${col.index}` return acc }, {} as Record ) await ApiController.index(request, response, mockNext) // Assert: Check the response const actualResponse = JSON.parse(response._getData()) expect(response.statusCode).toBe(200) expect(actualResponse).toEqual(expectedResponse) expect(mockNext).not.toHaveBeenCalled() }) it('handles database errors during find', async () => { // Arrange const request = createRequest() const response = createResponse() const error = new Error('Database find failed') vi.spyOn(CollectionModel, 'find').mockImplementationOnce(() => { const query = { select: vi.fn().mockReturnThis(), sort: vi.fn().mockReturnThis(), exec: vi.fn().mockRejectedValueOnce(error) } as any return query }) // Act await ApiController.index(request, response, mockNext) // Assert expect(response.statusCode).toBe(200) expect(response._getData()).toBe('') expect(mockNext).toHaveBeenCalledOnce() expect(mockNext).toHaveBeenCalledWith(error) }) it('returns an empty object when no collections exist', async () => { // Arrange: Cleanup is handled by setupModelCleanup const request = createRequest() const response = createResponse() await ApiController.index(request, response, mockNext) // Assert expect(response.statusCode).toBe(200) expect(JSON.parse(response._getData())).toEqual({}) expect(mockNext).not.toHaveBeenCalled() }) }) }) ================================================ FILE: src/tests/controllers/apiController.test.ts ================================================ import { createRequest, createResponse } from 'node-mocks-http' import { describe, expect, it, vi } from 'vitest' import deprecatedApiController from '@/controllers/apiController' import CollectionModel from '@/models/2014/collection' // Use Model suffix convention import { mockNext as defaultMockNext } from '@/tests/support' // Assuming mockNext is here import { generateUniqueDbUri, setupIsolatedDatabase, setupModelCleanup, teardownIsolatedDatabase } from '@/tests/support/db' const mockNext = vi.fn(defaultMockNext) // Generate URI for this test file const dbUri = generateUniqueDbUri('api') // Setup hooks using helpers setupIsolatedDatabase(dbUri) teardownIsolatedDatabase() setupModelCleanup(CollectionModel) describe('deprecated /api controller', () => { it('redirects /api/ to /api/2014/', async () => { const request = createRequest({ path: '/' }) // Path relative to where the controller is mounted const response = createResponse() const redirectSpy = vi.spyOn(response, 'redirect') await deprecatedApiController(request, response, mockNext) expect(response.statusCode).toBe(301) expect(redirectSpy).toHaveBeenCalledWith(301, '/api/2014/') expect(mockNext).not.toHaveBeenCalled() }) it('redirects /api/ to /api/2014/', async () => { await CollectionModel.insertMany([{ index: 'valid-endpoint' }]) const request = createRequest({ method: 'GET', path: '/valid-endpoint' }) const response = createResponse() const redirectSpy = vi.spyOn(response, 'redirect') await deprecatedApiController(request, response, mockNext) expect(response.statusCode).toBe(301) expect(redirectSpy).toHaveBeenCalledWith(301, '/api/2014/valid-endpoint') expect(mockNext).not.toHaveBeenCalled() }) it('responds with 404 for invalid sub-routes', async () => { const request = createRequest({ path: '/invalid-endpoint' }) const response = createResponse() const sendStatusSpy = vi.spyOn(response, 'sendStatus') await deprecatedApiController(request, response, mockNext) expect(sendStatusSpy).toHaveBeenCalledWith(404) expect(mockNext).not.toHaveBeenCalled() }) }) ================================================ FILE: src/tests/controllers/globalSetup.ts ================================================ import { MongoMemoryServer } from 'mongodb-memory-server' // Define types for global variables // Use declare global {} for augmenting the global scope safely declare global { var __MONGOD__: MongoMemoryServer | undefined } // Function to generate a temporary directory path for Redis // Removed getTmpDir as it's no longer needed export async function setup(): Promise<() => Promise> { console.log('\n[Global Setup - Unit Tests] Starting test servers...') // --- MongoDB Setup --- try { console.log('[Global Setup - Unit Tests] Starting MongoMemoryServer...') const mongod = await MongoMemoryServer.create() const baseMongoUri = mongod.getUri().split('?')[0] const serverUri = baseMongoUri.endsWith('/') ? baseMongoUri : baseMongoUri + '/' process.env.TEST_MONGODB_URI_BASE = serverUri // Set env var for tests globalThis.__MONGOD__ = mongod // Store instance globally console.log(`[Global Setup - Unit Tests] MongoMemoryServer started at base URI: ${serverUri}`) } catch (error) { console.error('[Global Setup - Unit Tests] Failed to start MongoMemoryServer:', error) throw new Error( `Failed to start MongoMemoryServer: ${error instanceof Error ? error.message : error}`, { cause: error } ) } console.log('[Global Setup - Unit Tests] MongoMemoryServer running.') // Return the teardown function return async () => { console.log('\n[Global Teardown - Unit Tests] Stopping test servers...') if (globalThis.__MONGOD__) { try { await globalThis.__MONGOD__.stop() console.log('[Global Teardown - Unit Tests] MongoMemoryServer stopped.') } catch (error) { console.error('[Global Teardown - Unit Tests] Error stopping MongoMemoryServer:', error) } globalThis.__MONGOD__ = undefined } console.log('[Global Teardown - Unit Tests] MongoMemoryServer stopped.') } } ================================================ FILE: src/tests/controllers/simpleController.test.ts ================================================ import { type Model } from 'mongoose' import { createRequest, createResponse } from 'node-mocks-http' import { beforeAll, describe, expect, it, vi } from 'vitest' import SimpleController from '@/controllers/simpleController' import AbilityScoreModel from '@/models/2014/abilityScore' // Use Model suffix convention import { abilityScoreFactory } from '@/tests/factories/2014/abilityScore.factory' // Import factory import { mockNext as defaultMockNext } from '@/tests/support' // Assuming mockNext is here import { generateUniqueDbUri, setupIsolatedDatabase, setupModelCleanup, teardownIsolatedDatabase } from '@/tests/support/db' const mockNext = vi.fn(defaultMockNext) // Generate URI for this test file const dbUri = generateUniqueDbUri('simple') // Setup hooks using helpers setupIsolatedDatabase(dbUri) teardownIsolatedDatabase() setupModelCleanup(AbilityScoreModel) let abilityScoreController: SimpleController // Keep declaration describe('SimpleController (with AbilityScore)', () => { beforeAll(async () => { // Initialize controller after connection abilityScoreController = new SimpleController(AbilityScoreModel as Model) }) describe('index', () => { it('returns a list of documents', async () => { // Arrange const scoresData = abilityScoreFactory.buildList(3) await AbilityScoreModel.insertMany(scoresData) const request = createRequest() const response = createResponse() // Act await abilityScoreController.index(request, response, mockNext) // Assert expect(response.statusCode).toBe(200) const responseData = JSON.parse(response._getData()) expect(responseData.count).toBe(3) expect(responseData.results).toHaveLength(3) expect(responseData.results).toEqual( expect.arrayContaining([ // SimpleController index returns index, name, url expect.objectContaining({ index: scoresData[0].index, name: scoresData[0].name, url: scoresData[0].url }), expect.objectContaining({ index: scoresData[1].index, name: scoresData[1].name, url: scoresData[1].url }), expect.objectContaining({ index: scoresData[2].index, name: scoresData[2].name, url: scoresData[2].url }) ]) ) expect(mockNext).not.toHaveBeenCalled() }) it('handles database errors during find', async () => { // Arrange const request = createRequest() const response = createResponse() const error = new Error('Database find failed') vi.spyOn(AbilityScoreModel, 'find').mockImplementationOnce(() => { const query = { select: vi.fn().mockReturnThis(), sort: vi.fn().mockReturnThis(), exec: vi.fn().mockRejectedValueOnce(error) } as any return query }) // Act await abilityScoreController.index(request, response, mockNext) // Assert expect(response.statusCode).toBe(200) expect(response._getData()).toBe('') expect(mockNext).toHaveBeenCalledOnce() expect(mockNext).toHaveBeenCalledWith(error) }) it('returns an empty list when no documents exist', async () => { const request = createRequest() const response = createResponse() await abilityScoreController.index(request, response, mockNext) expect(response.statusCode).toBe(200) const responseData = JSON.parse(response._getData()) expect(responseData.count).toBe(0) expect(responseData.results).toEqual([]) expect(mockNext).not.toHaveBeenCalled() }) }) describe('show', () => { it('returns a single document when found', async () => { // Arrange const scoreData = abilityScoreFactory.build({ index: 'str', name: 'STR' }) await AbilityScoreModel.insertMany([scoreData]) const request = createRequest({ params: { index: 'str' } }) const response = createResponse() // Act await abilityScoreController.show(request, response, mockNext) // Assert expect(response.statusCode).toBe(200) const responseData = JSON.parse(response._getData()) // Check against the specific fields returned by show expect(responseData).toMatchObject({ index: scoreData.index, name: scoreData.name, full_name: scoreData.full_name, desc: scoreData.desc // url is often included too }) expect(mockNext).not.toHaveBeenCalled() }) it('calls next() when the document is not found', async () => { // Arrange const request = createRequest({ params: { index: 'nonexistent' } }) const response = createResponse() // Act await abilityScoreController.show(request, response, mockNext) // Assert expect(response.statusCode).toBe(200) expect(response._getData()).toBe('') expect(mockNext).toHaveBeenCalledOnce() expect(mockNext).toHaveBeenCalledWith() }) it('handles database errors during findOne', async () => { // Arrange const request = createRequest({ params: { index: 'str' } }) const response = createResponse() const error = new Error('Database findOne failed') vi.spyOn(AbilityScoreModel, 'findOne').mockRejectedValueOnce(error) // Act await abilityScoreController.show(request, response, mockNext) // Assert expect(response.statusCode).toBe(200) expect(response._getData()).toBe('') expect(mockNext).toHaveBeenCalledOnce() expect(mockNext).toHaveBeenCalledWith(error) }) }) }) ================================================ FILE: src/tests/factories/2014/abilityScore.factory.ts ================================================ import { faker } from '@faker-js/faker' import { Factory } from 'fishery' import { AbilityScore } from '@/models/2014/abilityScore' import { apiReferenceFactory } from './common.factory' // Define the factory using fishery export const abilityScoreFactory = Factory.define( ({ sequence, params, transientParams }) => { // params are overrides // transientParams are params not part of the final object, useful for intermediate logic // sequence provides a unique number for each generated object // Use transientParams for defaults that might be complex or used multiple times const name = params.name ?? transientParams.baseName ?? `Ability Score ${sequence}` const index = params.index ?? name.toLowerCase().replace(/\s+/g, '-') return { // Required fields index, name, full_name: params.full_name ?? `Full ${name}`, desc: params.desc ?? [faker.lorem.paragraph()], // Simplified default url: params.url ?? `/api/ability-scores/${index}`, updated_at: params.updated_at ?? faker.date.recent().toISOString(), // Non-required fields - Use the imported factory skills: params.skills ?? apiReferenceFactory.buildList(2), // Build a list of 2 APIReferences // Merging params ensures overrides work correctly ...params } } ) ================================================ FILE: src/tests/factories/2014/alignment.factory.ts ================================================ import { faker } from '@faker-js/faker' import { Factory } from 'fishery' import { Alignment } from '@/models/2014/alignment' export const alignmentFactory = Factory.define(({ sequence, params }) => { const name = params.name ?? `Alignment ${sequence}` const index = params.index ?? name.toLowerCase().replace(/\s+/g, '-') return { index, name, abbreviation: params.abbreviation ?? name.substring(0, 2).toUpperCase(), // Simple default desc: params.desc ?? faker.lorem.sentence(), url: params.url ?? `/api/alignments/${index}`, updated_at: params.updated_at ?? faker.date.recent().toISOString(), ...params // Ensure overrides from params are applied } }) ================================================ FILE: src/tests/factories/2014/background.factory.ts ================================================ import { faker } from '@faker-js/faker' import { Factory } from 'fishery' import { Background, EquipmentRef } from '@/models/2014/background' import { apiReferenceFactory, choiceFactory } from './common.factory' // EquipmentRef factory const equipmentRefFactory = Factory.define(() => ({ equipment: apiReferenceFactory.build(), quantity: faker.number.int({ min: 1, max: 5 }) })) // Feature factory // Cannot name this Feature due to conflict with built-in Feature type const backgroundFeatureFactory = Factory.define(() => ({ name: faker.lorem.words(3), desc: [faker.lorem.paragraph()] })) // Main Background factory - USING COMMON FACTORIES export const backgroundFactory = Factory.define(({ sequence, params }) => { const name = params.name ?? `Background ${sequence}` const index = params.index ?? name.toLowerCase().replace(/\s+/g, '-') // Return object with defaults. Complex overrides require manual construction // or passing deep overrides to .build() return { index: index, name: name, starting_proficiencies: apiReferenceFactory.buildList(2), language_options: choiceFactory.build(), url: `/api/backgrounds/${index}`, starting_equipment: equipmentRefFactory.buildList(1), starting_equipment_options: choiceFactory.buildList(1), feature: backgroundFeatureFactory.build(), personality_traits: choiceFactory.build(), ideals: choiceFactory.build(), bonds: choiceFactory.build(), flaws: choiceFactory.build(), updated_at: faker.date.recent().toISOString() } }) ================================================ FILE: src/tests/factories/2014/class.factory.ts ================================================ import { faker } from '@faker-js/faker' import { Factory } from 'fishery' import { apiReferenceFactory, choiceFactory, createIndex, createUrl } from './common.factory' import type { Class, ClassEquipment, MultiClassing, MultiClassingPrereq, Spellcasting, SpellcastingInfo } from '@/models/2014/class' // --- Nested Factories --- // const equipmentFactory = Factory.define(({ params }) => { const builtEquipmentRef = apiReferenceFactory.build(params.equipment) return { equipment: { index: builtEquipmentRef.index, name: builtEquipmentRef.name, url: builtEquipmentRef.url }, quantity: params.quantity ?? faker.number.int({ min: 1, max: 5 }) } }) const spellcastingInfoFactory = Factory.define(({ params }) => ({ desc: params.desc ?? [faker.lorem.sentence()], name: params.name ?? faker.lorem.words(3) })) const spellcastingFactory = Factory.define(({ params }) => { const builtInfo = spellcastingInfoFactory.buildList(params.info?.length ?? 1, params.info?.[0]) // Build at least one info const builtAbility = apiReferenceFactory.build(params.spellcasting_ability) return { info: builtInfo.map((i) => ({ desc: i.desc, name: i.name })), // Map to ensure correct structure level: params.level ?? faker.number.int({ min: 1, max: 5 }), spellcasting_ability: { index: builtAbility.index, name: builtAbility.name, url: builtAbility.url } } }) const multiClassingPrereqFactory = Factory.define(({ params }) => { const builtAbility = apiReferenceFactory.build(params.ability_score) return { ability_score: { index: builtAbility.index, name: builtAbility.name, url: builtAbility.url }, minimum_score: params.minimum_score ?? faker.helpers.arrayElement([10, 12, 13, 14, 15]) } }) const multiClassingFactory = Factory.define(({ params }) => { // Build optional parts explicitly const builtPrereqs = params.prerequisites !== null ? (params.prerequisites ?? multiClassingPrereqFactory.buildList(faker.number.int({ min: 0, max: 1 }))) : undefined const builtPrereqOpts = params.prerequisite_options !== null ? (params.prerequisite_options ?? (faker.datatype.boolean(0.1) ? choiceFactory.build() : undefined)) : undefined const builtProfs = params.proficiencies !== null ? (params.proficiencies ?? apiReferenceFactory.buildList(faker.number.int({ min: 0, max: 2 }))) : undefined const builtProfChoices = params.proficiency_choices !== null ? (params.proficiency_choices ?? (faker.datatype.boolean(0.2) ? choiceFactory.buildList(faker.number.int({ min: 1, max: 2 })) : undefined)) : undefined return { prerequisites: builtPrereqs?.map((p) => ({ ability_score: p.ability_score, minimum_score: p.minimum_score })), // Map to ensure structure prerequisite_options: builtPrereqOpts ? choiceFactory.build(builtPrereqOpts) : undefined, // Re-build choice if needed proficiencies: builtProfs?.map((p) => ({ index: p.index, name: p.name, url: p.url })), // Map API Refs proficiency_choices: builtProfChoices?.map((c) => choiceFactory.build(c)) // Re-build choices if needed } }) // --- Class Factory (Main) --- // export const classFactory = Factory.define>( ({ sequence, params }) => { const name = params.name ?? `Class ${sequence}` const index = params.index ?? createIndex(name) const url = params.url ?? createUrl('classes', index) // Build dependencies const builtProfs = params.proficiencies ?? apiReferenceFactory.buildList(faker.number.int({ min: 2, max: 4 })) const builtProfChoices = params.proficiency_choices ?? choiceFactory.buildList(faker.number.int({ min: 1, max: 2 })) const builtSavingThrows = params.saving_throws ?? apiReferenceFactory.buildList(2) const builtStartingEquip = params.starting_equipment ?? equipmentFactory.buildList(faker.number.int({ min: 1, max: 3 })) const builtStartingEquipOpts = params.starting_equipment_options ?? choiceFactory.buildList(faker.number.int({ min: 1, max: 2 })) const builtSubclasses = params.subclasses ?? apiReferenceFactory.buildList(faker.number.int({ min: 1, max: 3 })) // Build optional/complex parts const builtMultiClassing = multiClassingFactory.build(params.multi_classing) // Always build this, pass null via params to omit parts const builtSpellcasting = params.spellcasting === null ? undefined : (params.spellcasting ?? (faker.datatype.boolean(0.5) ? spellcastingFactory.build() : undefined)) return { index, name, url, hit_die: params.hit_die ?? faker.helpers.arrayElement([6, 8, 10, 12]), class_levels: params.class_levels ?? `/api/classes/${index}/levels`, // Default URL structure multi_classing: builtMultiClassing, // Use the fully built object proficiencies: builtProfs.map((p) => ({ index: p.index, name: p.name, url: p.url })), // Map API Refs proficiency_choices: builtProfChoices.map((c) => choiceFactory.build(c)), // Re-build choices saving_throws: builtSavingThrows.map((p) => ({ index: p.index, name: p.name, url: p.url })), // Map API Refs spellcasting: builtSpellcasting ? spellcastingFactory.build(builtSpellcasting) : undefined, // Re-build if exists spells: params.spells ?? `/api/classes/${index}/spells`, // Default URL structure starting_equipment: builtStartingEquip.map((e) => equipmentFactory.build(e)), // Re-build equipment starting_equipment_options: builtStartingEquipOpts.map((c) => choiceFactory.build(c)), // Re-build choices subclasses: builtSubclasses.map((s) => ({ index: s.index, name: s.name, url: s.url })), // Map API Refs updated_at: params.updated_at ?? faker.date.past().toISOString() } } ) ================================================ FILE: src/tests/factories/2014/collection.factory.ts ================================================ import { faker } from '@faker-js/faker' import { Factory } from 'fishery' import { createIndex } from './common.factory' import type { Collection } from '@/models/2014/collection' // Factory only needs to define properties present in the Collection model export const collectionFactory = Factory.define>( ({ sequence, params }) => { // Generate a plausible index, or use one provided const index = params.index ?? createIndex(`${faker.word.noun()} ${sequence}`) return { index } } ) ================================================ FILE: src/tests/factories/2014/common.factory.ts ================================================ import { faker } from '@faker-js/faker' import { Factory } from 'fishery' import { APIReference } from '@/models/common/apiReference' import { AreaOfEffect } from '@/models/common/areaOfEffect' import { Choice, OptionsArrayOptionSet, StringOption } from '@/models/common/choice' import { Damage } from '@/models/common/damage' import { DifficultyClass } from '@/models/common/difficultyClass' // --- Helper Functions --- export const createIndex = (name: string): string => name .toLowerCase() .replace(/[^a-z0-9]+/g, '-') .replace(/^-|-$/g, '') export const createUrl = (resource: string, index: string): string => `/api/${resource}/${index}` // --- APIReference --- export const apiReferenceFactory = Factory.define(({ sequence, params }) => { const name = params?.name ?? `Reference ${sequence}` const index = params?.index ?? createIndex(name) // Default to a generic 'testing' resource if not provided const resource = params?.url?.split('/')[2] ?? 'testing' return { index: index, name: name, url: params?.url ?? createUrl(resource, index) } }) // --- AreaOfEffect --- export const areaOfEffectFactory = Factory.define(() => ({ size: faker.number.int({ min: 5, max: 30 }), type: faker.helpers.arrayElement(['sphere', 'cube', 'cylinder', 'line', 'cone']) })) // --- DifficultyClass --- export const difficultyClassFactory = Factory.define(() => ({ dc_type: apiReferenceFactory.build(), dc_value: faker.number.int({ min: 10, max: 25 }), success_type: faker.helpers.arrayElement(['none', 'half', 'other']) })) // --- Damage --- export const damageFactory = Factory.define(() => ({ damage_type: apiReferenceFactory.build(), damage_dice: `${faker.number.int({ min: 1, max: 4 })}d${faker.helpers.arrayElement([ 4, 6, 8, 10, 12 ])}` })) // --- Option (using StringOption as a simple representative) --- // Tests needing specific option types will need dedicated factories or manual construction export const stringOptionFactory = Factory.define(({ sequence }) => ({ option_type: 'string', string: `Option String ${sequence}` })) // --- OptionSet (using OptionsArrayOptionSet as representative) --- // Tests needing specific option set types will need dedicated factories or manual construction export const optionsArrayOptionSetFactory = Factory.define(() => ({ option_set_type: 'options_array', options: stringOptionFactory.buildList(1) // Default with one simple string option })) // --- Choice (Simplified) --- // This now uses the more concrete optionsArrayOptionSetFactory export const choiceFactory = Factory.define(() => ({ desc: faker.lorem.sentence(), choose: 1, type: 'equipment', // Default type from: optionsArrayOptionSetFactory.build() // Use the concrete subtype factory })) // --- Other Option Subtypes (Placeholders - build as needed) --- // export const referenceOptionFactory = Factory.define(...) // export const actionOptionFactory = Factory.define(...) // export const multipleOptionFactory = Factory.define(...) // export const idealOptionFactory = Factory.define(...) // export const countedReferenceOptionFactory = Factory.define(...) // ... etc. ================================================ FILE: src/tests/factories/2014/condition.factory.ts ================================================ import { faker } from '@faker-js/faker' import { Factory } from 'fishery' import { Condition } from '@/models/2014/condition' export const conditionFactory = Factory.define(({ sequence }) => { const name = `Condition ${sequence} - ${faker.lorem.words(2)}` const index = name.toLowerCase().replace(/\s+/g, '-') return { index: index, name: name, desc: [faker.lorem.paragraph(), faker.lorem.paragraph()], url: `/api/conditions/${index}`, updated_at: faker.date.recent().toISOString() } }) ================================================ FILE: src/tests/factories/2014/damageType.factory.ts ================================================ import { faker } from '@faker-js/faker' import { Factory } from 'fishery' import { DamageType } from '@/models/2014/damageType' export const damageTypeFactory = Factory.define(({ sequence }) => { const name = `Damage Type ${sequence} - ${faker.lorem.word()}` const index = name .toLowerCase() .replace(/[^a-z0-9]+/g, '-') .replace(/^-|-$/g, '') return { index: index, name: name, desc: [faker.lorem.paragraph()], url: `/api/damage-types/${index}`, updated_at: faker.date.recent().toISOString() } }) ================================================ FILE: src/tests/factories/2014/equipment.factory.ts ================================================ import { faker } from '@faker-js/faker' import { Factory } from 'fishery' import { ArmorClass, Content, Cost, Equipment, Range, Speed, ThrowRange } from '@/models/2014/equipment' import { Damage } from '@/models/common/damage' import { apiReferenceFactory } from './common.factory' // --- Sub-factories (Simple defaults/placeholders) --- const armorClassFactory = Factory.define(() => ({ base: faker.number.int({ min: 10, max: 18 }), dex_bonus: faker.datatype.boolean(), max_bonus: undefined // Usually optional })) const contentFactory = Factory.define(() => ({ item: apiReferenceFactory.build(), quantity: faker.number.int({ min: 1, max: 10 }) })) const costFactory = Factory.define(() => ({ quantity: faker.number.int({ min: 1, max: 1000 }), unit: faker.helpers.arrayElement(['cp', 'sp', 'gp']) })) const damageFactory = Factory.define(() => ({ damage_dice: `${faker.number.int({ min: 1, max: 2 })}d${faker.helpers.arrayElement([ 4, 6, 8, 10, 12 ])}`, damage_type: apiReferenceFactory.build() // Link to a damage type })) const rangeFactory = Factory.define(() => ({ normal: faker.number.int({ min: 5, max: 100 }), long: undefined // Optional })) const speedFactory = Factory.define(() => ({ quantity: faker.number.int({ min: 20, max: 60 }), unit: 'ft/round' })) const throwRangeFactory = Factory.define(() => ({ normal: faker.number.int({ min: 5, max: 30 }), long: faker.number.int({ min: 31, max: 120 }) })) const twoHandedDamageFactory = Factory.define(() => ({ damage_dice: `${faker.number.int({ min: 1, max: 2 })}d${faker.helpers.arrayElement([ 6, 8, 10, 12 ])}`, damage_type: apiReferenceFactory.build() })) // --- Main Equipment Factory --- export const equipmentFactory = Factory.define(({ sequence, params }) => { const name = `Equipment ${sequence} - ${faker.commerce.productName()}` const index = name.toLowerCase().replace(/\s+/g, '-') return { index: index, name: name, desc: [faker.lorem.sentence()], cost: costFactory.build(), equipment_category: apiReferenceFactory.build(), // Default category url: `/api/equipment/${index}`, updated_at: faker.date.recent().toISOString(), // Most fields are optional and depend on the equipment type. // Defaulting to undefined. Tests must build specific types. armor_category: undefined, armor_class: armorClassFactory.build(), capacity: undefined, category_range: undefined, contents: contentFactory.buildList(1), damage: damageFactory.build(), gear_category: undefined, image: params.image ?? `/images/equipment/${index}.png`, properties: apiReferenceFactory.buildList(faker.number.int({ min: 0, max: 3 })), quantity: undefined, range: rangeFactory.build(), special: undefined, speed: speedFactory.build(), stealth_disadvantage: undefined, str_minimum: undefined, throw_range: throwRangeFactory.build(), tool_category: undefined, two_handed_damage: twoHandedDamageFactory.build(), vehicle_category: undefined, weapon_category: undefined, weapon_range: undefined, weight: undefined } }) ================================================ FILE: src/tests/factories/2014/equipmentCategory.factory.ts ================================================ import { faker } from '@faker-js/faker' import { Factory } from 'fishery' import { EquipmentCategory } from '@/models/2014/equipmentCategory' import { apiReferenceFactory } from './common.factory' // Import common factory export const equipmentCategoryFactory = Factory.define(({ sequence }) => { const name = `Equipment Category ${sequence} - ${faker.lorem.words(2)}` const index = name .toLowerCase() .replace(/[^a-z0-9]+/g, '-') .replace(/^-|-$/g, '') return { index: index, name: name, equipment: apiReferenceFactory.buildList(faker.number.int({ min: 1, max: 5 })), // Build a list of equipment references url: `/api/equipment-categories/${index}`, updated_at: faker.date.recent().toISOString() } }) ================================================ FILE: src/tests/factories/2014/feat.factory.ts ================================================ import { faker } from '@faker-js/faker' import { Factory } from 'fishery' import { apiReferenceFactory, createIndex, createUrl } from './common.factory' // Import common factory for APIReference and choiceFactory import type { Feat, Prerequisite } from '@/models/2014/feat' // --- Prerequisite Factory --- const prerequisiteFactory = Factory.define(({ params }) => { // Build dependency first const builtApiRef = apiReferenceFactory.build(params.ability_score) return { // Explicitly construct the object ability_score: { index: builtApiRef.index, name: builtApiRef.name, url: builtApiRef.url }, minimum_score: params.minimum_score ?? faker.number.int({ min: 8, max: 15 }) } }) // --- Feat Factory --- export const featFactory = Factory.define>( ({ sequence, params }) => { const name = params.name ?? `${faker.word.adjective()} Feat ${sequence}` const index = params.index ?? createIndex(name) // Explicitly build a list of Prerequisite objects const prerequisites = params.prerequisites ?? prerequisiteFactory.buildList(faker.number.int({ min: 0, max: 1 })) return { index, name, prerequisites: prerequisites.map((p) => ({ // Ensure the array type is correct ability_score: p.ability_score, minimum_score: p.minimum_score })), desc: params.desc ?? [faker.lorem.paragraph()], url: params.url ?? createUrl('feats', index), updated_at: params.updated_at ?? faker.date.past().toISOString() } } ) ================================================ FILE: src/tests/factories/2014/feature.factory.ts ================================================ import { faker } from '@faker-js/faker' import { Factory } from 'fishery' import { Feature, FeaturePrerequisite, FeatureSpecific, LevelPrerequisite, SpellPrerequisite } from '@/models/2014/feature' import { apiReferenceFactory, choiceFactory } from './common.factory' // --- Sub-factories --- const levelPrerequisiteFactory = Factory.define(() => ({ type: 'level', level: faker.number.int({ min: 1, max: 20 }) })) const featurePrerequisiteFactory = Factory.define(() => ({ type: 'feature', feature: `/api/features/${faker.lorem.slug()}` // Example feature URL })) const spellPrerequisiteFactory = Factory.define(() => ({ type: 'spell', spell: `/api/spells/${faker.lorem.slug()}` // Example spell URL })) // Basic placeholder for FeatureSpecific - tests needing details should build manually const featureSpecificFactory = Factory.define(() => ({ subfeature_options: choiceFactory.build(), expertise_options: choiceFactory.build(), invocations: apiReferenceFactory.buildList(1) })) // --- Main Feature Factory --- export const featureFactory = Factory.define(({ sequence }) => { const name = `Feature ${sequence} - ${faker.lorem.words(2)}` const index = name .toLowerCase() .replace(/[^a-z0-9]+/g, '-') .replace(/^-|-$/g, '') const level = faker.number.int({ min: 1, max: 20 }) return { index: index, name: name, level: level, class: apiReferenceFactory.build(), // Link to a class by default desc: [faker.lorem.paragraph()], url: `/api/features/${index}`, updated_at: faker.date.recent().toISOString(), // Optional fields defaulted to undefined prerequisites: [ levelPrerequisiteFactory.build(), featurePrerequisiteFactory.build(), spellPrerequisiteFactory.build() ], // Default to no prerequisites parent: undefined, subclass: undefined, feature_specific: featureSpecificFactory.build(), reference: undefined } }) ================================================ FILE: src/tests/factories/2014/language.factory.ts ================================================ import { faker } from '@faker-js/faker' import { Factory } from 'fishery' import { Language } from '@/models/2014/language' const languageTypes = ['Standard', 'Exotic'] const scripts = ['Common', 'Elvish', 'Dwarvish', 'Infernal', 'Draconic', 'Celestial'] export const languageFactory = Factory.define(({ sequence }) => { const name = `Language ${sequence} - ${faker.lorem.word()}` const index = name .toLowerCase() .replace(/[^a-z0-9]+/g, '-') .replace(/^-|-$/g, '') return { index: index, name: name, desc: faker.lorem.sentence(), script: faker.helpers.arrayElement(scripts), type: faker.helpers.arrayElement(languageTypes), typical_speakers: [faker.person.jobTitle(), faker.person.jobTitle()], url: `/api/languages/${index}`, updated_at: faker.date.recent().toISOString() } }) ================================================ FILE: src/tests/factories/2014/level.factory.ts ================================================ import { faker } from '@faker-js/faker' import { Factory } from 'fishery' import { apiReferenceFactory } from './common.factory' import type { ClassSpecific, ClassSpecificCreatingSpellSlot, ClassSpecificMartialArt, ClassSpecificSneakAttack, Level, LevelSpellcasting, SubclassSpecific } from '@/models/2014/level' const createIndex = (base: string, level: number): string => `${base}-${level}` const createUrl = (resource: string, index: string): string => `/api/${resource}/${index}` // --- Nested Factories for Level --- // const classSpecificCreatingSpellSlotFactory = Factory.define( () => ({ sorcery_point_cost: faker.number.int({ min: 1, max: 5 }), spell_slot_level: faker.number.int({ min: 1, max: 3 }) }) ) const classSpecificMartialArtFactory = Factory.define(() => ({ dice_count: 1, dice_value: faker.helpers.arrayElement([4, 6]) })) const classSpecificSneakAttackFactory = Factory.define(() => ({ dice_count: faker.number.int({ min: 1, max: 4 }), dice_value: 6 })) // Factory for the main ClassSpecific object (many optional fields) const classSpecificFactory = Factory.define(() => { // Only include a few common ones by default, others can be added via params const specifics: Partial = {} if (faker.datatype.boolean(0.2)) specifics.extra_attacks = 1 if (faker.datatype.boolean(0.1)) specifics.ki_points = faker.number.int({ min: 2, max: 10 }) if (faker.datatype.boolean(0.1)) specifics.sorcery_points = faker.number.int({ min: 2, max: 10 }) if (faker.datatype.boolean(0.1)) specifics.rage_count = faker.number.int({ min: 2, max: 4 }) if (faker.datatype.boolean(0.1)) specifics.invocations_known = faker.number.int({ min: 2, max: 5 }) // Add others here as needed or pass through params return specifics }) // Factory for LevelSpellcasting (many required fields) const levelSpellcastingFactory = Factory.define(({ params }) => ({ cantrips_known: params.cantrips_known ?? faker.number.int({ min: 2, max: 4 }), spell_slots_level_1: params.spell_slots_level_1 ?? faker.number.int({ min: 0, max: 4 }), spell_slots_level_2: params.spell_slots_level_2 ?? faker.number.int({ min: 0, max: 3 }), spell_slots_level_3: params.spell_slots_level_3 ?? faker.number.int({ min: 0, max: 3 }), spell_slots_level_4: params.spell_slots_level_4 ?? faker.number.int({ min: 0, max: 3 }), spell_slots_level_5: params.spell_slots_level_5 ?? faker.number.int({ min: 0, max: 2 }), spell_slots_level_6: params.spell_slots_level_6 ?? faker.number.int({ min: 0, max: 1 }), spell_slots_level_7: params.spell_slots_level_7 ?? faker.number.int({ min: 0, max: 1 }), spell_slots_level_8: params.spell_slots_level_8 ?? faker.number.int({ min: 0, max: 1 }), spell_slots_level_9: params.spell_slots_level_9 ?? faker.number.int({ min: 0, max: 1 }), spells_known: params.spells_known ?? faker.number.int({ min: 0, max: 10 }) })) // Factory for SubclassSpecific (optional fields) const subclassSpecificFactory = Factory.define(() => { const specifics: Partial = {} if (faker.datatype.boolean(0.1)) specifics.additional_magical_secrets_max_lvl = faker.helpers.arrayElement([5, 7, 9]) if (faker.datatype.boolean(0.1)) specifics.aura_range = faker.helpers.arrayElement([10, 30]) return specifics }) // --- Level Factory (Main) --- // export const levelFactory = Factory.define>( ({ sequence, params }) => { const level = params.level ?? (sequence <= 20 ? sequence : faker.number.int({ min: 1, max: 20 })) // Build APIReference dependencies first const builtClass = apiReferenceFactory.build(params.class) const builtSubclass = params.subclass ? apiReferenceFactory.build(params.subclass) : undefined const builtFeatures = params.features ? apiReferenceFactory.buildList(params.features.length) : apiReferenceFactory.buildList(faker.number.int({ min: 0, max: 2 })) const index = params.index ?? createIndex( builtSubclass ? `${builtSubclass.index}-level` : `${builtClass.index}-level`, level ) const url = params.url ?? createUrl('levels', index) // Build potential complex nested objects - ensuring full structure const shouldBuildClassSpecific = params.class_specific !== undefined ? params.class_specific !== null : faker.datatype.boolean(0.5) const builtClassSpecific = shouldBuildClassSpecific ? classSpecificFactory.build(params.class_specific) : undefined const shouldBuildSpellcasting = params.spellcasting !== undefined ? params.spellcasting !== null : faker.datatype.boolean(0.5) const builtSpellcasting = shouldBuildSpellcasting ? levelSpellcastingFactory.build(params.spellcasting) : undefined const shouldBuildSubclassSpecific = builtSubclass != null && (params.subclass_specific !== undefined ? params.subclass_specific !== null : faker.datatype.boolean(0.3)) const builtSubclassSpecific = shouldBuildSubclassSpecific ? subclassSpecificFactory.build(params.subclass_specific) : undefined return { level, index, url, class: { // Explicit construction index: builtClass.index, name: builtClass.name, url: builtClass.url }, subclass: builtSubclass ? { // Explicit construction index: builtSubclass.index, name: builtSubclass.name, url: builtSubclass.url } : undefined, features: builtFeatures.map((f) => ({ index: f.index, name: f.name, url: f.url })), // Explicit construction ability_score_bonuses: params.ability_score_bonuses ?? (level % 4 === 0 ? 1 : 0), prof_bonus: params.prof_bonus ?? Math.floor((level - 1) / 4) + 2, // Explicitly construct complex nested objects, providing defaults/undefined for all fields class_specific: builtClassSpecific ? { action_surges: builtClassSpecific.action_surges, arcane_recovery_levels: builtClassSpecific.arcane_recovery_levels, aura_range: builtClassSpecific.aura_range, bardic_inspiration_die: builtClassSpecific.bardic_inspiration_die, brutal_critical_dice: builtClassSpecific.brutal_critical_dice, channel_divinity_charges: builtClassSpecific.channel_divinity_charges, creating_spell_slots: builtClassSpecific.creating_spell_slots?.map((s) => classSpecificCreatingSpellSlotFactory.build(s) ), // Need factory for nested array destroy_undead_cr: builtClassSpecific.destroy_undead_cr, extra_attacks: builtClassSpecific.extra_attacks, favored_enemies: builtClassSpecific.favored_enemies, favored_terrain: builtClassSpecific.favored_terrain, indomitable_uses: builtClassSpecific.indomitable_uses, invocations_known: builtClassSpecific.invocations_known, ki_points: builtClassSpecific.ki_points, magical_secrets_max_5: builtClassSpecific.magical_secrets_max_5, magical_secrets_max_7: builtClassSpecific.magical_secrets_max_7, magical_secrets_max_9: builtClassSpecific.magical_secrets_max_9, martial_arts: builtClassSpecific.martial_arts ? classSpecificMartialArtFactory.build(builtClassSpecific.martial_arts) : undefined, metamagic_known: builtClassSpecific.metamagic_known, mystic_arcanum_level_6: builtClassSpecific.mystic_arcanum_level_6, mystic_arcanum_level_7: builtClassSpecific.mystic_arcanum_level_7, mystic_arcanum_level_8: builtClassSpecific.mystic_arcanum_level_8, mystic_arcanum_level_9: builtClassSpecific.mystic_arcanum_level_9, rage_count: builtClassSpecific.rage_count, rage_damage_bonus: builtClassSpecific.rage_damage_bonus, sneak_attack: builtClassSpecific.sneak_attack ? classSpecificSneakAttackFactory.build(builtClassSpecific.sneak_attack) : undefined, song_of_rest_die: builtClassSpecific.song_of_rest_die, sorcery_points: builtClassSpecific.sorcery_points, unarmored_movement: builtClassSpecific.unarmored_movement, wild_shape_fly: builtClassSpecific.wild_shape_fly, wild_shape_max_cr: builtClassSpecific.wild_shape_max_cr, wild_shape_swim: builtClassSpecific.wild_shape_swim } : undefined, spellcasting: builtSpellcasting ? { cantrips_known: builtSpellcasting.cantrips_known, spell_slots_level_1: builtSpellcasting.spell_slots_level_1 ?? 0, // Ensure required fields have defaults spell_slots_level_2: builtSpellcasting.spell_slots_level_2 ?? 0, spell_slots_level_3: builtSpellcasting.spell_slots_level_3 ?? 0, spell_slots_level_4: builtSpellcasting.spell_slots_level_4 ?? 0, spell_slots_level_5: builtSpellcasting.spell_slots_level_5 ?? 0, spell_slots_level_6: builtSpellcasting.spell_slots_level_6, spell_slots_level_7: builtSpellcasting.spell_slots_level_7, spell_slots_level_8: builtSpellcasting.spell_slots_level_8, spell_slots_level_9: builtSpellcasting.spell_slots_level_9, spells_known: builtSpellcasting.spells_known } : undefined, subclass_specific: builtSubclassSpecific ? { additional_magical_secrets_max_lvl: builtSubclassSpecific.additional_magical_secrets_max_lvl, aura_range: builtSubclassSpecific.aura_range } : undefined, updated_at: params.updated_at ?? faker.date.past().toISOString() } } ) ================================================ FILE: src/tests/factories/2014/magicItem.factory.ts ================================================ import { faker } from '@faker-js/faker' import { Factory } from 'fishery' import { apiReferenceFactory, createIndex, createUrl } from './common.factory' import type { MagicItem, Rarity } from '@/models/2014/magicItem' // --- Rarity Factory --- const rarityFactory = Factory.define(() => ({ name: faker.helpers.arrayElement(['Common', 'Uncommon', 'Rare', 'Very Rare', 'Legendary']) })) // --- MagicItem Factory --- export const magicItemFactory = Factory.define>( ({ sequence, params }) => { const name = params.name ?? `Magic Item ${sequence}` const index = params.index ?? createIndex(name) // Build dependencies first to ensure complete objects const builtEquipmentCategory = apiReferenceFactory.build(params.equipment_category) const builtRarity = rarityFactory.build(params.rarity) return { index, name, desc: params.desc ?? [faker.lorem.paragraph()], equipment_category: { index: builtEquipmentCategory.index, name: builtEquipmentCategory.name, url: builtEquipmentCategory.url }, rarity: { name: builtRarity.name }, variants: params.variants ?? [], variant: params.variant ?? false, url: params.url ?? createUrl('magic-items', index), image: params.image ?? `/images/magic-items/${index}.png`, updated_at: params.updated_at ?? faker.date.past().toISOString() } } ) ================================================ FILE: src/tests/factories/2014/magicSchool.factory.ts ================================================ import { faker } from '@faker-js/faker' import { Factory } from 'fishery' import { MagicSchool } from '@/models/2014/magicSchool' export const magicSchoolFactory = Factory.define(({ sequence }) => { const name = `Magic School ${sequence} - ${faker.lorem.words(1)}` const index = name .toLowerCase() .replace(/[^a-z0-9]+/g, '-') .replace(/^-|-$/g, '') return { index: index, name: name, desc: faker.lorem.paragraph(), url: `/api/magic-schools/${index}`, updated_at: faker.date.recent().toISOString() } }) ================================================ FILE: src/tests/factories/2014/monster.factory.ts ================================================ import { faker } from '@faker-js/faker' import { Factory } from 'fishery' import mongoose from 'mongoose' import { apiReferenceFactory, choiceFactory, damageFactory, difficultyClassFactory } from './common.factory' import type { ActionOption, ActionUsage, ArmorClassArmor, ArmorClassCondition, ArmorClassDex, ArmorClassNatural, ArmorClassSpell, LegendaryAction, Monster, MonsterAction, MonsterDocument, MonsterProficiency, MonsterSpeed, Reaction, Sense, SpecialAbility, SpecialAbilitySpell, SpecialAbilitySpellcasting, SpecialAbilityUsage } from '@/models/2014/monster' // Factory for ActionUsage const actionUsageFactory = Factory.define(() => ({ type: faker.lorem.words(1), dice: `${faker.number.int({ min: 1, max: 4 })}d${faker.number.int({ min: 4, max: 12 })}`, min_value: faker.number.int({ min: 1, max: 10 }) })) // Factory for ActionOption const actionOptionFactory = Factory.define(() => ({ action_name: faker.lorem.words(2), count: faker.helpers.arrayElement([faker.number.int({ min: 1, max: 3 }), faker.lorem.word()]), type: faker.helpers.arrayElement(['melee', 'ranged', 'ability', 'magic']) })) // Factory for Action const actionFactory = Factory.define< MonsterAction, { has_damage?: boolean; has_dc?: boolean; has_attack_bonus?: boolean; has_usage?: boolean }, MonsterAction >(({ transientParams, associations }) => { const generated_multiattack_type = faker.helpers.arrayElement(['actions', 'action_options']) const baseAction = { name: faker.lorem.words(2), desc: faker.lorem.paragraph(), attack_bonus: transientParams?.has_attack_bonus === true ? faker.number.int({ min: 0, max: 10 }) : undefined, damage: associations.damage ?? (transientParams?.has_damage === true ? damageFactory.buildList(faker.number.int({ min: 1, max: 2 })) : []), dc: associations.dc ?? (transientParams?.has_dc === true ? difficultyClassFactory.build() : undefined), options: undefined, // Assuming Action['options'] is optional or handled elsewhere usage: associations.usage ?? (transientParams?.has_usage === true ? actionUsageFactory.build() : undefined) } if (generated_multiattack_type === 'actions') { return { ...baseAction, multiattack_type: 'actions', actions: actionOptionFactory.buildList(faker.number.int({ min: 1, max: 3 })), action_options: choiceFactory.build() } as MonsterAction } else { return { ...baseAction, multiattack_type: 'action_options', actions: [], action_options: choiceFactory.build() } as MonsterAction } }) // Factory for ArmorClass (Union Type - need specific factories or a generic one) // Example for 'natural' type // const armorClassNaturalFactory = Factory.define(() => ({ // type: 'natural', // value: faker.number.int({ min: 10, max: 20 }), // desc: faker.datatype.boolean() ? faker.lorem.sentence() : undefined // })) // Example for 'armor' type // const armorClassArmorFactory = Factory.define(({ associations }) => ({ // type: 'armor', // value: faker.number.int({ min: 12, max: 18 }), // armor: associations.armor // ? associations.armor // : apiReferenceFactory.buildList(faker.number.int({ min: 0, max: 1 })), // desc: faker.datatype.boolean() ? faker.lorem.sentence() : undefined // })) // A helper to create a random ArmorClass type const armorClassFactory = Factory.define< ArmorClassDex | ArmorClassNatural | ArmorClassArmor | ArmorClassSpell | ArmorClassCondition >(() => { const type = faker.helpers.arrayElement(['dex', 'natural', 'armor', 'spell', 'condition']) const value = faker.number.int({ min: 10, max: 25 }) const desc = faker.datatype.boolean() ? faker.lorem.sentence() : undefined switch (type) { case 'dex': return { type, value, desc } case 'natural': return { type, value, desc } case 'armor': return { type, value, desc, armor: apiReferenceFactory.buildList(faker.number.int({ min: 0, max: 1 })) } case 'spell': return { type, value, desc, spell: apiReferenceFactory.build() } case 'condition': return { type, value, desc, condition: apiReferenceFactory.build() } default: // Should not happen with the defined types return { type: 'natural', value: 10 } // Fallback } }) // Factory for LegendaryAction const legendaryActionFactory = Factory.define(({ associations }) => ({ name: faker.lorem.words(3), desc: faker.lorem.sentence(), attack_bonus: faker.datatype.boolean() ? faker.number.int({ min: 0, max: 12 }) : undefined, damage: associations.damage ? associations.damage : damageFactory.buildList(faker.number.int({ min: 0, max: 2 })), dc: associations.dc ? associations.dc : faker.datatype.boolean() ? difficultyClassFactory.build() : undefined })) // Factory for Proficiency const proficiencyFactory = Factory.define(({ associations }) => ({ proficiency: apiReferenceFactory.build(associations.proficiency), value: faker.number.int({ min: 1, max: 10 }) })) // Factory for Reaction const reactionFactory = Factory.define(({ associations }) => ({ name: faker.lorem.words(2), desc: faker.lorem.sentence(), dc: associations.dc ? associations.dc : faker.datatype.boolean() ? difficultyClassFactory.build() : undefined })) // Factory for Sense const senseFactory = Factory.define(() => ({ blindsight: faker.datatype.boolean() ? `${faker.number.int({ min: 10, max: 120 })} ft.` : undefined, darkvision: faker.datatype.boolean() ? `${faker.number.int({ min: 30, max: 120 })} ft.` : undefined, passive_perception: faker.number.int({ min: 8, max: 25 }), tremorsense: faker.datatype.boolean() ? `${faker.number.int({ min: 10, max: 60 })} ft.` : undefined, truesight: faker.datatype.boolean() ? `${faker.number.int({ min: 10, max: 120 })} ft.` : undefined })) // Factory for SpecialAbilityUsage const specialAbilityUsageFactory = Factory.define(() => ({ type: faker.helpers.arrayElement([ 'At Will', 'Per Day', 'Recharge after Rest', 'Recharge on Roll' ]), times: faker.datatype.boolean() ? faker.number.int({ min: 1, max: 3 }) : undefined, rest_types: faker.datatype.boolean() ? faker.helpers.arrayElements(['short', 'long'], faker.number.int({ min: 1, max: 2 })) : undefined })) // Factory for SpecialAbilitySpell const specialAbilitySpellFactory = Factory.define(({ associations }) => ({ name: faker.lorem.words(3), level: faker.number.int({ min: 0, max: 9 }), url: `/api/spells/${faker.lorem.slug()}`, notes: faker.datatype.boolean() ? faker.lorem.sentence() : undefined, usage: associations.usage ? associations.usage : faker.datatype.boolean() ? specialAbilityUsageFactory.build() : undefined })) // Factory for SpecialAbilitySpellcasting const specialAbilitySpellcastingFactory = Factory.define( ({ associations }) => { // Generate key-value pairs first const slotEntries: [string, number][] = [] for (let i = 1; i <= 9; i++) { if (faker.datatype.boolean()) { slotEntries.push([i.toString(), faker.number.int({ min: 1, max: 4 })]) } } // Create a plain object from the entries, matching the new schema type const slotsObject = slotEntries.length > 0 ? Object.fromEntries(slotEntries) : undefined return { level: faker.datatype.boolean() ? faker.number.int({ min: 1, max: 20 }) : undefined, ability: apiReferenceFactory.build( associations.ability ?? { index: faker.helpers.arrayElement(['int', 'wis', 'cha']) } ), dc: faker.datatype.boolean() ? faker.number.int({ min: 10, max: 20 }) : undefined, modifier: faker.datatype.boolean() ? faker.number.int({ min: 0, max: 5 }) : undefined, components_required: faker.helpers.arrayElements( ['V', 'S', 'M'], faker.number.int({ min: 1, max: 3 }) ), school: faker.datatype.boolean() ? faker.lorem.word() : undefined, // Assign the plain object slots: slotsObject, spells: specialAbilitySpellFactory.buildList(faker.number.int({ min: 1, max: 5 })) } } ) // Factory for SpecialAbility const specialAbilityFactory = Factory.define(({ associations }) => { const usage = associations.usage ? associations.usage : specialAbilityUsageFactory.build() return { name: faker.lorem.words(2), desc: faker.lorem.sentence(), attack_bonus: faker.datatype.boolean() ? faker.number.int({ min: -1, max: 8 }) : undefined, damage: associations.damage ? associations.damage : damageFactory.buildList(faker.number.int({ min: 0, max: 1 })), dc: associations.dc ? associations.dc : faker.datatype.boolean() ? difficultyClassFactory.build() : undefined, spellcasting: associations.spellcasting ? associations.spellcasting : faker.datatype.boolean() ? specialAbilitySpellcastingFactory.build() : undefined, usage: usage } }) // Factory for Speed const speedFactory = Factory.define(() => { const speeds: Partial = {} if (faker.datatype.boolean()) speeds.walk = `${faker.number.int({ min: 10, max: 60 })} ft.` if (faker.datatype.boolean()) speeds.swim = `${faker.number.int({ min: 10, max: 60 })} ft.` if (faker.datatype.boolean()) speeds.fly = `${faker.number.int({ min: 10, max: 60 })} ft.` if (faker.datatype.boolean()) speeds.burrow = `${faker.number.int({ min: 10, max: 60 })} ft.` if (faker.datatype.boolean()) speeds.climb = `${faker.number.int({ min: 10, max: 60 })} ft.` if (faker.datatype.boolean()) speeds.hover = faker.datatype.boolean() // Ensure at least one speed exists if (Object.keys(speeds).length === 0 || (Object.keys(speeds).length === 1 && 'hover' in speeds)) { speeds.walk = `${faker.number.int({ min: 10, max: 60 })} ft.` } return speeds as MonsterSpeed // Cast needed because we build it partially }) // Factory for Monster - Define return type explicitly as Monster const monsterFactory = Factory.define( ({ associations, transientParams }) => { const size = faker.helpers.arrayElement([ 'Tiny', 'Small', 'Medium', 'Large', 'Huge', 'Gargantuan' ]) const type = faker.lorem.word() // Consider using helpers.arrayElement for specific types if needed const subtype = faker.datatype.boolean() ? faker.lorem.word() : undefined const alignment = faker.helpers.arrayElement([ 'chaotic neutral', 'chaotic evil', 'chaotic good', 'lawful neutral', 'lawful evil', 'lawful good', 'neutral', 'neutral evil', 'neutral good', 'any alignment', 'unaligned' ]) const slug = transientParams?.index ?? faker.lorem.slug() // Use transient index if provided // Return a plain object matching the Monster interface return { index: slug, name: transientParams?.name ?? faker.person.firstName(), // Use transient name if provided desc: faker.lorem.paragraph(), size: size, type: type, subtype: subtype, alignment: alignment, armor_class: associations.armor_class ?? armorClassFactory.buildList(faker.number.int({ min: 1, max: 2 })), hit_points: faker.number.int({ min: 10, max: 300 }), hit_dice: `${faker.number.int({ min: 1, max: 20 })}d${faker.number.int({ min: 4, max: 12 })}`, hit_points_roll: `${faker.number.int({ min: 1, max: 20 })}d${faker.number.int({ min: 4, max: 12 })} + ${faker.number.int({ min: 0, max: 50 })}`, speed: associations.speed ?? speedFactory.build(), strength: faker.number.int({ min: 3, max: 30 }), dexterity: faker.number.int({ min: 3, max: 30 }), constitution: faker.number.int({ min: 3, max: 30 }), intelligence: faker.number.int({ min: 3, max: 30 }), wisdom: faker.number.int({ min: 3, max: 30 }), charisma: faker.number.int({ min: 3, max: 30 }), proficiencies: associations.proficiencies ?? proficiencyFactory.buildList(faker.number.int({ min: 0, max: 5 })), damage_vulnerabilities: Array.from({ length: faker.number.int({ min: 0, max: 2 }) }, () => faker.lorem.word() ), damage_resistances: Array.from({ length: faker.number.int({ min: 0, max: 3 }) }, () => faker.lorem.word() ), damage_immunities: Array.from({ length: faker.number.int({ min: 0, max: 3 }) }, () => faker.lorem.word() ), condition_immunities: apiReferenceFactory.buildList( associations.condition_immunities?.length ?? faker.number.int({ min: 0, max: 3 }), associations.condition_immunities?.[0] // Pass potential overrides ), senses: associations.senses ?? senseFactory.build(), languages: transientParams?.languages ?? 'Common', // Add languages field - defaulting to "Common", allow override challenge_rating: transientParams?.challenge_rating ?? faker.helpers.arrayElement([ 0, 0.125, 0.25, 0.5, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30 ]), proficiency_bonus: faker.number.int({ min: 2, max: 9 }), xp: faker.number.int({ min: 10, max: 155000 }), special_abilities: associations.special_abilities ?? specialAbilityFactory.buildList(faker.number.int({ min: 0, max: 4 })), actions: associations.actions ?? actionFactory.buildList( faker.number.int({ min: 1, max: 5 }), {}, { transient: { has_damage: true } } ), legendary_actions: associations.legendary_actions ?? legendaryActionFactory.buildList(faker.number.int({ min: 0, max: 3 })), image: faker.datatype.boolean() ? `/api/images/monsters/${slug}.png` : undefined, reactions: transientParams?.has_reactions === true ? reactionFactory.buildList(faker.number.int({ min: 1, max: 2 })) : undefined, url: `/api/monsters/${slug}`, updated_at: faker.date.recent().toISOString() } } ) export const monsterModelFactory = Factory.define( ({ associations, transientParams }) => { // sequence is implicitly passed via the GeneratorFnOptions, not BuildOptions const monsterProps = monsterFactory.build(associations, { transient: transientParams }) const doc = { _id: new mongoose.Types.ObjectId(), __v: 0, ...monsterProps } as MonsterDocument return doc } ) export { monsterFactory } ================================================ FILE: src/tests/factories/2014/proficiency.factory.ts ================================================ import { faker } from '@faker-js/faker' import { Factory } from 'fishery' import { Proficiency } from '@/models/2014/proficiency' import { apiReferenceFactory } from './common.factory' // Main Proficiency factory export const proficiencyFactory = Factory.define(({ sequence }) => { const name = `Proficiency ${sequence} - ${faker.lorem.words(2)}` const index = name .toLowerCase() .replace(/[^a-z0-9]+/g, '-') .replace(/^-|-$/g, '') const type = faker.helpers.arrayElement([ 'saving-throws', 'skills', 'armor', 'weapons', 'tools', 'languages' ]) return { index: index, name: name, type: type, reference: apiReferenceFactory.build(), url: `/api/proficiencies/${index}`, updated_at: faker.date.recent().toISOString(), classes: apiReferenceFactory.buildList(faker.number.int({ min: 0, max: 2 })), races: apiReferenceFactory.buildList(faker.number.int({ min: 0, max: 1 })) } }) ================================================ FILE: src/tests/factories/2014/race.factory.ts ================================================ import { faker } from '@faker-js/faker' import { Factory } from 'fishery' import { apiReferenceFactory, choiceFactory } from './common.factory' import type { Race, RaceAbilityBonus } from '@/models/2014/race' // Factory for the nested RaceAbilityBonus const raceAbilityBonusFactory = Factory.define(({ associations }) => ({ ability_score: associations.ability_score ?? apiReferenceFactory.build(), bonus: faker.number.int({ min: 1, max: 2 }) })) // Factory for Race export const raceFactory = Factory.define(({ sequence, associations, transientParams }) => { const name = transientParams?.name ?? `Race ${sequence}` const index = name.toLowerCase().replace(/ /g, '-') return { index, name, speed: faker.number.int({ min: 25, max: 35 }), ability_bonuses: raceAbilityBonusFactory.buildList(faker.number.int({ min: 1, max: 3 })), ability_bonus_options: associations.ability_bonus_options ?? (faker.datatype.boolean() ? choiceFactory.build() : undefined), alignment: faker.lorem.paragraph(), age: faker.lorem.paragraph(), size: faker.helpers.arrayElement(['Small', 'Medium', 'Large']), size_description: faker.lorem.paragraph(), languages: associations.languages ?? apiReferenceFactory.buildList( faker.number.int({ min: 1, max: 3 }), {}, { transient: { resourceType: 'languages' } } ), language_options: associations.language_options ?? choiceFactory.build(), // Required language_desc: faker.lorem.paragraph(), traits: associations.traits ?? apiReferenceFactory.buildList( faker.number.int({ min: 0, max: 4 }), {}, { transient: { resourceType: 'traits' } } ), subraces: associations.subraces ?? apiReferenceFactory.buildList( faker.number.int({ min: 0, max: 2 }), {}, { transient: { resourceType: 'subraces' } } ), url: `/api/races/${index}`, updated_at: faker.date.recent().toISOString() } }) ================================================ FILE: src/tests/factories/2014/rule.factory.ts ================================================ import { faker } from '@faker-js/faker' import { Factory } from 'fishery' import { Rule } from '@/models/2014/rule' import { apiReferenceFactory } from './common.factory' // Import common factory export const ruleFactory = Factory.define(({ sequence }) => { const name = `Rule ${sequence} - ${faker.lorem.words(2)}` const index = name .toLowerCase() .replace(/[^a-z0-9]+/g, '-') .replace(/^-|-$/g, '') return { index: index, name: name, desc: faker.lorem.paragraph(), // Build a list of subsection references subsections: apiReferenceFactory.buildList(faker.number.int({ min: 0, max: 3 })), url: `/api/rules/${index}`, updated_at: faker.date.recent().toISOString() } }) ================================================ FILE: src/tests/factories/2014/ruleSection.factory.ts ================================================ import { faker } from '@faker-js/faker' import { Factory } from 'fishery' import { RuleSection } from '@/models/2014/ruleSection' export const ruleSectionFactory = Factory.define(({ sequence }) => { const name = `Rule Section ${sequence} - ${faker.lorem.words(3)}` const index = name .toLowerCase() .replace(/[^a-z0-9]+/g, '-') .replace(/^-|-$/g, '') return { index: index, name: name, desc: faker.lorem.paragraph(), // Single string url: `/api/rule-sections/${index}`, updated_at: faker.date.recent().toISOString() } }) ================================================ FILE: src/tests/factories/2014/skill.factory.ts ================================================ import { faker } from '@faker-js/faker' import { Factory } from 'fishery' import { Skill } from '@/models/2014/skill' import { apiReferenceFactory } from './common.factory' // Import common factory export const skillFactory = Factory.define(({ sequence }) => { const name = `Skill ${sequence} - ${faker.lorem.words(2)}` const index = name .toLowerCase() .replace(/[^a-z0-9]+/g, '-') .replace(/^-|-$/g, '') return { index: index, name: name, desc: [faker.lorem.paragraph()], // Build a default ability score using the common factory ability_score: apiReferenceFactory.build({ index: faker.helpers.arrayElement(['str', 'dex', 'con', 'int', 'wis', 'cha']), name: faker.helpers.arrayElement(['STR', 'DEX', 'CON', 'INT', 'WIS', 'CHA']), url: faker.internet.url() }), url: `/api/skills/${index}`, updated_at: faker.date.recent().toISOString() } }) ================================================ FILE: src/tests/factories/2014/spell.factory.ts ================================================ import { faker } from '@faker-js/faker' import { Factory } from 'fishery' import { Spell, SpellDamage, SpellDC } from '@/models/2014/spell' import { apiReferenceFactory, areaOfEffectFactory } from './common.factory' // --- Sub-factories (Placeholders/Simple Defaults) --- const spellDamageFactory = Factory.define(() => ({ // Defaulting to undefined. Tests needing damage must build it. damage_type: undefined, damage_at_slot_level: undefined, damage_at_character_level: undefined })) const spellDcFactory = Factory.define(() => ({ dc_type: apiReferenceFactory.build(), dc_success: faker.helpers.arrayElement(['none', 'half']), desc: undefined // Optional })) // --- Main Spell Factory --- export const spellFactory = Factory.define(({ sequence }) => { const name = `Spell ${sequence} - ${faker.lorem.words(2)}` const index = name .toLowerCase() .replace(/[^a-z0-9]+/g, '-') .replace(/^-|-$/g, '') const level = faker.number.int({ min: 0, max: 9 }) return { index: index, name: name, desc: [faker.lorem.paragraph()], level: level, attack_type: faker.datatype.boolean(0.5) ? faker.helpers.arrayElement(['melee', 'ranged']) : undefined, casting_time: faker.word.noun(), ritual: faker.datatype.boolean(), duration: faker.helpers.arrayElement(['Instantaneous', '1 round', '1 minute']), concentration: faker.datatype.boolean(), components: faker.helpers.arrayElements(['V', 'S', 'M'], faker.number.int({ min: 1, max: 3 })), range: faker.helpers.arrayElement(['Self', 'Touch', '60 feet']), school: apiReferenceFactory.build(), classes: apiReferenceFactory.buildList(faker.number.int({ min: 1, max: 2 })), subclasses: apiReferenceFactory.buildList(faker.number.int({ min: 0, max: 1 })), url: `/api/spells/${index}`, updated_at: faker.date.recent().toISOString(), // Optional/Complex fields - Defaulted to undefined or simple placeholders // Tests needing specific values must override. higher_level: undefined, material: undefined, damage: spellDamageFactory.build(), dc: spellDcFactory.build(), heal_at_slot_level: undefined, area_of_effect: areaOfEffectFactory.build() } }) ================================================ FILE: src/tests/factories/2014/subclass.factory.ts ================================================ import { faker } from '@faker-js/faker' import { Factory } from 'fishery' import { apiReferenceFactory, createIndex, createUrl } from './common.factory' import type { Prerequisite, Subclass, SubclassSpell } from '@/models/2014/subclass' // --- Prerequisite Factory --- const prerequisiteFactory = Factory.define(({ params }) => ({ index: params.index ?? createIndex(faker.word.adjective()), name: params.name ?? faker.word.adjective(), type: params.type ?? 'spell', url: params.url ?? createUrl('testing', params.index ?? createIndex(faker.word.adjective())) })) const subclassSpellFactory = Factory.define(({ params }) => { // Build dependencies first const builtPrereqs = prerequisiteFactory.buildList( params.prerequisites?.length ?? faker.number.int({ min: 0, max: 1 }) ) const builtSpell = apiReferenceFactory.build(params.spell) return { prerequisites: builtPrereqs.map((p) => ({ index: p.index, name: p.name, type: p.type, url: p.url })), spell: builtSpell } }) // --- Subclass Factory --- export const subclassFactory = Factory.define>( ({ sequence, params }) => { const name = params.name ?? `${faker.word.adjective()} Subclass ${sequence}` const index = params.index ?? createIndex(name) // Build dependencies const builtClass = apiReferenceFactory.build(params.class) // Optional spells - build list only if params.spells is provided or randomly const spells = params.spells ?? (faker.datatype.boolean(0.3) ? subclassSpellFactory.buildList(faker.number.int({ min: 1, max: 5 })) : undefined) return { index, name, class: builtClass, subclass_flavor: params.subclass_flavor ?? faker.lorem.words(3), desc: params.desc ?? [faker.lorem.paragraph()], subclass_levels: params.subclass_levels ?? `/api/subclasses/${index}/levels`, spells: spells?.map((s: SubclassSpell) => ({ prerequisites: s.prerequisites, spell: s.spell })), url: params.url ?? createUrl('subclasses', index), updated_at: params.updated_at ?? faker.date.past().toISOString() } } ) ================================================ FILE: src/tests/factories/2014/subrace.factory.ts ================================================ import { faker } from '@faker-js/faker' import { Factory } from 'fishery' import { apiReferenceFactory } from './common.factory' import type { Subrace, SubraceAbilityBonus } from '@/models/2014/subrace' // Factory for the nested SubraceAbilityBonus const subraceAbilityBonusFactory = Factory.define(({ associations }) => ({ ability_score: associations.ability_score ?? apiReferenceFactory.build(), bonus: faker.number.int({ min: 1, max: 2 }) })) // Factory for Subrace export const subraceFactory = Factory.define( ({ sequence, associations, transientParams }) => { const name = transientParams?.name ?? `Subrace ${sequence}` const index = name.toLowerCase().replace(/ /g, '-') return { index: index, name: name, race: associations.race ?? apiReferenceFactory.build({}, { transient: { resourceType: 'races' } }), desc: faker.lorem.paragraph(), ability_bonuses: subraceAbilityBonusFactory.buildList(faker.number.int({ min: 1, max: 2 })), racial_traits: associations.racial_traits ?? apiReferenceFactory.buildList( faker.number.int({ min: 0, max: 3 }), {}, { transient: { resourceType: 'traits' } } ), url: `/api/subraces/${index}`, updated_at: faker.date.recent().toISOString() } } ) ================================================ FILE: src/tests/factories/2014/trait.factory.ts ================================================ import { faker } from '@faker-js/faker' import { Factory } from 'fishery' import { Trait } from '@/models/2014/trait' import { apiReferenceFactory, choiceFactory } from './common.factory' export const traitFactory = Factory.define(({ sequence }) => { const name = `Trait ${sequence} - ${faker.lorem.words(2)}` const index = name .toLowerCase() .replace(/[^a-z0-9]+/g, '-') .replace(/^-|-$/g, '') return { index: index, name: name, desc: [faker.lorem.paragraph()], races: [apiReferenceFactory.build()], // Default with one race subraces: [apiReferenceFactory.build()], // Default with one subrace url: `/api/traits/${index}`, updated_at: faker.date.recent().toISOString(), // Optional fields defaulted for basic structure // Tests needing specific values should override or build manually languages: [], proficiencies: [], language_options: choiceFactory.build(), proficiency_choices: choiceFactory.build(), parent: apiReferenceFactory.build(), trait_specific: undefined } }) ================================================ FILE: src/tests/factories/2014/weaponProperty.factory.ts ================================================ import { faker } from '@faker-js/faker' import { Factory } from 'fishery' import type { WeaponProperty } from '@/models/2014/weaponProperty' export const weaponPropertyFactory = Factory.define( ({ sequence, transientParams }) => { const name = transientParams?.name ?? `Weapon Property ${sequence}` const index = name.toLowerCase().replace(/\s+/g, '-') return { index, name, desc: [faker.lorem.paragraph()], // desc is string[] url: `/api/weapon-properties/${index}`, updated_at: faker.date.recent().toISOString() } } ) ================================================ FILE: src/tests/factories/2024/abilityScore.factory.ts ================================================ import { faker } from '@faker-js/faker' import { Factory } from 'fishery' import { AbilityScore2024 } from '@/models/2024/abilityScore' import { apiReferenceFactory } from './common.factory' // Define the factory using fishery export const abilityScoreFactory = Factory.define( ({ sequence, params, transientParams }) => { // params are overrides // transientParams are params not part of the final object, useful for intermediate logic // sequence provides a unique number for each generated object // Use transientParams for defaults that might be complex or used multiple times const name = params.name ?? transientParams.baseName ?? `Ability Score ${sequence}` const index = params.index ?? name.toLowerCase().replace(/\s+/g, '-') return { // Required fields index, name, full_name: params.full_name ?? `Full ${name}`, description: params.description ?? faker.lorem.paragraph(), // Simplified default url: params.url ?? `/api/ability-scores/${index}`, updated_at: params.updated_at ?? faker.date.recent().toISOString(), // Non-required fields - Use the imported factory skills: params.skills ?? apiReferenceFactory.buildList(2), // Build a list of 2 APIReferences // Merging params ensures overrides work correctly ...params } } ) ================================================ FILE: src/tests/factories/2024/alignment.factory.ts ================================================ import { faker } from '@faker-js/faker' import { Factory } from 'fishery' import { Alignment2024 } from '@/models/2024/alignment' export const alignmentFactory = Factory.define(() => { const name = faker.lorem.words() const index = name.toLowerCase().replace(/\s+/g, '-') return { index, name, abbreviation: name.substring(0, 2).toUpperCase(), description: faker.lorem.paragraph(), url: `/api/alignments/${index}`, updated_at: faker.date.recent().toISOString() } }) ================================================ FILE: src/tests/factories/2024/background.factory.ts ================================================ import { faker } from '@faker-js/faker' import { Factory } from 'fishery' import { Background2024, BackgroundFeatReference } from '@/models/2024/background' import { apiReferenceFactory } from './common.factory' export const backgroundFeatReferenceFactory = Factory.define( ({ sequence }) => { const name = `Feat ${sequence}` const index = name.toLowerCase().replace(/[^a-z0-9]+/g, '-') return { index, name, url: `/api/2024/feats/${index}` } } ) export const backgroundFactory = Factory.define(({ sequence }) => { const name = `Background ${sequence} - ${faker.lorem.word()}` const index = name .toLowerCase() .replace(/[^a-z0-9]+/g, '-') .replace(/^-|-$/g, '') return { index, name, ability_scores: apiReferenceFactory.buildList(2, {}, { transient: {} }), feat: backgroundFeatReferenceFactory.build(), proficiencies: apiReferenceFactory.buildList(2, {}, { transient: {} }), url: `/api/2024/backgrounds/${index}`, updated_at: faker.date.recent().toISOString() } }) ================================================ FILE: src/tests/factories/2024/collection.factory.ts ================================================ import { faker } from '@faker-js/faker' import { Factory } from 'fishery' import { createIndex } from './common.factory' import type { Collection2024 } from '@/models/2024/collection' // Factory only needs to define properties present in the Collection model export const collectionFactory = Factory.define>( ({ sequence, params }) => { // Generate a plausible index, or use one provided const index = params.index ?? createIndex(`${faker.word.noun()} ${sequence}`) return { index } } ) ================================================ FILE: src/tests/factories/2024/common.factory.ts ================================================ import { faker } from '@faker-js/faker' import { Factory } from 'fishery' import { APIReference } from '@/models/common/apiReference' import { AreaOfEffect } from '@/models/common/areaOfEffect' import { Choice, OptionsArrayOptionSet, StringOption } from '@/models/common/choice' import { Damage } from '@/models/common/damage' import { DifficultyClass } from '@/models/common/difficultyClass' // --- Helper Functions --- export const createIndex = (name: string): string => name .toLowerCase() .replace(/[^a-z0-9]+/g, '-') .replace(/^-|-$/g, '') export const createUrl = (resource: string, index: string): string => `/api/2024/${resource}/${index}` // --- APIReference --- export const apiReferenceFactory = Factory.define(({ sequence, params }) => { const name = params?.name ?? `Reference ${sequence}` const index = params?.index ?? createIndex(name) // Default to a generic 'testing' resource if not provided const resource = params?.url?.split('/')[3] ?? 'testing' return { index: index, name: name, url: params?.url ?? createUrl(resource, index) } }) // --- AreaOfEffect --- export const areaOfEffectFactory = Factory.define(() => ({ size: faker.number.int({ min: 5, max: 30 }), type: faker.helpers.arrayElement(['sphere', 'cube', 'cylinder', 'line', 'cone']) })) // --- DifficultyClass --- export const difficultyClassFactory = Factory.define(() => ({ dc_type: apiReferenceFactory.build(), dc_value: faker.number.int({ min: 10, max: 25 }), success_type: faker.helpers.arrayElement(['none', 'half', 'other']) })) // --- Damage --- export const damageFactory = Factory.define(() => ({ damage_type: apiReferenceFactory.build(), damage_dice: `${faker.number.int({ min: 1, max: 4 })}d${faker.helpers.arrayElement([ 4, 6, 8, 10, 12 ])}` })) // --- Option (using StringOption as a simple representative) --- // Tests needing specific option types will need dedicated factories or manual construction export const stringOptionFactory = Factory.define(({ sequence }) => ({ option_type: 'string', string: `Option String ${sequence}` })) // --- OptionSet (using OptionsArrayOptionSet as representative) --- // Tests needing specific option set types will need dedicated factories or manual construction export const optionsArrayOptionSetFactory = Factory.define(() => ({ option_set_type: 'options_array', options: stringOptionFactory.buildList(1) // Default with one simple string option })) // --- Choice (Simplified) --- // This now uses the more concrete optionsArrayOptionSetFactory export const choiceFactory = Factory.define(() => ({ desc: faker.lorem.sentence(), choose: 1, type: 'equipment', // Default type from: optionsArrayOptionSetFactory.build() // Use the concrete subtype factory })) // --- Other Option Subtypes (Placeholders - build as needed) --- // export const referenceOptionFactory = Factory.define(...) // export const actionOptionFactory = Factory.define(...) // export const multipleOptionFactory = Factory.define(...) // export const idealOptionFactory = Factory.define(...) // export const countedReferenceOptionFactory = Factory.define(...) // ... etc. ================================================ FILE: src/tests/factories/2024/condition.factory.ts ================================================ import { faker } from '@faker-js/faker' import { Factory } from 'fishery' import { Condition2024 } from '@/models/2024/condition' export const conditionFactory = Factory.define(() => { const name = faker.lorem.words() const index = name.toLowerCase().replace(/\s+/g, '-') return { index, name, description: faker.lorem.paragraph(), url: `/api/conditions/${index}`, updated_at: faker.date.recent().toISOString() } }) ================================================ FILE: src/tests/factories/2024/damageType.factory.ts ================================================ import { faker } from '@faker-js/faker' import { Factory } from 'fishery' import { DamageType2024 } from '@/models/2024/damageType' export const damageTypeFactory = Factory.define(() => { const name = faker.lorem.words() const index = name.toLowerCase().replace(/\s+/g, '-') return { index, name, description: faker.lorem.paragraph(), url: `/api/damage-types/${index}`, updated_at: faker.date.recent().toISOString() } }) ================================================ FILE: src/tests/factories/2024/equipment.factory.ts ================================================ import { faker } from '@faker-js/faker' import { Factory } from 'fishery' import { Cost, Equipment2024, Range, ThrowRange } from '@/models/2024/equipment' import { apiReferenceFactory, createIndex, damageFactory } from './common.factory' // --- Sub-factories --- const costFactory = Factory.define(() => ({ quantity: faker.number.int({ min: 1, max: 1000 }), unit: faker.helpers.arrayElement(['cp', 'sp', 'gp']) })) const rangeFactory = Factory.define(() => ({ normal: faker.number.int({ min: 5, max: 100 }), long: faker.number.int({ min: 10, max: 200 }) })) const throwRangeFactory = Factory.define(() => ({ normal: faker.number.int({ min: 5, max: 30 }), long: faker.number.int({ min: 31, max: 120 }) })) // --- Main Equipment Factory --- export const equipmentFactory = Factory.define(({ sequence, params }) => { const name = params.name ?? `Equipment ${sequence} - ${faker.commerce.productName()}` const index = createIndex(name) return { index, name, description: [faker.lorem.sentence()], equipment_categories: apiReferenceFactory.buildList( 1, {}, { transient: { resource: 'equipment-categories' } } ), cost: costFactory.build(), url: `/api/2024/equipment/${index}`, updated_at: faker.date.recent().toISOString(), // Optional fields - should be added by specific tests ammunition: undefined, weight: undefined, damage: undefined, image: undefined, mastery: undefined, notes: undefined, properties: undefined, quantity: undefined, range: undefined, throw_range: undefined, two_handed_damage: undefined } }) // --- Specific Equipment Type Builders --- export const weaponFactory = equipmentFactory.params({ damage: damageFactory.build(), range: rangeFactory.build(), properties: apiReferenceFactory.buildList( 2, {}, { transient: { resource: 'weapon-properties' } } ), weight: faker.number.int({ min: 1, max: 20 }) }) export const armorFactory = equipmentFactory.params({ weight: faker.number.int({ min: 10, max: 65 }), properties: apiReferenceFactory.buildList(1, {}, { transient: { resource: 'armor-properties' } }) }) export const thrownWeaponFactory = weaponFactory.params({ throw_range: throwRangeFactory.build() }) ================================================ FILE: src/tests/factories/2024/equipmentCategory.factory.ts ================================================ import { faker } from '@faker-js/faker' import { Factory } from 'fishery' import { EquipmentCategory2024 } from '@/models/2024/equipmentCategory' import { apiReferenceFactory } from './common.factory' // Import common factory export const equipmentCategoryFactory = Factory.define(({ sequence }) => { const name = `Equipment Category ${sequence} - ${faker.lorem.words(2)}` const index = name .toLowerCase() .replace(/[^a-z0-9]+/g, '-') .replace(/^-|-$/g, '') return { index: index, name: name, equipment: apiReferenceFactory.buildList(faker.number.int({ min: 1, max: 5 })), // Build a list of equipment references url: `/api/equipment-categories/${index}`, updated_at: faker.date.recent().toISOString() } }) ================================================ FILE: src/tests/factories/2024/feat.factory.ts ================================================ import { faker } from '@faker-js/faker' import { Factory } from 'fishery' import { Feat2024, FeatPrerequisites2024 } from '@/models/2024/feat' const FEAT_TYPES = ['origin', 'general', 'fighting-style', 'epic-boon'] as const export const featPrerequisitesFactory = Factory.define(() => ({ minimum_level: faker.number.int({ min: 1, max: 20 }) })) export const featFactory = Factory.define(({ sequence }) => { const name = `Feat ${sequence} - ${faker.lorem.words(2)}` const index = name .toLowerCase() .replace(/[^a-z0-9]+/g, '-') .replace(/^-|-$/g, '') return { index, name, description: faker.lorem.paragraph(), type: faker.helpers.arrayElement(FEAT_TYPES), url: `/api/2024/feats/${index}`, updated_at: faker.date.recent().toISOString() } }) ================================================ FILE: src/tests/factories/2024/language.factory.ts ================================================ import { faker } from '@faker-js/faker' import { Factory } from 'fishery' import { Language2024 } from '@/models/2024/language' export const languageFactory = Factory.define(() => { const name = faker.lorem.words() const index = name.toLowerCase().replace(/\s+/g, '-') return { index, name, is_rare: faker.datatype.boolean(), note: faker.lorem.sentence(), url: `/api/languages/${index}`, updated_at: faker.date.recent().toISOString() } }) ================================================ FILE: src/tests/factories/2024/magicItem.factory.ts ================================================ import { faker } from '@faker-js/faker' import { Factory } from 'fishery' import { MagicItem2024, Rarity2024 } from '@/models/2024/magicItem' import { apiReferenceFactory, createIndex, createUrl } from './common.factory' const RARITY_NAMES = ['Common', 'Uncommon', 'Rare', 'Very Rare', 'Legendary'] as const export const rarity2024Factory = Factory.define(() => ({ name: faker.helpers.arrayElement(RARITY_NAMES) })) export const magicItemFactory = Factory.define(({ sequence, params }) => { const name = params.name ?? `Magic Item ${sequence}` const index = params.index ?? createIndex(name) return { index, name, desc: params.desc ?? faker.lorem.paragraph(), image: params.image ?? `/images/magic-items/${index}.png`, equipment_category: apiReferenceFactory.build( params.equipment_category ?? { url: createUrl('equipment-categories', 'wondrous-items') } ), attunement: params.attunement ?? false, variant: params.variant ?? false, variants: params.variants ?? [], rarity: rarity2024Factory.build(params.rarity), url: params.url ?? createUrl('magic-items', index), updated_at: params.updated_at ?? faker.date.recent().toISOString() } }) ================================================ FILE: src/tests/factories/2024/magicSchool.factory.ts ================================================ import { faker } from '@faker-js/faker' import { Factory } from 'fishery' import { MagicSchool2024 } from '@/models/2024/magicSchool' export const magicSchoolFactory = Factory.define(() => { const name = faker.lorem.words() const index = name.toLowerCase().replace(/\s+/g, '-') return { index, name, description: faker.lorem.paragraph(), url: `/api/magic-schools/${index}`, updated_at: faker.date.recent().toISOString() } }) ================================================ FILE: src/tests/factories/2024/proficiency.factory.ts ================================================ import { faker } from '@faker-js/faker' import { Factory } from 'fishery' import { Proficiency2024 } from '@/models/2024/proficiency' import { apiReferenceFactory } from './common.factory' export const proficiencyFactory = Factory.define(({ sequence }) => { const name = `Proficiency ${sequence} - ${faker.lorem.words(2)}` const index = name .toLowerCase() .replace(/[^a-z0-9]+/g, '-') .replace(/^-|-$/g, '') return { index, name, type: faker.helpers.arrayElement(['Skills', 'Tools']), backgrounds: [], classes: [], reference: apiReferenceFactory.build(), url: `/api/2024/proficiencies/${index}`, updated_at: faker.date.recent().toISOString() } }) ================================================ FILE: src/tests/factories/2024/skill.factory.ts ================================================ import { faker } from '@faker-js/faker' import { Factory } from 'fishery' import { Skill2024 } from '@/models/2024/skill' import { apiReferenceFactory } from './common.factory' // Import common factory export const skillFactory = Factory.define(({ sequence }) => { const name = `Skill ${sequence} - ${faker.lorem.words(2)}` const index = name .toLowerCase() .replace(/[^a-z0-9]+/g, '-') .replace(/^-|-$/g, '') return { index: index, name: name, description: faker.lorem.paragraph(), // Build a default ability score using the common factory ability_score: apiReferenceFactory.build({ index: faker.helpers.arrayElement(['str', 'dex', 'con', 'int', 'wis', 'cha']), name: faker.helpers.arrayElement(['STR', 'DEX', 'CON', 'INT', 'WIS', 'CHA']), url: faker.internet.url() }), url: `/api/skills/${index}`, updated_at: faker.date.recent().toISOString() } }) ================================================ FILE: src/tests/factories/2024/species.factory.ts ================================================ import { faker } from '@faker-js/faker' import { Factory } from 'fishery' import { Species2024 } from '@/models/2024/species' import { apiReferenceFactory, createIndex, createUrl } from './common.factory' export const speciesFactory = Factory.define(({ sequence }) => { const name = `Species ${sequence} - ${faker.lorem.words(2)}` const index = createIndex(name) return { index, name, url: createUrl('species', index), type: faker.helpers.arrayElement(['Humanoid', 'Construct', 'Fey']), size: faker.helpers.arrayElement(['Small', 'Medium']), size_options: undefined, speed: faker.helpers.arrayElement([25, 30, 35]), traits: apiReferenceFactory.buildList(faker.number.int({ min: 0, max: 4 }), {}, { transient: { resourceType: 'traits' } }), subspecies: apiReferenceFactory.buildList(faker.number.int({ min: 0, max: 3 }), {}, { transient: { resourceType: 'subspecies' } }), updated_at: faker.date.recent().toISOString() } }) ================================================ FILE: src/tests/factories/2024/subclass.factory.ts ================================================ import { faker } from '@faker-js/faker' import { Factory } from 'fishery' import { Subclass2024, SubclassFeature2024 } from '@/models/2024/subclass' export const subclassFeatureFactory = Factory.define(() => ({ name: faker.lorem.words(3), level: faker.number.int({ min: 1, max: 20 }), description: faker.lorem.paragraph() })) export const subclassFactory = Factory.define(({ sequence }) => { const name = `Subclass ${sequence} - ${faker.lorem.words(2)}` const index = name .toLowerCase() .replace(/[^a-z0-9]+/g, '-') .replace(/^-|-$/g, '') return { index, name, summary: faker.lorem.sentence(), description: faker.lorem.paragraph(), features: subclassFeatureFactory.buildList(2), url: `/api/2024/subclasses/${index}`, updated_at: faker.date.recent().toISOString() } }) ================================================ FILE: src/tests/factories/2024/subspecies.factory.ts ================================================ import { faker } from '@faker-js/faker' import { Factory } from 'fishery' import { Subspecies2024, SubspeciesTrait } from '@/models/2024/subspecies' import { apiReferenceFactory, createIndex, createUrl } from './common.factory' const subspeciesTraitFactory = Factory.define(({ sequence }) => { const name = `Subspecies Trait ${sequence} - ${faker.lorem.words(2)}` const index = createIndex(name) return { index, name, url: createUrl('traits', index), level: faker.helpers.arrayElement([1, 3, 5, 7]) } }) export const subspeciesFactory = Factory.define(({ sequence }) => { const name = `Subspecies ${sequence} - ${faker.lorem.words(2)}` const index = createIndex(name) const speciesIndex = `species-${faker.lorem.word()}` return { index, name, url: createUrl('subspecies', index), species: apiReferenceFactory.build({ index: speciesIndex, name: faker.lorem.words(2), url: createUrl('species', speciesIndex) }), traits: subspeciesTraitFactory.buildList(faker.number.int({ min: 1, max: 3 })), damage_type: undefined, updated_at: faker.date.recent().toISOString() } }) ================================================ FILE: src/tests/factories/2024/trait.factory.ts ================================================ import { faker } from '@faker-js/faker' import { Factory } from 'fishery' import { Trait2024 } from '@/models/2024/trait' import { apiReferenceFactory, createIndex, createUrl } from './common.factory' export const traitFactory = Factory.define(({ sequence }) => { const name = `Trait ${sequence} - ${faker.lorem.words(2)}` const index = createIndex(name) return { index, name, url: createUrl('traits', index), description: faker.lorem.paragraph(), species: [apiReferenceFactory.build({}, { transient: { resourceType: 'species' } })], subspecies: [], proficiency_choices: undefined, speed: undefined, updated_at: faker.date.recent().toISOString() } }) ================================================ FILE: src/tests/factories/2024/weaponMasteryProperty.factory.ts ================================================ import { faker } from '@faker-js/faker' import { Factory } from 'fishery' import { WeaponMasteryProperty2024 } from '@/models/2024/weaponMasteryProperty' export const weaponMasteryPropertyFactory = Factory.define(() => { const name = faker.lorem.words() const index = name.toLowerCase().replace(/\s+/g, '-') return { index, name, description: faker.lorem.paragraph(), url: `/api/weapon-mastery-properties/${index}`, updated_at: faker.date.recent().toISOString() } }) ================================================ FILE: src/tests/factories/2024/weaponProperty.factory.ts ================================================ import { faker } from '@faker-js/faker' import { Factory } from 'fishery' import { WeaponProperty2024 } from '@/models/2024/weaponProperty' export const weaponPropertyFactory = Factory.define(() => { const name = faker.lorem.words() const index = name.toLowerCase().replace(/\s+/g, '-') return { index, name, description: faker.lorem.paragraph(), url: `/api/weapon-properties/${index}`, updated_at: faker.date.recent().toISOString() } }) ================================================ FILE: src/tests/integration/api/2014/abilityScores.itest.ts ================================================ import { Application } from 'express' import mongoose from 'mongoose' import request from 'supertest' import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from 'vitest' import createApp from '@/server' import { mongodbUri, redisClient } from '@/util' let app: Application let server: any afterEach(() => { vi.clearAllMocks() }) beforeAll(async () => { await mongoose.connect(mongodbUri) await redisClient.connect() app = await createApp() server = app.listen() // Start the server and store the instance }) afterAll(async () => { await mongoose.disconnect() await redisClient.quit() server.close() }) describe('/api/2014/ability-scores', () => { it('should list ability scores', async () => { const res = await request(app).get('/api/2014/ability-scores') expect(res.statusCode).toEqual(200) expect(res.body.results.length).not.toEqual(0) }) describe('with name query', () => { it('returns the named object', async () => { const indexRes = await request(app).get('/api/2014/ability-scores') const name = indexRes.body.results[1].name const res = await request(app).get(`/api/2014/ability-scores?name=${name}`) expect(res.statusCode).toEqual(200) expect(res.body.results[0].name).toEqual(name) }) it('is case insensitive', async () => { const indexRes = await request(app).get('/api/2014/ability-scores') const name = indexRes.body.results[1].name const queryName = name.toLowerCase() const res = await request(app).get(`/api/2014/ability-scores?name=${queryName}`) expect(res.statusCode).toEqual(200) expect(res.body.results[0].name).toEqual(name) }) }) describe('/api/2014/ability-scores/:index', () => { it('should return one object', async () => { const indexRes = await request(app).get('/api/2014/ability-scores') const index = indexRes.body.results[0].index const showRes = await request(app).get(`/api/2014/ability-scores/${index}`) expect(showRes.statusCode).toEqual(200) expect(showRes.body.index).toEqual(index) }) describe('with an invalid index', () => { it('should return 404', async () => { const invalidIndex = 'invalid-index' const showRes = await request(app).get(`/api/2014/ability-scores/${invalidIndex}`) expect(showRes.statusCode).toEqual(404) }) }) }) }) ================================================ FILE: src/tests/integration/api/2014/classes.itest.ts ================================================ import { Application } from 'express' import mongoose from 'mongoose' import request from 'supertest' import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from 'vitest' import createApp from '@/server' import { mongodbUri, redisClient } from '@/util' let app: Application let server: any afterEach(() => { vi.clearAllMocks() }) beforeAll(async () => { await mongoose.connect(mongodbUri) await redisClient.connect() app = await createApp() server = app.listen() // Start the server and store the instance }) afterAll(async () => { await mongoose.disconnect() await redisClient.quit() server.close() }) describe('/api/2014/classes', () => { it('should list classes', async () => { const res = await request(app).get('/api/2014/classes') expect(res.statusCode).toEqual(200) expect(res.body.results.length).not.toEqual(0) }) describe('with name query', () => { it('returns the named object', async () => { const indexRes = await request(app).get('/api/2014/classes') const name = indexRes.body.results[1].name const res = await request(app).get(`/api/2014/classes?name=${name}`) expect(res.statusCode).toEqual(200) expect(res.body.results[0].name).toEqual(name) }) it('is case insensitive', async () => { const indexRes = await request(app).get('/api/2014/classes') const name = indexRes.body.results[1].name const queryName = name.toLowerCase() const res = await request(app).get(`/api/2014/classes?name=${queryName}`) expect(res.statusCode).toEqual(200) expect(res.body.results[0].name).toEqual(name) }) }) describe('/api/2014/classes/:index', () => { it('should return one object', async () => { const indexRes = await request(app).get('/api/2014/classes') const index = indexRes.body.results[0].index const showRes = await request(app).get(`/api/2014/classes/${index}`) expect(showRes.statusCode).toEqual(200) expect(showRes.body.index).toEqual(index) }) describe('with an invalid index', () => { it('should return 404', async () => { const invalidIndex = 'invalid-index' const showRes = await request(app).get(`/api/2014/classes/${invalidIndex}`) expect(showRes.statusCode).toEqual(404) }) }) describe('/api/2014/classes/:index/subclasses', () => { it('returns objects', async () => { const indexRes = await request(app).get('/api/2014/classes') const index = indexRes.body.results[1].index const res = await request(app).get(`/api/2014/classes/${index}/subclasses`) expect(res.statusCode).toEqual(200) expect(res.body.results.length).not.toEqual(0) }) }) describe('/api/2014/classes/:index/starting-equipment', () => { it('returns objects', async () => { const indexRes = await request(app).get('/api/2014/classes') const index = indexRes.body.results[1].index const res = await request(app).get(`/api/2014/classes/${index}/starting-equipment`) expect(res.statusCode).toEqual(200) }) }) describe('/api/2014/classes/:index/spellcasting', () => { it('returns objects', async () => { const indexRes = await request(app).get('/api/2014/classes') const index = indexRes.body.results[1].index const res = await request(app).get(`/api/2014/classes/${index}/spellcasting`) expect(res.statusCode).toEqual(200) }) }) describe('/api/2014/classes/:index/spells', () => { it('returns objects', async () => { const res = await request(app).get('/api/2014/classes/wizard/spells') expect(res.statusCode).toEqual(200) expect(res.body.results.length).not.toEqual(0) }) }) describe('/api/2014/classes/:index/features', () => { it('returns objects', async () => { const indexRes = await request(app).get('/api/2014/classes') const index = indexRes.body.results[1].index const res = await request(app).get(`/api/2014/classes/${index}/features`) expect(res.statusCode).toEqual(200) expect(res.body.results.length).not.toEqual(0) }) }) describe('/api/2014/classes/:index/proficiencies', () => { it('returns objects', async () => { const indexRes = await request(app).get('/api/2014/classes') const index = indexRes.body.results[1].index const res = await request(app).get(`/api/2014/classes/${index}/proficiencies`) expect(res.statusCode).toEqual(200) expect(res.body.results.length).not.toEqual(0) }) }) describe('/api/2014/classes/:index/multi-classing', () => { it('returns objects', async () => { const indexRes = await request(app).get('/api/2014/classes') const index = indexRes.body.results[1].index const res = await request(app).get(`/api/2014/classes/${index}/multi-classing`) expect(res.statusCode).toEqual(200) }) }) describe('/api/2014/classes/:index/levels', () => { it('returns objects', async () => { const indexRes = await request(app).get('/api/2014/classes') const index = indexRes.body.results[1].index const res = await request(app).get(`/api/2014/classes/${index}/levels`) expect(res.statusCode).toEqual(200) expect(res.body.length).not.toEqual(0) expect(res.body.length).toEqual(20) }) it('returns the subclass levels as well', async () => { const indexRes = await request(app).get('/api/2014/classes') const index = indexRes.body.results[1].index const classRes = await request(app).get(`/api/2014/classes/${index}`) const subclass = classRes.body.subclasses[0].index const res = await request(app).get(`/api/2014/classes/${index}/levels?subclass=${subclass}`) expect(res.statusCode).toEqual(200) expect(res.body.length).not.toEqual(0) expect(res.body.length).toBeGreaterThan(20) }) describe('/api/2014/classes/:index/levels/:level', () => { it('returns objects', async () => { const indexRes = await request(app).get('/api/2014/classes') const index = indexRes.body.results[1].index const level = 1 const res = await request(app).get(`/api/2014/classes/${index}/levels/${level}`) expect(res.statusCode).toEqual(200) expect(res.body.level).toEqual(level) }) }) describe('/api/2014/classes/:index/levels/:level/spells', () => { it('returns objects', async () => { const index = 'wizard' const level = 1 const res = await request(app).get(`/api/2014/classes/${index}/levels/${level}/spells`) expect(res.statusCode).toEqual(200) expect(res.body.results.length).not.toEqual(0) }) }) describe('/api/2014/classes/:index/levels/:level/features', () => { it('returns objects', async () => { const indexRes = await request(app).get('/api/2014/classes') const index = indexRes.body.results[1].index const level = 1 const res = await request(app).get(`/api/2014/classes/${index}/levels/${level}/spells`) expect(res.statusCode).toEqual(200) expect(res.body.results.length).not.toEqual(0) }) }) }) }) }) ================================================ FILE: src/tests/integration/api/2014/conditions.itest.ts ================================================ import { Application } from 'express' import mongoose from 'mongoose' import request from 'supertest' import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from 'vitest' import createApp from '@/server' import { mongodbUri, redisClient } from '@/util' let app: Application let server: any afterEach(() => { vi.clearAllMocks() }) beforeAll(async () => { await mongoose.connect(mongodbUri) await redisClient.connect() app = await createApp() server = app.listen() // Start the server and store the instance }) afterAll(async () => { await mongoose.disconnect() await redisClient.quit() server.close() }) describe('/api/2014/conditions', () => { it('should list conditions', async () => { const res = await request(app).get('/api/2014/conditions') expect(res.statusCode).toEqual(200) expect(res.body.results.length).not.toEqual(0) }) describe('with name query', () => { it('returns the named object', async () => { const indexRes = await request(app).get('/api/2014/conditions') const name = indexRes.body.results[1].name const res = await request(app).get(`/api/2014/conditions?name=${name}`) expect(res.statusCode).toEqual(200) expect(res.body.results[0].name).toEqual(name) }) it('is case insensitive', async () => { const indexRes = await request(app).get('/api/2014/conditions') const name = indexRes.body.results[1].name const queryName = name.toLowerCase() const res = await request(app).get(`/api/2014/conditions?name=${queryName}`) expect(res.statusCode).toEqual(200) expect(res.body.results[0].name).toEqual(name) }) }) describe('/api/2014/conditions/:index', () => { it('should return one object', async () => { const indexRes = await request(app).get('/api/2014/conditions') const index = indexRes.body.results[0].index const showRes = await request(app).get(`/api/2014/conditions/${index}`) expect(showRes.statusCode).toEqual(200) expect(showRes.body.index).toEqual(index) }) describe('with an invalid index', () => { it('should return 404', async () => { const invalidIndex = 'invalid-index' const showRes = await request(app).get(`/api/2014/conditions/${invalidIndex}`) expect(showRes.statusCode).toEqual(404) }) }) }) }) ================================================ FILE: src/tests/integration/api/2014/damageTypes.itest.ts ================================================ import { Application } from 'express' import mongoose from 'mongoose' import request from 'supertest' import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from 'vitest' import createApp from '@/server' import { mongodbUri, redisClient } from '@/util' let app: Application let server: any afterEach(() => { vi.clearAllMocks() }) beforeAll(async () => { await mongoose.connect(mongodbUri) await redisClient.connect() app = await createApp() server = app.listen() // Start the server and store the instance }) afterAll(async () => { await mongoose.disconnect() await redisClient.quit() server.close() }) describe('/api/2014/damage-types', () => { it('should list damage types', async () => { const res = await request(app).get('/api/2014/damage-types') expect(res.statusCode).toEqual(200) expect(res.body.results.length).not.toEqual(0) }) describe('with name query', () => { it('returns the named object', async () => { const indexRes = await request(app).get('/api/2014/damage-types') const name = indexRes.body.results[1].name const res = await request(app).get(`/api/2014/damage-types?name=${name}`) expect(res.statusCode).toEqual(200) expect(res.body.results[0].name).toEqual(name) }) it('is case insensitive', async () => { const indexRes = await request(app).get('/api/2014/damage-types') const name = indexRes.body.results[1].name const queryName = name.toLowerCase() const res = await request(app).get(`/api/2014/damage-types?name=${queryName}`) expect(res.statusCode).toEqual(200) expect(res.body.results[0].name).toEqual(name) }) }) describe('/api/2014/damage-types/:index', () => { it('should return one object', async () => { const indexRes = await request(app).get('/api/2014/damage-types') const index = indexRes.body.results[0].index const showRes = await request(app).get(`/api/2014/damage-types/${index}`) expect(showRes.statusCode).toEqual(200) expect(showRes.body.index).toEqual(index) }) describe('with an invalid index', () => { it('should return 404', async () => { const invalidIndex = 'invalid-index' const showRes = await request(app).get(`/api/2014/damage-types/${invalidIndex}`) expect(showRes.statusCode).toEqual(404) }) }) }) }) ================================================ FILE: src/tests/integration/api/2014/equipment.itest.ts ================================================ import { Application } from 'express' import mongoose from 'mongoose' import request from 'supertest' import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from 'vitest' import createApp from '@/server' import { mongodbUri, redisClient } from '@/util' let app: Application let server: any afterEach(() => { vi.clearAllMocks() }) beforeAll(async () => { await mongoose.connect(mongodbUri) await redisClient.connect() app = await createApp() server = app.listen() // Start the server and store the instance }) afterAll(async () => { await mongoose.disconnect() await redisClient.quit() server.close() }) describe('/api/2014/equipment', () => { it('should list equipment', async () => { const res = await request(app).get('/api/2014/equipment') expect(res.statusCode).toEqual(200) expect(res.body.results.length).not.toEqual(0) }) describe('with name query', () => { it('returns the named object', async () => { const indexRes = await request(app).get('/api/2014/equipment') const name = indexRes.body.results[1].name const res = await request(app).get(`/api/2014/equipment?name=${name}`) expect(res.statusCode).toEqual(200) expect(res.body.results[0].name).toEqual(name) }) it('is case insensitive', async () => { const indexRes = await request(app).get('/api/2014/equipment') const name = indexRes.body.results[1].name const queryName = name.toLowerCase() const res = await request(app).get(`/api/2014/equipment?name=${queryName}`) expect(res.statusCode).toEqual(200) expect(res.body.results[0].name).toEqual(name) }) }) describe('/api/2014/equipment/:index', () => { it('should return one object', async () => { const indexRes = await request(app).get('/api/2014/equipment') const index = indexRes.body.results[0].index const showRes = await request(app).get(`/api/2014/equipment/${index}`) expect(showRes.statusCode).toEqual(200) expect(showRes.body.index).toEqual(index) }) describe('with an invalid index', () => { it('should return 404', async () => { const invalidIndex = 'invalid-index' const showRes = await request(app).get(`/api/2014/equipment/${invalidIndex}`) expect(showRes.statusCode).toEqual(404) }) }) }) }) ================================================ FILE: src/tests/integration/api/2014/equipmentCategories.itest.ts ================================================ import { Application } from 'express' import mongoose from 'mongoose' import request from 'supertest' import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from 'vitest' import createApp from '@/server' import { mongodbUri, redisClient } from '@/util' let app: Application let server: any afterEach(() => { vi.clearAllMocks() }) beforeAll(async () => { await mongoose.connect(mongodbUri) await redisClient.connect() app = await createApp() server = app.listen() // Start the server and store the instance }) afterAll(async () => { await mongoose.disconnect() await redisClient.quit() server.close() }) describe('/api/2014/equipment-categories', () => { it('should list equipment categories', async () => { const res = await request(app).get('/api/2014/equipment-categories') expect(res.statusCode).toEqual(200) expect(res.body.results.length).not.toEqual(0) }) describe('with name query', () => { it('returns the named object', async () => { const indexRes = await request(app).get('/api/2014/equipment-categories') const name = indexRes.body.results[1].name const res = await request(app).get(`/api/2014/equipment-categories?name=${name}`) expect(res.statusCode).toEqual(200) expect(res.body.results[0].name).toEqual(name) }) it('is case insensitive', async () => { const indexRes = await request(app).get('/api/2014/equipment-categories') const name = indexRes.body.results[1].name const queryName = name.toLowerCase() const res = await request(app).get(`/api/2014/equipment-categories?name=${queryName}`) expect(res.statusCode).toEqual(200) expect(res.body.results[0].name).toEqual(name) }) }) describe('/api/2014/equipment-categories/:index', () => { it('should return one object', async () => { const indexRes = await request(app).get('/api/2014/equipment-categories') const index = indexRes.body.results[0].index const showRes = await request(app).get(`/api/2014/equipment-categories/${index}`) expect(showRes.statusCode).toEqual(200) expect(showRes.body.index).toEqual(index) }) describe('with an invalid index', () => { it('should return 404', async () => { const invalidIndex = 'invalid-index' const showRes = await request(app).get(`/api/2014/equipment-categories/${invalidIndex}`) expect(showRes.statusCode).toEqual(404) }) }) }) }) ================================================ FILE: src/tests/integration/api/2014/feats.itest.ts ================================================ import { Application } from 'express' import mongoose from 'mongoose' import request from 'supertest' import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from 'vitest' import createApp from '@/server' import { mongodbUri, redisClient } from '@/util' let app: Application let server: any afterEach(() => { vi.clearAllMocks() }) beforeAll(async () => { await mongoose.connect(mongodbUri) await redisClient.connect() app = await createApp() server = app.listen() // Start the server and store the instance }) afterAll(async () => { await mongoose.disconnect() await redisClient.quit() server.close() }) describe('/api/2014/feats', () => { it('should list feats', async () => { const res = await request(app).get('/api/2014/feats') expect(res.statusCode).toEqual(200) expect(res.body.results.length).not.toEqual(0) }) describe('with name query', () => { it('returns the named object', async () => { const indexRes = await request(app).get('/api/2014/feats') const name = indexRes.body.results[0].name const res = await request(app).get(`/api/2014/feats?name=${name}`) expect(res.statusCode).toEqual(200) expect(res.body.results[0].name).toEqual(name) }) it('is case insensitive', async () => { const indexRes = await request(app).get('/api/2014/feats') const name = indexRes.body.results[0].name const queryName = name.toLowerCase() const res = await request(app).get(`/api/2014/feats?name=${queryName}`) expect(res.statusCode).toEqual(200) expect(res.body.results[0].name).toEqual(name) }) }) describe('/api/2014/feats/:index', () => { it('should return one object', async () => { const indexRes = await request(app).get('/api/2014/feats') const index = indexRes.body.results[0].index const showRes = await request(app).get(`/api/2014/feats/${index}`) expect(showRes.statusCode).toEqual(200) expect(showRes.body.index).toEqual(index) }) describe('with an invalid index', () => { it('should return 404', async () => { const invalidIndex = 'invalid-index' const showRes = await request(app).get(`/api/2014/feats/${invalidIndex}`) expect(showRes.statusCode).toEqual(404) }) }) }) }) ================================================ FILE: src/tests/integration/api/2014/features.itest.ts ================================================ import { Application } from 'express' import mongoose from 'mongoose' import request from 'supertest' import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from 'vitest' import createApp from '@/server' import { mongodbUri, redisClient } from '@/util' let app: Application let server: any afterEach(() => { vi.clearAllMocks() }) beforeAll(async () => { await mongoose.connect(mongodbUri) await redisClient.connect() app = await createApp() server = app.listen() // Start the server and store the instance }) afterAll(async () => { await mongoose.disconnect() await redisClient.quit() server.close() }) describe('/api/2014/features', () => { it('should list features', async () => { const res = await request(app).get('/api/2014/features') expect(res.statusCode).toEqual(200) expect(res.body.results.length).not.toEqual(0) }) describe('with name query', () => { it('returns the named object', async () => { const indexRes = await request(app).get('/api/2014/features') const name = indexRes.body.results[1].name const res = await request(app).get(`/api/2014/features?name=${name}`) expect(res.statusCode).toEqual(200) expect(res.body.results[0].name).toEqual(name) }) it('is case insensitive', async () => { const indexRes = await request(app).get('/api/2014/features') const name = indexRes.body.results[1].name const queryName = name.toLowerCase() const res = await request(app).get(`/api/2014/features?name=${queryName}`) expect(res.statusCode).toEqual(200) expect(res.body.results[0].name).toEqual(name) }) }) describe('/api/2014/features/:index', () => { it('should return one object', async () => { const indexRes = await request(app).get('/api/2014/features') const index = indexRes.body.results[0].index const showRes = await request(app).get(`/api/2014/features/${index}`) expect(showRes.statusCode).toEqual(200) expect(showRes.body.index).toEqual(index) }) describe('with an invalid index', () => { it('should return 404', async () => { const invalidIndex = 'invalid-index' const showRes = await request(app).get(`/api/2014/features/${invalidIndex}`) expect(showRes.statusCode).toEqual(404) }) }) }) }) ================================================ FILE: src/tests/integration/api/2014/languages.itest.ts ================================================ import { Application } from 'express' import mongoose from 'mongoose' import request from 'supertest' import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from 'vitest' import createApp from '@/server' import { mongodbUri, redisClient } from '@/util' let app: Application let server: any afterEach(() => { vi.clearAllMocks() }) beforeAll(async () => { await mongoose.connect(mongodbUri) await redisClient.connect() app = await createApp() server = app.listen() // Start the server and store the instance }) afterAll(async () => { await mongoose.disconnect() await redisClient.quit() server.close() }) describe('/api/2014/languages', () => { it('should list languages', async () => { const res = await request(app).get('/api/2014/languages') expect(res.statusCode).toEqual(200) expect(res.body.results.length).not.toEqual(0) }) describe('with name query', () => { it('returns the named object', async () => { const indexRes = await request(app).get('/api/2014/languages') const name = indexRes.body.results[1].name const res = await request(app).get(`/api/2014/languages?name=${name}`) expect(res.statusCode).toEqual(200) expect(res.body.results[0].name).toEqual(name) }) it('is case insensitive', async () => { const indexRes = await request(app).get('/api/2014/languages') const name = indexRes.body.results[1].name const queryName = name.toLowerCase() const res = await request(app).get(`/api/2014/languages?name=${queryName}`) expect(res.statusCode).toEqual(200) expect(res.body.results[0].name).toEqual(name) }) }) describe('/api/2014/languages/:index', () => { it('should return one object', async () => { const indexRes = await request(app).get('/api/2014/languages') const index = indexRes.body.results[0].index const showRes = await request(app).get(`/api/2014/languages/${index}`) expect(showRes.statusCode).toEqual(200) expect(showRes.body.index).toEqual(index) }) describe('with an invalid index', () => { it('should return 404', async () => { const invalidIndex = 'invalid-index' const showRes = await request(app).get(`/api/2014/languages/${invalidIndex}`) expect(showRes.statusCode).toEqual(404) }) }) }) }) ================================================ FILE: src/tests/integration/api/2014/magicItems.itest.ts ================================================ import { Application } from 'express' import mongoose from 'mongoose' import request from 'supertest' import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from 'vitest' import createApp from '@/server' import { mongodbUri, redisClient } from '@/util' let app: Application let server: any afterEach(() => { vi.clearAllMocks() }) beforeAll(async () => { await mongoose.connect(mongodbUri) await redisClient.connect() app = await createApp() server = app.listen() // Start the server and store the instance }) afterAll(async () => { await mongoose.disconnect() await redisClient.quit() server.close() }) describe('/api/2014/magic-items', () => { it('should list magic items', async () => { const res = await request(app).get('/api/2014/magic-items') expect(res.statusCode).toEqual(200) expect(res.body.results.length).not.toEqual(0) }) it('should hit the cache', async () => { await redisClient.del('/api/2014/magic-items') const clientSet = vi.spyOn(redisClient, 'set') await request(app).get('/api/2014/magic-items') const res = await request(app).get('/api/2014/magic-items') expect(res.statusCode).toEqual(200) expect(res.body.results.length).not.toEqual(0) expect(clientSet).toHaveBeenCalledTimes(1) }) describe('with name query', () => { it('returns the named object', async () => { const indexRes = await request(app).get('/api/2014/magic-items') const name = indexRes.body.results[5].name const res = await request(app).get(`/api/2014/magic-items?name=${name}`) expect(res.statusCode).toEqual(200) expect(res.body.results[0].name).toEqual(name) }) it('is case insensitive', async () => { const indexRes = await request(app).get('/api/2014/magic-items') const name = indexRes.body.results[5].name const queryName = name.toLowerCase() const res = await request(app).get(`/api/2014/magic-items?name=${queryName}`) expect(res.statusCode).toEqual(200) expect(res.body.results[0].name).toEqual(name) }) }) describe('/api/2014/magic-items/:index', () => { it('should return one object', async () => { const indexRes = await request(app).get('/api/2014/magic-items') const index = indexRes.body.results[0].index const showRes = await request(app).get(`/api/2014/magic-items/${index}`) expect(showRes.statusCode).toEqual(200) expect(showRes.body.index).toEqual(index) }) describe('with an invalid index', () => { it('should return 404', async () => { const invalidIndex = 'invalid-index' const showRes = await request(app).get(`/api/2014/magic-items/${invalidIndex}`) expect(showRes.statusCode).toEqual(404) }) }) }) }) ================================================ FILE: src/tests/integration/api/2014/magicSchools.itest.ts ================================================ import { Application } from 'express' import mongoose from 'mongoose' import request from 'supertest' import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from 'vitest' import createApp from '@/server' import { mongodbUri, redisClient } from '@/util' let app: Application let server: any afterEach(() => { vi.clearAllMocks() }) beforeAll(async () => { await mongoose.connect(mongodbUri) await redisClient.connect() app = await createApp() server = app.listen() // Start the server and store the instance }) afterAll(async () => { await mongoose.disconnect() await redisClient.quit() server.close() }) describe('/api/2014/magic-schools', () => { it('should list magic items', async () => { const res = await request(app).get('/api/2014/magic-schools') expect(res.statusCode).toEqual(200) expect(res.body.results.length).not.toEqual(0) }) describe('with name query', () => { it('returns the named object', async () => { const indexRes = await request(app).get('/api/2014/magic-schools') const name = indexRes.body.results[1].name const res = await request(app).get(`/api/2014/magic-schools?name=${name}`) expect(res.statusCode).toEqual(200) expect(res.body.results[0].name).toEqual(name) }) it('is case insensitive', async () => { const indexRes = await request(app).get('/api/2014/magic-schools') const name = indexRes.body.results[1].name const queryName = name.toLowerCase() const res = await request(app).get(`/api/2014/magic-schools?name=${queryName}`) expect(res.statusCode).toEqual(200) expect(res.body.results[0].name).toEqual(name) }) }) describe('/api/2014/magic-schools/:index', () => { it('should return one object', async () => { const indexRes = await request(app).get('/api/2014/magic-schools') const index = indexRes.body.results[0].index const showRes = await request(app).get(`/api/2014/magic-schools/${index}`) expect(showRes.statusCode).toEqual(200) expect(showRes.body.index).toEqual(index) }) describe('with an invalid index', () => { it('should return 404', async () => { const invalidIndex = 'invalid-index' const showRes = await request(app).get(`/api/2014/magic-schools/${invalidIndex}`) expect(showRes.statusCode).toEqual(404) }) }) }) }) ================================================ FILE: src/tests/integration/api/2014/monsters.itest.ts ================================================ import { Application } from 'express' import mongoose from 'mongoose' import request from 'supertest' import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from 'vitest' import createApp from '@/server' import { mongodbUri, redisClient } from '@/util' let app: Application let server: any afterEach(() => { vi.clearAllMocks() }) beforeAll(async () => { await mongoose.connect(mongodbUri) await redisClient.connect() app = await createApp() server = app.listen() // Start the server and store the instance }) afterAll(async () => { await mongoose.disconnect() await redisClient.quit() server.close() }) describe('/api/2014/monsters', () => { it('should list monsters', async () => { const res = await request(app).get('/api/2014/monsters') expect(res.statusCode).toEqual(200) expect(res.body.results.length).not.toEqual(0) }) it('should hit the cache', async () => { await redisClient.del('/api/2014/monsters') const clientSet = vi.spyOn(redisClient, 'set') await request(app).get('/api/2014/monsters') const res = await request(app).get('/api/2014/monsters') expect(res.statusCode).toEqual(200) expect(res.body.results.length).not.toEqual(0) expect(clientSet).toHaveBeenCalledTimes(1) }) describe('with name query', () => { it('returns the named object', async () => { const indexRes = await request(app).get('/api/2014/monsters') const name = indexRes.body.results[1].name const res = await request(app).get(`/api/2014/monsters?name=${name}`) expect(res.statusCode).toEqual(200) expect(res.body.results[0].name).toEqual(name) }) it('is case insensitive', async () => { const indexRes = await request(app).get('/api/2014/monsters') const name = indexRes.body.results[1].name const queryName = name.toLowerCase() const res = await request(app).get(`/api/2014/monsters?name=${queryName}`) expect(res.statusCode).toEqual(200) expect(res.body.results[0].name).toEqual(name) }) }) describe('with challenge_rating query', () => { describe('with only one provided challenge rating', () => { it('returns expected objects', async () => { const expectedCR = 0.25 const res = await request(app).get(`/api/2014/monsters?challenge_rating=${expectedCR}`) expect(res.statusCode).toEqual(200) const randomIndex = Math.floor(Math.random() * res.body.results.length) const randomResult = res.body.results[randomIndex] const indexRes = await request(app).get(`/api/2014/monsters/${randomResult.index}`) expect(indexRes.statusCode).toEqual(200) expect(indexRes.body.challenge_rating).toEqual(expectedCR) }) }) describe('with many provided challenge ratings', () => { it('returns expected objects', async () => { const cr1 = 1 const cr1Res = await request(app).get(`/api/2014/monsters?challenge_rating=${cr1}`) expect(cr1Res.statusCode).toEqual(200) const cr20 = 20 const cr20Res = await request(app).get(`/api/2014/monsters?challenge_rating=${cr20}`) expect(cr20Res.statusCode).toEqual(200) const bothRes = await request(app).get( `/api/2014/monsters?challenge_rating=${cr1}&challenge_rating=${cr20}` ) expect(bothRes.statusCode).toEqual(200) expect(bothRes.body.count).toEqual(cr1Res.body.count + cr20Res.body.count) const altBothRes = await request(app).get( `/api/2014/monsters?challenge_rating=${cr1},${cr20}` ) expect(altBothRes.statusCode).toEqual(200) expect(altBothRes.body.count).toEqual(cr1Res.body.count + cr20Res.body.count) const randomIndex = Math.floor(Math.random() * bothRes.body.results.length) const randomResult = bothRes.body.results[randomIndex] const indexRes = await request(app).get(`/api/2014/monsters/${randomResult.index}`) expect(indexRes.statusCode).toEqual(200) expect( indexRes.body.challenge_rating == cr1 || indexRes.body.challenge_rating == cr20 ).toBeTruthy() }) }) }) describe('/api/2014/monsters/:index', () => { it('should return one object', async () => { const indexRes = await request(app).get('/api/2014/monsters') const index = indexRes.body.results[0].index const showRes = await request(app).get(`/api/2014/monsters/${index}`) expect(showRes.statusCode).toEqual(200) expect(showRes.body.index).toEqual(index) }) describe('with an invalid index', () => { it('should return 404', async () => { const invalidIndex = 'invalid-index' const showRes = await request(app).get(`/api/2014/monsters/${invalidIndex}`) expect(showRes.statusCode).toEqual(404) }) }) }) }) ================================================ FILE: src/tests/integration/api/2014/proficiencies.itest.ts ================================================ import { Application } from 'express' import mongoose from 'mongoose' import request from 'supertest' import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from 'vitest' import createApp from '@/server' import { mongodbUri, redisClient } from '@/util' let app: Application let server: any afterEach(() => { vi.clearAllMocks() }) beforeAll(async () => { await mongoose.connect(mongodbUri) await redisClient.connect() app = await createApp() server = app.listen() // Start the server and store the instance }) afterAll(async () => { await mongoose.disconnect() await redisClient.quit() server.close() }) describe('/api/2014/proficiencies', () => { it('should list proficiencies', async () => { const res = await request(app).get('/api/2014/proficiencies') expect(res.statusCode).toEqual(200) expect(res.body.results.length).not.toEqual(0) }) describe('with name query', () => { it('returns the named object', async () => { const indexRes = await request(app).get('/api/2014/proficiencies') const name = indexRes.body.results[1].name const res = await request(app).get(`/api/2014/proficiencies?name=${name}`) expect(res.statusCode).toEqual(200) expect(res.body.results[0].name).toEqual(name) }) it('is case insensitive', async () => { const indexRes = await request(app).get('/api/2014/proficiencies') const name = indexRes.body.results[1].name const queryName = name.toLowerCase() const res = await request(app).get(`/api/2014/proficiencies?name=${queryName}`) expect(res.statusCode).toEqual(200) expect(res.body.results[0].name).toEqual(name) }) }) describe('/api/2014/proficiencies/:index', () => { it('should return one object', async () => { const indexRes = await request(app).get('/api/2014/proficiencies') const index = indexRes.body.results[0].index const showRes = await request(app).get(`/api/2014/proficiencies/${index}`) expect(showRes.statusCode).toEqual(200) expect(showRes.body.index).toEqual(index) }) describe('with an invalid index', () => { it('should return 404', async () => { const invalidIndex = 'invalid-index' const showRes = await request(app).get(`/api/2014/proficiencies/${invalidIndex}`) expect(showRes.statusCode).toEqual(404) }) }) }) }) ================================================ FILE: src/tests/integration/api/2014/races.itest.ts ================================================ import { Application } from 'express' import mongoose from 'mongoose' import request from 'supertest' import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from 'vitest' import createApp from '@/server' import { mongodbUri, redisClient } from '@/util' let app: Application let server: any afterEach(() => { vi.clearAllMocks() }) beforeAll(async () => { await mongoose.connect(mongodbUri) await redisClient.connect() app = await createApp() server = app.listen() // Start the server and store the instance }) afterAll(async () => { await mongoose.disconnect() await redisClient.quit() server.close() }) describe('/api/2014/races', () => { it('should list races', async () => { const res = await request(app).get('/api/2014/races') expect(res.statusCode).toEqual(200) expect(res.body.results.length).not.toEqual(0) }) describe('with name query', () => { it('returns the named object', async () => { const indexRes = await request(app).get('/api/2014/races') const name = indexRes.body.results[1].name const res = await request(app).get(`/api/2014/races?name=${name}`) expect(res.statusCode).toEqual(200) expect(res.body.results[0].name).toEqual(name) }) it('is case insensitive', async () => { const indexRes = await request(app).get('/api/2014/races') const name = indexRes.body.results[1].name const queryName = name.toLowerCase() const res = await request(app).get(`/api/2014/races?name=${queryName}`) expect(res.statusCode).toEqual(200) expect(res.body.results[0].name).toEqual(name) }) }) describe('/api/2014/races/:index', () => { it('should return one object', async () => { const indexRes = await request(app).get('/api/2014/races') const index = indexRes.body.results[0].index const showRes = await request(app).get(`/api/2014/races/${index}`) expect(showRes.statusCode).toEqual(200) expect(showRes.body.index).toEqual(index) }) describe('with an invalid index', () => { it('should return 404', async () => { const invalidIndex = 'invalid-index' const showRes = await request(app).get(`/api/2014/races/${invalidIndex}`) expect(showRes.statusCode).toEqual(404) }) }) describe('/api/2014/races/:index/subraces', () => { it('returns objects', async () => { const indexRes = await request(app).get('/api/2014/races') const index = indexRes.body.results[1].index const res = await request(app).get(`/api/2014/races/${index}/subraces`) expect(res.statusCode).toEqual(200) expect(res.body.results.length).not.toEqual(0) }) }) describe('/api/2014/races/:index/proficiencies', () => { it('returns objects', async () => { const indexRes = await request(app).get('/api/2014/races') const index = indexRes.body.results[1].index const res = await request(app).get(`/api/2014/races/${index}/proficiencies`) expect(res.statusCode).toEqual(200) expect(res.body.results.length).not.toEqual(0) }) }) describe('/api/2014/races/:index/traits', () => { it('returns objects', async () => { const indexRes = await request(app).get('/api/2014/races') const index = indexRes.body.results[1].index const res = await request(app).get(`/api/2014/races/${index}/traits`) expect(res.statusCode).toEqual(200) expect(res.body.results.length).not.toEqual(0) }) }) }) }) ================================================ FILE: src/tests/integration/api/2014/ruleSections.itest.ts ================================================ import { Application } from 'express' import mongoose from 'mongoose' import request from 'supertest' import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from 'vitest' import createApp from '@/server' import { mongodbUri, redisClient } from '@/util' let app: Application let server: any afterEach(() => { vi.clearAllMocks() }) beforeAll(async () => { await mongoose.connect(mongodbUri) await redisClient.connect() app = await createApp() server = app.listen() // Start the server and store the instance }) afterAll(async () => { await mongoose.disconnect() await redisClient.quit() server.close() }) describe('/api/2014/rule-sections', () => { it('should list rule sections', async () => { const res = await request(app).get('/api/2014/rule-sections') expect(res.statusCode).toEqual(200) expect(res.body.results.length).not.toEqual(0) }) it('should hit the cache', async () => { await redisClient.del('/api/2014/rule-sections') const clientSet = vi.spyOn(redisClient, 'set') await request(app).get('/api/2014/rule-sections') const res = await request(app).get('/api/2014/rule-sections') expect(res.statusCode).toEqual(200) expect(res.body.results.length).not.toEqual(0) expect(clientSet).toHaveBeenCalledTimes(1) }) describe('with name query', () => { it('returns the named object', async () => { const indexRes = await request(app).get('/api/2014/rule-sections') const name = indexRes.body.results[1].name const res = await request(app).get(`/api/2014/rule-sections?name=${name}`) expect(res.statusCode).toEqual(200) expect(res.body.results[0].name).toEqual(name) }) it('is case insensitive', async () => { const indexRes = await request(app).get('/api/2014/rule-sections') const name = indexRes.body.results[1].name const queryName = name.toLowerCase() const res = await request(app).get(`/api/2014/rule-sections?name=${queryName}`) expect(res.statusCode).toEqual(200) expect(res.body.results[0].name).toEqual(name) }) }) describe('with desc query', () => { it('returns the object with matching desc', async () => { const indexRes = await request(app).get('/api/2014/rule-sections') const index = indexRes.body.results[1].index const res = await request(app).get(`/api/2014/rule-sections/${index}`) const name = res.body.name const descRes = await request(app).get(`/api/2014/rule-sections?desc=${name}`) expect(descRes.statusCode).toEqual(200) expect(descRes.body.results[0].index).toEqual(index) }) it('is case insensitive', async () => { const indexRes = await request(app).get('/api/2014/rule-sections') const index = indexRes.body.results[1].index const name = indexRes.body.results[1].name const queryDesc = name.toLowerCase() const res = await request(app).get(`/api/2014/rule-sections?desc=${queryDesc}`) expect(res.statusCode).toEqual(200) expect(res.body.results[0].index).toEqual(index) }) }) }) describe('/api/2014/rule-sections/:index', () => { it('should return one object', async () => { const indexRes = await request(app).get('/api/2014/rule-sections') const index = indexRes.body.results[0].index const showRes = await request(app).get(`/api/2014/rule-sections/${index}`) expect(showRes.statusCode).toEqual(200) expect(showRes.body.index).toEqual(index) }) describe('with an invalid index', () => { it('should return 404', async () => { const invalidIndex = 'invalid-index' const showRes = await request(app).get(`/api/2014/rule-sections/${invalidIndex}`) expect(showRes.statusCode).toEqual(404) }) }) }) ================================================ FILE: src/tests/integration/api/2014/rules.itest.ts ================================================ import { Application } from 'express' import mongoose from 'mongoose' import request from 'supertest' import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from 'vitest' import createApp from '@/server' import { mongodbUri, redisClient } from '@/util' let app: Application let server: any afterEach(() => { vi.clearAllMocks() }) beforeAll(async () => { await mongoose.connect(mongodbUri) await redisClient.connect() app = await createApp() server = app.listen() // Start the server and store the instance }) afterAll(async () => { await mongoose.disconnect() await redisClient.quit() server.close() }) describe('/api/2014/rules', () => { it('should list rules', async () => { const res = await request(app).get('/api/2014/rules') expect(res.statusCode).toEqual(200) expect(res.body.results.length).not.toEqual(0) }) it('should hit the cache', async () => { await redisClient.del('/api/2014/rules') const clientSet = vi.spyOn(redisClient, 'set') await request(app).get('/api/2014/rules') const res = await request(app).get('/api/2014/rules') expect(res.statusCode).toEqual(200) expect(res.body.results.length).not.toEqual(0) expect(clientSet).toHaveBeenCalledTimes(1) }) describe('with name query', () => { it('returns the named object', async () => { const indexRes = await request(app).get('/api/2014/rules') const name = indexRes.body.results[1].name const res = await request(app).get(`/api/2014/rules?name=${name}`) expect(res.statusCode).toEqual(200) expect(res.body.results[0].name).toEqual(name) }) it('is case insensitive', async () => { const indexRes = await request(app).get('/api/2014/rules') const name = indexRes.body.results[1].name const queryName = name.toLowerCase() const res = await request(app).get(`/api/2014/rules?name=${queryName}`) expect(res.statusCode).toEqual(200) expect(res.body.results[0].name).toEqual(name) }) }) describe('with desc query', () => { it('returns the object with matching desc', async () => { const indexRes = await request(app).get('/api/2014/rules') const index = indexRes.body.results[1].index const res = await request(app).get(`/api/2014/rules/${index}`) const name = res.body.name const descRes = await request(app).get(`/api/2014/rules?desc=${name}`) expect(descRes.statusCode).toEqual(200) expect(descRes.body.results[0].index).toEqual(index) }) it('is case insensitive', async () => { const indexRes = await request(app).get('/api/2014/rules') const index = indexRes.body.results[1].index const name = indexRes.body.results[1].name const queryDesc = name.toLowerCase() const res = await request(app).get(`/api/2014/rules?desc=${queryDesc}`) expect(res.statusCode).toEqual(200) expect(res.body.results[0].index).toEqual(index) }) }) describe('/api/2014/rules/:index', () => { it('should return one object', async () => { const indexRes = await request(app).get('/api/2014/rules') const index = indexRes.body.results[0].index const showRes = await request(app).get(`/api/2014/rules/${index}`) expect(showRes.statusCode).toEqual(200) expect(showRes.body.index).toEqual(index) }) describe('with an invalid index', () => { it('should return 404', async () => { const invalidIndex = 'invalid-index' const showRes = await request(app).get(`/api/2014/rules/${invalidIndex}`) expect(showRes.statusCode).toEqual(404) }) }) }) }) ================================================ FILE: src/tests/integration/api/2014/skills.itest.ts ================================================ import { Application } from 'express' import mongoose from 'mongoose' import request from 'supertest' import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from 'vitest' import createApp from '@/server' import { mongodbUri, redisClient } from '@/util' let app: Application let server: any afterEach(() => { vi.clearAllMocks() }) beforeAll(async () => { await mongoose.connect(mongodbUri) await redisClient.connect() app = await createApp() server = app.listen() // Start the server and store the instance }) afterAll(async () => { await mongoose.disconnect() await redisClient.quit() server.close() }) describe('/api/2014/skills', () => { it('should list skills', async () => { const res = await request(app).get('/api/2014/skills') expect(res.statusCode).toEqual(200) expect(res.body.results.length).not.toEqual(0) }) describe('with name query', () => { it('returns the named object', async () => { const indexRes = await request(app).get('/api/2014/skills') const name = indexRes.body.results[1].name const res = await request(app).get(`/api/2014/skills?name=${name}`) expect(res.statusCode).toEqual(200) expect(res.body.results[0].name).toEqual(name) }) it('is case insensitive', async () => { const indexRes = await request(app).get('/api/2014/skills') const name = indexRes.body.results[1].name const queryName = name.toLowerCase() const res = await request(app).get(`/api/2014/skills?name=${queryName}`) expect(res.statusCode).toEqual(200) expect(res.body.results[0].name).toEqual(name) }) }) describe('/api/2014/skills/:index', () => { it('should return one object', async () => { const indexRes = await request(app).get('/api/2014/skills') const index = indexRes.body.results[0].index const showRes = await request(app).get(`/api/2014/skills/${index}`) expect(showRes.statusCode).toEqual(200) expect(showRes.body.index).toEqual(index) }) describe('with an invalid index', () => { it('should return 404', async () => { const invalidIndex = 'invalid-index' const showRes = await request(app).get(`/api/2014/skills/${invalidIndex}`) expect(showRes.statusCode).toEqual(404) }) }) }) }) ================================================ FILE: src/tests/integration/api/2014/spells.itest.ts ================================================ import { Application } from 'express' import mongoose from 'mongoose' import request from 'supertest' import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from 'vitest' import createApp from '@/server' import { mongodbUri, redisClient } from '@/util' let app: Application let server: any afterEach(() => { vi.clearAllMocks() }) beforeAll(async () => { await mongoose.connect(mongodbUri) await redisClient.connect() app = await createApp() server = app.listen() // Start the server and store the instance }) afterAll(async () => { await mongoose.disconnect() await redisClient.quit() server.close() }) describe('/api/2014/spells', () => { it('should list spells', async () => { const res = await request(app).get('/api/2014/spells') expect(res.statusCode).toEqual(200) expect(res.body.results.length).not.toEqual(0) }) it('should hit the cache', async () => { await redisClient.del('/api/2014/spells') const clientSet = vi.spyOn(redisClient, 'set') await request(app).get('/api/2014/spells') const res = await request(app).get('/api/2014/spells') expect(res.statusCode).toEqual(200) expect(res.body.results.length).not.toEqual(0) expect(clientSet).toHaveBeenCalledTimes(1) }) describe('with name query', () => { it('returns the named object', async () => { const indexRes = await request(app).get('/api/2014/spells') const name = indexRes.body.results[1].name const res = await request(app).get(`/api/2014/spells?name=${name}`) expect(res.statusCode).toEqual(200) expect(res.body.results[0].name).toEqual(name) }) it('is case insensitive', async () => { const indexRes = await request(app).get('/api/2014/spells') const name = indexRes.body.results[1].name const queryName = name.toLowerCase() const res = await request(app).get(`/api/2014/spells?name=${queryName}`) expect(res.statusCode).toEqual(200) expect(res.body.results[0].name).toEqual(name) }) }) describe('with level query', () => { it('returns expected objects', async () => { const expectedLevel = 2 const res = await request(app).get(`/api/2014/spells?level=${expectedLevel}`) expect(res.statusCode).toEqual(200) const randomIndex = Math.floor(Math.random() * res.body.results.length) const randomResult = res.body.results[randomIndex] const indexRes = await request(app).get(`/api/2014/spells/${randomResult.index}`) expect(indexRes.statusCode).toEqual(200) expect(indexRes.body.level).toEqual(expectedLevel) }) describe('with many provided level', () => { it('returns expected objects', async () => { const expectedLevel1 = 1 const res1 = await request(app).get(`/api/2014/spells?level=${expectedLevel1}`) expect(res1.statusCode).toEqual(200) const expectLevel2 = 8 const res2 = await request(app).get(`/api/2014/spells?level=${expectLevel2}`) expect(res2.statusCode).toEqual(200) const bothRes = await request(app).get( `/api/2014/spells?level=${expectedLevel1}&level=${expectLevel2}` ) expect(bothRes.statusCode).toEqual(200) expect(bothRes.body.count).toEqual(res1.body.count + res2.body.count) const randomIndex = Math.floor(Math.random() * bothRes.body.results.length) const randomResult = bothRes.body.results[randomIndex] const indexRes = await request(app).get(`/api/2014/spells/${randomResult.index}`) expect(indexRes.statusCode).toEqual(200) expect( indexRes.body.level == expectedLevel1 || indexRes.body.level == expectLevel2 ).toBeTruthy() }) }) }) describe('with school query', () => { it('returns expected objects', async () => { const expectedSchool = 'Illusion' const res = await request(app).get(`/api/2014/spells?school=${expectedSchool}`) expect(res.statusCode).toEqual(200) const randomIndex = Math.floor(Math.random() * res.body.results.length) const randomResult = res.body.results[randomIndex] const indexRes = await request(app).get(`/api/2014/spells/${randomResult.index}`) expect(indexRes.statusCode).toEqual(200) expect(indexRes.body.school.name).toEqual(expectedSchool) }) describe('with many provided schools', () => { it('returns expected objects', async () => { const expectedSchool1 = 'Illusion' const res1 = await request(app).get(`/api/2014/spells?school=${expectedSchool1}`) expect(res1.statusCode).toEqual(200) const expectedSchool2 = 'Evocation' const res2 = await request(app).get(`/api/2014/spells?school=${expectedSchool2}`) expect(res2.statusCode).toEqual(200) const bothRes = await request(app).get( `/api/2014/spells?school=${expectedSchool1}&school=${expectedSchool2}` ) expect(bothRes.statusCode).toEqual(200) expect(bothRes.body.count).toEqual(res1.body.count + res2.body.count) const randomIndex = Math.floor(Math.random() * bothRes.body.results.length) const randomResult = bothRes.body.results[randomIndex] const indexRes = await request(app).get(`/api/2014/spells/${randomResult.index}`) expect(indexRes.statusCode).toEqual(200) expect( indexRes.body.school.name == expectedSchool1 || indexRes.body.school.name == expectedSchool2 ).toBeTruthy() }) }) it('is case insensitive', async () => { const indexRes = await request(app).get('/api/2014/spells') const index = indexRes.body.results[0].index const showRes = await request(app).get(`/api/2014/spells/${index}`) const school = showRes.body.school.name const querySchool = school.toLowerCase() const res = await request(app).get(`/api/2014/spells?school=${querySchool}`) const randomIndex = Math.floor(Math.random() * res.body.results.length) const randomResult = res.body.results[randomIndex] const queryRes = await request(app).get(`/api/2014/spells/${randomResult.index}`) expect(queryRes.statusCode).toEqual(200) expect(queryRes.body.school.name).toEqual(school) }) }) describe('/api/2014/spells/:index', () => { it('should return one object', async () => { const indexRes = await request(app).get('/api/2014/spells') const index = indexRes.body.results[0].index const showRes = await request(app).get(`/api/2014/spells/${index}`) expect(showRes.statusCode).toEqual(200) expect(showRes.body.index).toEqual(index) }) describe('with an invalid index', () => { it('should return 404', async () => { const invalidIndex = 'invalid-index' const showRes = await request(app).get(`/api/2014/spells/${invalidIndex}`) expect(showRes.statusCode).toEqual(404) }) }) }) }) ================================================ FILE: src/tests/integration/api/2014/subclasses.itest.ts ================================================ import { Application } from 'express' import mongoose from 'mongoose' import request from 'supertest' import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from 'vitest' import createApp from '@/server' import { mongodbUri, redisClient } from '@/util' let app: Application let server: any afterEach(() => { vi.clearAllMocks() }) beforeAll(async () => { await mongoose.connect(mongodbUri) await redisClient.connect() app = await createApp() server = app.listen() // Start the server and store the instance }) afterAll(async () => { await mongoose.disconnect() await redisClient.quit() server.close() }) describe('/api/2014/subclasses', () => { it('should list subclasses', async () => { const res = await request(app).get('/api/2014/subclasses') expect(res.statusCode).toEqual(200) expect(res.body.results.length).not.toEqual(0) }) describe('with name query', () => { it('returns the named object', async () => { const indexRes = await request(app).get('/api/2014/subclasses') const name = indexRes.body.results[1].name const res = await request(app).get(`/api/2014/subclasses?name=${name}`) expect(res.statusCode).toEqual(200) expect(res.body.results[0].name).toEqual(name) }) it('is case insensitive', async () => { const indexRes = await request(app).get('/api/2014/subclasses') const name = indexRes.body.results[1].name const queryName = name.toLowerCase() const res = await request(app).get(`/api/2014/subclasses?name=${queryName}`) expect(res.statusCode).toEqual(200) expect(res.body.results[0].name).toEqual(name) }) }) describe('/api/2014/subclasses/:index', () => { it('should return one object', async () => { const indexRes = await request(app).get('/api/2014/subclasses') const index = indexRes.body.results[0].index const showRes = await request(app).get(`/api/2014/subclasses/${index}`) expect(showRes.statusCode).toEqual(200) expect(showRes.body.index).toEqual(index) }) describe('with an invalid index', () => { it('should return 404', async () => { const invalidIndex = 'invalid-index' const showRes = await request(app).get(`/api/2014/subclasses/${invalidIndex}`) expect(showRes.statusCode).toEqual(404) }) }) describe('/api/2014/subclasses/:index/levels', () => { it('returns objects', async () => { const index = 'berserker' const res = await request(app).get(`/api/2014/subclasses/${index}/levels`) expect(res.statusCode).toEqual(200) expect(res.body.length).not.toEqual(0) }) describe('/api/2014/subclasses/:index/levels/:level', () => { it('returns objects', async () => { const index = 'berserker' const level = 3 const res = await request(app).get(`/api/2014/subclasses/${index}/levels/${level}`) expect(res.statusCode).toEqual(200) expect(res.body.level).toEqual(level) }) describe('/api/2014/subclasses/:index/levels/:level/features', () => { it('returns objects', async () => { const index = 'berserker' const level = 3 const res = await request(app).get( `/api/2014/subclasses/${index}/levels/${level}/features` ) expect(res.statusCode).toEqual(200) expect(res.body.results.length).not.toEqual(0) }) }) }) }) }) }) ================================================ FILE: src/tests/integration/api/2014/subraces.itest.ts ================================================ import { Application } from 'express' import mongoose from 'mongoose' import request from 'supertest' import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from 'vitest' import createApp from '@/server' import { mongodbUri, redisClient } from '@/util' let app: Application let server: any afterEach(() => { vi.clearAllMocks() }) beforeAll(async () => { await mongoose.connect(mongodbUri) await redisClient.connect() app = await createApp() server = app.listen() // Start the server and store the instance }) afterAll(async () => { await mongoose.disconnect() await redisClient.quit() server.close() }) describe('/api/2014/subraces', () => { it('should list subraces', async () => { const res = await request(app).get('/api/2014/subraces') expect(res.statusCode).toEqual(200) expect(res.body.results.length).not.toEqual(0) }) describe('with name query', () => { it('returns the named object', async () => { const indexRes = await request(app).get('/api/2014/subraces') const name = indexRes.body.results[1].name const res = await request(app).get(`/api/2014/subraces?name=${name}`) expect(res.statusCode).toEqual(200) expect(res.body.results[0].name).toEqual(name) }) it('is case insensitive', async () => { const indexRes = await request(app).get('/api/2014/subraces') const name = indexRes.body.results[1].name const queryName = name.toLowerCase() const res = await request(app).get(`/api/2014/subraces?name=${queryName}`) expect(res.statusCode).toEqual(200) expect(res.body.results[0].name).toEqual(name) }) }) describe('/api/2014/subraces/:index', () => { it('should return one object', async () => { const indexRes = await request(app).get('/api/2014/subraces') const index = indexRes.body.results[0].index const showRes = await request(app).get(`/api/2014/subraces/${index}`) expect(showRes.statusCode).toEqual(200) expect(showRes.body.index).toEqual(index) }) describe('with an invalid index', () => { it('should return 404', async () => { const invalidIndex = 'invalid-index' const showRes = await request(app).get(`/api/2014/subraces/${invalidIndex}`) expect(showRes.statusCode).toEqual(404) }) }) describe('/api/2014/subraces/:index/traits', () => { it('returns objects', async () => { const indexRes = await request(app).get('/api/2014/subraces') const index = indexRes.body.results[1].index const res = await request(app).get(`/api/2014/subraces/${index}/traits`) expect(res.statusCode).toEqual(200) expect(res.body.results.length).not.toEqual(0) }) }) describe('/api/2014/subraces/:index/proficiencies', () => { it('returns objects', async () => { const res = await request(app).get('/api/2014/subraces/high-elf/proficiencies') expect(res.statusCode).toEqual(200) expect(res.body.results.length).not.toEqual(0) }) }) }) }) ================================================ FILE: src/tests/integration/api/2014/traits.itest.ts ================================================ import { Application } from 'express' import mongoose from 'mongoose' import request from 'supertest' import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from 'vitest' import createApp from '@/server' import { mongodbUri, redisClient } from '@/util' let app: Application let server: any afterEach(() => { vi.clearAllMocks() }) beforeAll(async () => { await mongoose.connect(mongodbUri) await redisClient.connect() app = await createApp() server = app.listen() // Start the server and store the instance }) afterAll(async () => { await mongoose.disconnect() await redisClient.quit() server.close() }) describe('/api/2014/traits', () => { it('should list traits', async () => { const res = await request(app).get('/api/2014/traits') expect(res.statusCode).toEqual(200) expect(res.body.results.length).not.toEqual(0) }) describe('with name query', () => { it('returns the named object', async () => { const indexRes = await request(app).get('/api/2014/traits') const name = indexRes.body.results[1].name const res = await request(app).get(`/api/2014/traits?name=${name}`) expect(res.statusCode).toEqual(200) expect(res.body.results[0].name).toEqual(name) }) it('is case insensitive', async () => { const indexRes = await request(app).get('/api/2014/traits') const name = indexRes.body.results[1].name const queryName = name.toLowerCase() const res = await request(app).get(`/api/2014/traits?name=${queryName}`) expect(res.statusCode).toEqual(200) expect(res.body.results[0].name).toEqual(name) }) }) describe('/api/2014/traits/:index', () => { it('should return one object', async () => { const indexRes = await request(app).get('/api/2014/traits') const index = indexRes.body.results[0].index const showRes = await request(app).get(`/api/2014/traits/${index}`) expect(showRes.statusCode).toEqual(200) expect(showRes.body.index).toEqual(index) }) describe('with an invalid index', () => { it('should return 404', async () => { const invalidIndex = 'invalid-index' const showRes = await request(app).get(`/api/2014/traits/${invalidIndex}`) expect(showRes.statusCode).toEqual(404) }) }) }) }) ================================================ FILE: src/tests/integration/api/2014/weaponProperties.itest.ts ================================================ import { Application } from 'express' import mongoose from 'mongoose' import request from 'supertest' import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from 'vitest' import createApp from '@/server' import { mongodbUri, redisClient } from '@/util' let app: Application let server: any afterEach(() => { vi.clearAllMocks() }) beforeAll(async () => { await mongoose.connect(mongodbUri) await redisClient.connect() app = await createApp() server = app.listen() // Start the server and store the instance }) afterAll(async () => { await mongoose.disconnect() await redisClient.quit() server.close() }) describe('/api/2014/weapon-properties', () => { it('should list weapon properties', async () => { const res = await request(app).get('/api/2014/weapon-properties') expect(res.statusCode).toEqual(200) expect(res.body.results.length).not.toEqual(0) }) describe('with name query', () => { it('returns the named object', async () => { const indexRes = await request(app).get('/api/2014/weapon-properties') const name = indexRes.body.results[1].name const res = await request(app).get(`/api/2014/weapon-properties?name=${name}`) expect(res.statusCode).toEqual(200) expect(res.body.results[0].name).toEqual(name) }) it('is case insensitive', async () => { const indexRes = await request(app).get('/api/2014/weapon-properties') const name = indexRes.body.results[1].name const queryName = name.toLowerCase() const res = await request(app).get(`/api/2014/weapon-properties?name=${queryName}`) expect(res.statusCode).toEqual(200) expect(res.body.results[0].name).toEqual(name) }) }) describe('/api/2014/weapon-properties/:index', () => { it('should return one object', async () => { const indexRes = await request(app).get('/api/2014/weapon-properties') const index = indexRes.body.results[0].index const showRes = await request(app).get(`/api/2014/weapon-properties/${index}`) expect(showRes.statusCode).toEqual(200) expect(showRes.body.index).toEqual(index) }) describe('with an invalid index', () => { it('should return 404', async () => { const invalidIndex = 'invalid-index' const showRes = await request(app).get(`/api/2014/weapon-properties/${invalidIndex}`) expect(showRes.statusCode).toEqual(404) }) }) }) }) ================================================ FILE: src/tests/integration/api/2024/abilityScores.itest.ts ================================================ import { Application } from 'express' import mongoose from 'mongoose' import request from 'supertest' import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from 'vitest' import createApp from '@/server' import { mongodbUri, redisClient } from '@/util' let app: Application let server: any afterEach(() => { vi.clearAllMocks() }) beforeAll(async () => { await mongoose.connect(mongodbUri) await redisClient.connect() app = await createApp() server = app.listen() // Start the server and store the instance }) afterAll(async () => { await mongoose.disconnect() await redisClient.quit() server.close() }) describe('/api/2024/ability-scores', () => { it('should list ability scores', async () => { const res = await request(app).get('/api/2024/ability-scores') expect(res.statusCode).toEqual(200) expect(res.body.results.length).not.toEqual(0) }) describe('with name query', () => { it('returns the named object', async () => { const indexRes = await request(app).get('/api/2024/ability-scores') const name = indexRes.body.results[1].name const res = await request(app).get(`/api/2024/ability-scores?name=${name}`) expect(res.statusCode).toEqual(200) expect(res.body.results[0].name).toEqual(name) }) it('is case insensitive', async () => { const indexRes = await request(app).get('/api/2024/ability-scores') const name = indexRes.body.results[1].name const queryName = name.toLowerCase() const res = await request(app).get(`/api/2024/ability-scores?name=${queryName}`) expect(res.statusCode).toEqual(200) expect(res.body.results[0].name).toEqual(name) }) }) describe('/api/2024/ability-scores/:index', () => { it('should return one object', async () => { const indexRes = await request(app).get('/api/2024/ability-scores') const index = indexRes.body.results[0].index const showRes = await request(app).get(`/api/2024/ability-scores/${index}`) expect(showRes.statusCode).toEqual(200) expect(showRes.body.index).toEqual(index) }) describe('with an invalid index', () => { it('should return 404', async () => { const invalidIndex = 'invalid-index' const showRes = await request(app).get(`/api/2024/ability-scores/${invalidIndex}`) expect(showRes.statusCode).toEqual(404) }) }) }) }) ================================================ FILE: src/tests/integration/api/2024/alignment.itest.ts ================================================ import { Application } from 'express' import mongoose from 'mongoose' import request from 'supertest' import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from 'vitest' import createApp from '@/server' import { mongodbUri, redisClient } from '@/util' let app: Application let server: any afterEach(() => { vi.clearAllMocks() }) beforeAll(async () => { await mongoose.connect(mongodbUri) await redisClient.connect() app = await createApp() server = app.listen() }) afterAll(async () => { await mongoose.disconnect() await redisClient.quit() server.close() }) describe('/api/2024/alignments', () => { it('should list alignments', async () => { const res = await request(app).get('/api/2024/alignments') expect(res.statusCode).toEqual(200) expect(res.body.results.length).not.toEqual(0) }) describe('with name query', () => { it('returns the named object', async () => { const indexRes = await request(app).get('/api/2024/alignments') const name = indexRes.body.results[1].name const res = await request(app).get(`/api/2024/alignments?name=${name}`) expect(res.statusCode).toEqual(200) expect(res.body.results[0].name).toEqual(name) }) it('is case insensitive', async () => { const indexRes = await request(app).get('/api/2024/alignments') const name = indexRes.body.results[1].name const queryName = name.toLowerCase() const res = await request(app).get(`/api/2024/alignments?name=${queryName}`) expect(res.statusCode).toEqual(200) expect(res.body.results[0].name).toEqual(name) }) }) describe('/api/2024/alignments/:index', () => { it('should return one object', async () => { const indexRes = await request(app).get('/api/2024/alignments') const index = indexRes.body.results[0].index const showRes = await request(app).get(`/api/2024/alignments/${index}`) expect(showRes.statusCode).toEqual(200) expect(showRes.body.index).toEqual(index) }) describe('with an invalid index', () => { it('should return 404', async () => { const invalidIndex = 'invalid-index' const showRes = await request(app).get(`/api/2024/alignments/${invalidIndex}`) expect(showRes.statusCode).toEqual(404) }) }) }) }) ================================================ FILE: src/tests/integration/api/2024/background.itest.ts ================================================ import { Application } from 'express' import mongoose from 'mongoose' import request from 'supertest' import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from 'vitest' import createApp from '@/server' import { mongodbUri, redisClient } from '@/util' let app: Application let server: any afterEach(() => { vi.clearAllMocks() }) beforeAll(async () => { await mongoose.connect(mongodbUri) await redisClient.connect() app = await createApp() server = app.listen() }) afterAll(async () => { await mongoose.disconnect() await redisClient.quit() server.close() }) describe('/api/2024/backgrounds', () => { it('should list backgrounds', async () => { const res = await request(app).get('/api/2024/backgrounds') expect(res.statusCode).toEqual(200) expect(res.body.results.length).not.toEqual(0) }) describe('with name query', () => { it('returns the named object', async () => { const indexRes = await request(app).get('/api/2024/backgrounds') const name = indexRes.body.results[0].name const res = await request(app).get(`/api/2024/backgrounds?name=${name}`) expect(res.statusCode).toEqual(200) expect(res.body.results[0].name).toEqual(name) }) it('is case insensitive', async () => { const indexRes = await request(app).get('/api/2024/backgrounds') const name = indexRes.body.results[0].name const res = await request(app).get(`/api/2024/backgrounds?name=${name.toLowerCase()}`) expect(res.statusCode).toEqual(200) expect(res.body.results[0].name).toEqual(name) }) }) describe('/api/2024/backgrounds/:index', () => { it('should return one object', async () => { const indexRes = await request(app).get('/api/2024/backgrounds') const index = indexRes.body.results[0].index const showRes = await request(app).get(`/api/2024/backgrounds/${index}`) expect(showRes.statusCode).toEqual(200) expect(showRes.body.index).toEqual(index) }) describe('with an invalid index', () => { it('should return 404', async () => { const showRes = await request(app).get('/api/2024/backgrounds/invalid-index') expect(showRes.statusCode).toEqual(404) }) }) }) }) ================================================ FILE: src/tests/integration/api/2024/condition.itest.ts ================================================ import { Application } from 'express' import mongoose from 'mongoose' import request from 'supertest' import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from 'vitest' import createApp from '@/server' import { mongodbUri, redisClient } from '@/util' let app: Application let server: any afterEach(() => { vi.clearAllMocks() }) beforeAll(async () => { await mongoose.connect(mongodbUri) await redisClient.connect() app = await createApp() server = app.listen() }) afterAll(async () => { await mongoose.disconnect() await redisClient.quit() server.close() }) describe('/api/2024/conditions', () => { it('should list conditions', async () => { const res = await request(app).get('/api/2024/conditions') expect(res.statusCode).toEqual(200) expect(res.body.results.length).not.toEqual(0) }) describe('with name query', () => { it('returns the named object', async () => { const indexRes = await request(app).get('/api/2024/conditions') const name = indexRes.body.results[1].name const res = await request(app).get(`/api/2024/conditions?name=${name}`) expect(res.statusCode).toEqual(200) expect(res.body.results[0].name).toEqual(name) }) it('is case insensitive', async () => { const indexRes = await request(app).get('/api/2024/conditions') const name = indexRes.body.results[1].name const queryName = name.toLowerCase() const res = await request(app).get(`/api/2024/conditions?name=${queryName}`) expect(res.statusCode).toEqual(200) expect(res.body.results[0].name).toEqual(name) }) }) describe('/api/2024/conditions/:index', () => { it('should return one object', async () => { const indexRes = await request(app).get('/api/2024/conditions') const index = indexRes.body.results[0].index const showRes = await request(app).get(`/api/2024/conditions/${index}`) expect(showRes.statusCode).toEqual(200) expect(showRes.body.index).toEqual(index) }) describe('with an invalid index', () => { it('should return 404', async () => { const invalidIndex = 'invalid-index' const showRes = await request(app).get(`/api/2024/conditions/${invalidIndex}`) expect(showRes.statusCode).toEqual(404) }) }) }) }) ================================================ FILE: src/tests/integration/api/2024/damageType.itest.ts ================================================ import { Application } from 'express' import mongoose from 'mongoose' import request from 'supertest' import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from 'vitest' import createApp from '@/server' import { mongodbUri, redisClient } from '@/util' let app: Application let server: any afterEach(() => { vi.clearAllMocks() }) beforeAll(async () => { await mongoose.connect(mongodbUri) await redisClient.connect() app = await createApp() server = app.listen() }) afterAll(async () => { await mongoose.disconnect() await redisClient.quit() server.close() }) describe('/api/2024/damage-types', () => { it('should list damage types', async () => { const res = await request(app).get('/api/2024/damage-types') expect(res.statusCode).toEqual(200) expect(res.body.results.length).not.toEqual(0) }) describe('with name query', () => { it('returns the named object', async () => { const indexRes = await request(app).get('/api/2024/damage-types') const name = indexRes.body.results[1].name const res = await request(app).get(`/api/2024/damage-types?name=${name}`) expect(res.statusCode).toEqual(200) expect(res.body.results[0].name).toEqual(name) }) it('is case insensitive', async () => { const indexRes = await request(app).get('/api/2024/damage-types') const name = indexRes.body.results[1].name const queryName = name.toLowerCase() const res = await request(app).get(`/api/2024/damage-types?name=${queryName}`) expect(res.statusCode).toEqual(200) expect(res.body.results[0].name).toEqual(name) }) }) describe('/api/2024/damage-types/:index', () => { it('should return one object', async () => { const indexRes = await request(app).get('/api/2024/damage-types') const index = indexRes.body.results[0].index const showRes = await request(app).get(`/api/2024/damage-types/${index}`) expect(showRes.statusCode).toEqual(200) expect(showRes.body.index).toEqual(index) }) describe('with an invalid index', () => { it('should return 404', async () => { const invalidIndex = 'invalid-index' const showRes = await request(app).get(`/api/2024/damage-types/${invalidIndex}`) expect(showRes.statusCode).toEqual(404) }) }) }) }) ================================================ FILE: src/tests/integration/api/2024/equipment.itest.ts ================================================ import { Application } from 'express' import mongoose from 'mongoose' import request from 'supertest' import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from 'vitest' import createApp from '@/server' import { mongodbUri, redisClient } from '@/util' let app: Application let server: any afterEach(() => { vi.clearAllMocks() }) beforeAll(async () => { await mongoose.connect(mongodbUri) await redisClient.connect() app = await createApp() server = app.listen() // Start the server and store the instance }) afterAll(async () => { await mongoose.disconnect() await redisClient.quit() server.close() }) describe('/api/2024/equipment', () => { it('should list equipment', async () => { const res = await request(app).get('/api/2024/equipment') expect(res.statusCode).toEqual(200) expect(res.body.results.length).not.toEqual(0) }) describe('with name query', () => { it('returns the named object', async () => { const indexRes = await request(app).get('/api/2024/equipment') const name = indexRes.body.results[1].name const res = await request(app).get(`/api/2024/equipment?name=${name}`) expect(res.statusCode).toEqual(200) expect(res.body.results[0].name).toEqual(name) }) it('is case insensitive', async () => { const indexRes = await request(app).get('/api/2024/equipment') const name = indexRes.body.results[1].name const queryName = name.toLowerCase() const res = await request(app).get(`/api/2024/equipment?name=${queryName}`) expect(res.statusCode).toEqual(200) expect(res.body.results[0].name).toEqual(name) }) }) describe('/api/2024/equipment/:index', () => { it('should return one object', async () => { const indexRes = await request(app).get('/api/2024/equipment') const index = indexRes.body.results[0].index const showRes = await request(app).get(`/api/2024/equipment/${index}`) expect(showRes.statusCode).toEqual(200) expect(showRes.body.index).toEqual(index) }) describe('with an invalid index', () => { it('should return 404', async () => { const invalidIndex = 'invalid-index' const showRes = await request(app).get(`/api/2024/equipment/${invalidIndex}`) expect(showRes.statusCode).toEqual(404) }) }) }) }) ================================================ FILE: src/tests/integration/api/2024/equipmentCategories.itest.ts ================================================ import { Application } from 'express' import mongoose from 'mongoose' import request from 'supertest' import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from 'vitest' import createApp from '@/server' import { mongodbUri, redisClient } from '@/util' let app: Application let server: any afterEach(() => { vi.clearAllMocks() }) beforeAll(async () => { await mongoose.connect(mongodbUri) await redisClient.connect() app = await createApp() server = app.listen() // Start the server and store the instance }) afterAll(async () => { await mongoose.disconnect() await redisClient.quit() server.close() }) describe('/api/2024/equipment-categories', () => { it('should list equipment categories', async () => { const res = await request(app).get('/api/2024/equipment-categories') expect(res.statusCode).toEqual(200) expect(res.body.results.length).not.toEqual(0) }) describe('with name query', () => { it('returns the named object', async () => { const indexRes = await request(app).get('/api/2024/equipment-categories') const name = indexRes.body.results[1].name const res = await request(app).get(`/api/2024/equipment-categories?name=${name}`) expect(res.statusCode).toEqual(200) expect(res.body.results[0].name).toEqual(name) }) it('is case insensitive', async () => { const indexRes = await request(app).get('/api/2024/equipment-categories') const name = indexRes.body.results[1].name const queryName = name.toLowerCase() const res = await request(app).get(`/api/2024/equipment-categories?name=${queryName}`) expect(res.statusCode).toEqual(200) expect(res.body.results[0].name).toEqual(name) }) }) describe('/api/2024/equipment-categories/:index', () => { it('should return one object', async () => { const indexRes = await request(app).get('/api/2024/equipment-categories') const index = indexRes.body.results[0].index const showRes = await request(app).get(`/api/2024/equipment-categories/${index}`) expect(showRes.statusCode).toEqual(200) expect(showRes.body.index).toEqual(index) }) describe('with an invalid index', () => { it('should return 404', async () => { const invalidIndex = 'invalid-index' const showRes = await request(app).get(`/api/2024/equipment-categories/${invalidIndex}`) expect(showRes.statusCode).toEqual(404) }) }) }) }) ================================================ FILE: src/tests/integration/api/2024/feat.itest.ts ================================================ import { Application } from 'express' import mongoose from 'mongoose' import request from 'supertest' import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from 'vitest' import createApp from '@/server' import { mongodbUri, redisClient } from '@/util' let app: Application let server: any afterEach(() => { vi.clearAllMocks() }) beforeAll(async () => { await mongoose.connect(mongodbUri) await redisClient.connect() app = await createApp() server = app.listen() }) afterAll(async () => { await mongoose.disconnect() await redisClient.quit() server.close() }) describe('/api/2024/feats', () => { it('should list feats', async () => { const res = await request(app).get('/api/2024/feats') expect(res.statusCode).toEqual(200) expect(res.body.results.length).not.toEqual(0) }) describe('with name query', () => { it('returns the named object', async () => { const indexRes = await request(app).get('/api/2024/feats') const name = indexRes.body.results[0].name const res = await request(app).get(`/api/2024/feats?name=${name}`) expect(res.statusCode).toEqual(200) expect(res.body.results[0].name).toEqual(name) }) it('is case insensitive', async () => { const indexRes = await request(app).get('/api/2024/feats') const name = indexRes.body.results[0].name const res = await request(app).get(`/api/2024/feats?name=${name.toLowerCase()}`) expect(res.statusCode).toEqual(200) expect(res.body.results[0].name).toEqual(name) }) }) describe('/api/2024/feats/:index', () => { it('should return one object', async () => { const indexRes = await request(app).get('/api/2024/feats') const index = indexRes.body.results[0].index const showRes = await request(app).get(`/api/2024/feats/${index}`) expect(showRes.statusCode).toEqual(200) expect(showRes.body.index).toEqual(index) }) describe('with an invalid index', () => { it('should return 404', async () => { const showRes = await request(app).get('/api/2024/feats/invalid-index') expect(showRes.statusCode).toEqual(404) }) }) }) }) ================================================ FILE: src/tests/integration/api/2024/language.itest.ts ================================================ import { Application } from 'express' import mongoose from 'mongoose' import request from 'supertest' import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from 'vitest' import createApp from '@/server' import { mongodbUri, redisClient } from '@/util' let app: Application let server: any afterEach(() => { vi.clearAllMocks() }) beforeAll(async () => { await mongoose.connect(mongodbUri) await redisClient.connect() app = await createApp() server = app.listen() }) afterAll(async () => { await mongoose.disconnect() await redisClient.quit() server.close() }) describe('/api/2024/languages', () => { it('should list languages', async () => { const res = await request(app).get('/api/2024/languages') expect(res.statusCode).toEqual(200) expect(res.body.results.length).not.toEqual(0) }) describe('with name query', () => { it('returns the named object', async () => { const indexRes = await request(app).get('/api/2024/languages') const name = indexRes.body.results[1].name const res = await request(app).get(`/api/2024/languages?name=${name}`) expect(res.statusCode).toEqual(200) expect(res.body.results[0].name).toEqual(name) }) it('is case insensitive', async () => { const indexRes = await request(app).get('/api/2024/languages') const name = indexRes.body.results[1].name const queryName = name.toLowerCase() const res = await request(app).get(`/api/2024/languages?name=${queryName}`) expect(res.statusCode).toEqual(200) expect(res.body.results[0].name).toEqual(name) }) }) describe('/api/2024/languages/:index', () => { it('should return one object', async () => { const indexRes = await request(app).get('/api/2024/languages') const index = indexRes.body.results[0].index const showRes = await request(app).get(`/api/2024/languages/${index}`) expect(showRes.statusCode).toEqual(200) expect(showRes.body.index).toEqual(index) }) describe('with an invalid index', () => { it('should return 404', async () => { const invalidIndex = 'invalid-index' const showRes = await request(app).get(`/api/2024/languages/${invalidIndex}`) expect(showRes.statusCode).toEqual(404) }) }) }) }) ================================================ FILE: src/tests/integration/api/2024/magicItem.itest.ts ================================================ import { Application } from 'express' import mongoose from 'mongoose' import request from 'supertest' import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from 'vitest' import createApp from '@/server' import { mongodbUri, redisClient } from '@/util' let app: Application let server: any afterEach(() => { vi.clearAllMocks() }) beforeAll(async () => { await mongoose.connect(mongodbUri) await redisClient.connect() app = await createApp() server = app.listen() }) afterAll(async () => { await mongoose.disconnect() await redisClient.quit() server.close() }) describe('/api/2024/magic-items', () => { it('should list magic items', async () => { const res = await request(app).get('/api/2024/magic-items') expect(res.statusCode).toEqual(200) expect(res.body.results.length).not.toEqual(0) }) describe('with name query', () => { it('returns the named object', async () => { const indexRes = await request(app).get('/api/2024/magic-items') const name = indexRes.body.results[0].name const res = await request(app).get(`/api/2024/magic-items?name=${name}`) expect(res.statusCode).toEqual(200) expect(res.body.results[0].name).toEqual(name) }) it('is case insensitive', async () => { const indexRes = await request(app).get('/api/2024/magic-items') const name = indexRes.body.results[0].name const res = await request(app).get(`/api/2024/magic-items?name=${name.toLowerCase()}`) expect(res.statusCode).toEqual(200) expect(res.body.results[0].name).toEqual(name) }) }) describe('/api/2024/magic-items/:index', () => { it('should return one object', async () => { const indexRes = await request(app).get('/api/2024/magic-items') const index = indexRes.body.results[0].index const showRes = await request(app).get(`/api/2024/magic-items/${index}`) expect(showRes.statusCode).toEqual(200) expect(showRes.body.index).toEqual(index) }) describe('with an invalid index', () => { it('should return 404', async () => { const showRes = await request(app).get('/api/2024/magic-items/invalid-index') expect(showRes.statusCode).toEqual(404) }) }) }) }) ================================================ FILE: src/tests/integration/api/2024/magicSchool.itest.ts ================================================ import { Application } from 'express' import mongoose from 'mongoose' import request from 'supertest' import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from 'vitest' import createApp from '@/server' import { mongodbUri, redisClient } from '@/util' let app: Application let server: any afterEach(() => { vi.clearAllMocks() }) beforeAll(async () => { await mongoose.connect(mongodbUri) await redisClient.connect() app = await createApp() server = app.listen() }) afterAll(async () => { await mongoose.disconnect() await redisClient.quit() server.close() }) describe('/api/2024/magic-schools', () => { it('should list magic schools', async () => { const res = await request(app).get('/api/2024/magic-schools') expect(res.statusCode).toEqual(200) expect(res.body.results.length).not.toEqual(0) }) describe('with name query', () => { it('returns the named object', async () => { const indexRes = await request(app).get('/api/2024/magic-schools') const name = indexRes.body.results[1].name const res = await request(app).get(`/api/2024/magic-schools?name=${name}`) expect(res.statusCode).toEqual(200) expect(res.body.results[0].name).toEqual(name) }) it('is case insensitive', async () => { const indexRes = await request(app).get('/api/2024/magic-schools') const name = indexRes.body.results[1].name const queryName = name.toLowerCase() const res = await request(app).get(`/api/2024/magic-schools?name=${queryName}`) expect(res.statusCode).toEqual(200) expect(res.body.results[0].name).toEqual(name) }) }) describe('/api/2024/magic-schools/:index', () => { it('should return one object', async () => { const indexRes = await request(app).get('/api/2024/magic-schools') const index = indexRes.body.results[0].index const showRes = await request(app).get(`/api/2024/magic-schools/${index}`) expect(showRes.statusCode).toEqual(200) expect(showRes.body.index).toEqual(index) }) describe('with an invalid index', () => { it('should return 404', async () => { const invalidIndex = 'invalid-index' const showRes = await request(app).get(`/api/2024/magic-schools/${invalidIndex}`) expect(showRes.statusCode).toEqual(404) }) }) }) }) ================================================ FILE: src/tests/integration/api/2024/proficiency.itest.ts ================================================ import { Application } from 'express' import mongoose from 'mongoose' import request from 'supertest' import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from 'vitest' import createApp from '@/server' import { mongodbUri, redisClient } from '@/util' let app: Application let server: any afterEach(() => { vi.clearAllMocks() }) beforeAll(async () => { await mongoose.connect(mongodbUri) await redisClient.connect() app = await createApp() server = app.listen() }) afterAll(async () => { await mongoose.disconnect() await redisClient.quit() server.close() }) describe('/api/2024/proficiencies', () => { it('should list proficiencies', async () => { const res = await request(app).get('/api/2024/proficiencies') expect(res.statusCode).toEqual(200) expect(res.body.results.length).not.toEqual(0) }) describe('with name query', () => { it('returns the named object', async () => { const indexRes = await request(app).get('/api/2024/proficiencies') const name = indexRes.body.results[0].name const res = await request(app).get(`/api/2024/proficiencies?name=${name}`) expect(res.statusCode).toEqual(200) expect(res.body.results[0].name).toEqual(name) }) it('is case insensitive', async () => { const indexRes = await request(app).get('/api/2024/proficiencies') const name = indexRes.body.results[0].name const res = await request(app).get(`/api/2024/proficiencies?name=${name.toLowerCase()}`) expect(res.statusCode).toEqual(200) expect(res.body.results[0].name).toEqual(name) }) }) describe('/api/2024/proficiencies/:index', () => { it('should return one object', async () => { const indexRes = await request(app).get('/api/2024/proficiencies') const index = indexRes.body.results[0].index const showRes = await request(app).get(`/api/2024/proficiencies/${index}`) expect(showRes.statusCode).toEqual(200) expect(showRes.body.index).toEqual(index) }) describe('with an invalid index', () => { it('should return 404', async () => { const showRes = await request(app).get('/api/2024/proficiencies/invalid-index') expect(showRes.statusCode).toEqual(404) }) }) }) }) ================================================ FILE: src/tests/integration/api/2024/skills.itest.ts ================================================ import { Application } from 'express' import mongoose from 'mongoose' import request from 'supertest' import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from 'vitest' import createApp from '@/server' import { mongodbUri, redisClient } from '@/util' let app: Application let server: any afterEach(() => { vi.clearAllMocks() }) beforeAll(async () => { await mongoose.connect(mongodbUri) await redisClient.connect() app = await createApp() server = app.listen() // Start the server and store the instance }) afterAll(async () => { await mongoose.disconnect() await redisClient.quit() server.close() }) describe('/api/2024/skills', () => { it('should list skills', async () => { const res = await request(app).get('/api/2024/skills') expect(res.statusCode).toEqual(200) expect(res.body.results.length).not.toEqual(0) }) describe('with name query', () => { it('returns the named object', async () => { const indexRes = await request(app).get('/api/2024/skills') const name = indexRes.body.results[1].name const res = await request(app).get(`/api/2024/skills?name=${name}`) expect(res.statusCode).toEqual(200) expect(res.body.results[0].name).toEqual(name) }) it('is case insensitive', async () => { const indexRes = await request(app).get('/api/2024/skills') const name = indexRes.body.results[1].name const queryName = name.toLowerCase() const res = await request(app).get(`/api/2024/skills?name=${queryName}`) expect(res.statusCode).toEqual(200) expect(res.body.results[0].name).toEqual(name) }) }) describe('/api/2024/skills/:index', () => { it('should return one object', async () => { const indexRes = await request(app).get('/api/2024/skills') const index = indexRes.body.results[0].index const showRes = await request(app).get(`/api/2024/skills/${index}`) expect(showRes.statusCode).toEqual(200) expect(showRes.body.index).toEqual(index) }) describe('with an invalid index', () => { it('should return 404', async () => { const invalidIndex = 'invalid-index' const showRes = await request(app).get(`/api/2024/skills/${invalidIndex}`) expect(showRes.statusCode).toEqual(404) }) }) }) }) ================================================ FILE: src/tests/integration/api/2024/species.itest.ts ================================================ import { Application } from 'express' import mongoose from 'mongoose' import request from 'supertest' import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from 'vitest' import createApp from '@/server' import { mongodbUri, redisClient } from '@/util' let app: Application let server: any afterEach(() => { vi.clearAllMocks() }) beforeAll(async () => { await mongoose.connect(mongodbUri) await redisClient.connect() app = await createApp() server = app.listen() }) afterAll(async () => { await mongoose.disconnect() await redisClient.quit() server.close() }) describe('/api/2024/species', () => { it('should list species', async () => { const res = await request(app).get('/api/2024/species') expect(res.statusCode).toEqual(200) expect(res.body.results.length).not.toEqual(0) }) describe('with name query', () => { it('returns the named object', async () => { const indexRes = await request(app).get('/api/2024/species') const name = indexRes.body.results[1].name const res = await request(app).get(`/api/2024/species?name=${name}`) expect(res.statusCode).toEqual(200) expect(res.body.results[0].name).toEqual(name) }) it('is case insensitive', async () => { const indexRes = await request(app).get('/api/2024/species') const name = indexRes.body.results[1].name const queryName = name.toLowerCase() const res = await request(app).get(`/api/2024/species?name=${queryName}`) expect(res.statusCode).toEqual(200) expect(res.body.results[0].name).toEqual(name) }) }) describe('/api/2024/species/:index', () => { it('should return one object', async () => { const indexRes = await request(app).get('/api/2024/species') const index = indexRes.body.results[0].index const showRes = await request(app).get(`/api/2024/species/${index}`) expect(showRes.statusCode).toEqual(200) expect(showRes.body.index).toEqual(index) }) describe('with an invalid index', () => { it('should return 404', async () => { const showRes = await request(app).get('/api/2024/species/invalid-index') expect(showRes.statusCode).toEqual(404) }) }) }) describe('/api/2024/species/:index/subspecies', () => { it('should return a list of subspecies for a species that has them', async () => { // Dragonborn has subspecies in the 2024 data const res = await request(app).get('/api/2024/species/dragonborn/subspecies') expect(res.statusCode).toEqual(200) expect(res.body.results).toBeDefined() }) }) describe('/api/2024/species/:index/traits', () => { it('should return a list of traits for a species', async () => { const indexRes = await request(app).get('/api/2024/species') const index = indexRes.body.results[0].index const res = await request(app).get(`/api/2024/species/${index}/traits`) expect(res.statusCode).toEqual(200) expect(res.body.results).toBeDefined() }) }) }) ================================================ FILE: src/tests/integration/api/2024/subclass.itest.ts ================================================ import { Application } from 'express' import mongoose from 'mongoose' import request from 'supertest' import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from 'vitest' import createApp from '@/server' import { mongodbUri, redisClient } from '@/util' let app: Application let server: any afterEach(() => { vi.clearAllMocks() }) beforeAll(async () => { await mongoose.connect(mongodbUri) await redisClient.connect() app = await createApp() server = app.listen() }) afterAll(async () => { await mongoose.disconnect() await redisClient.quit() server.close() }) describe('/api/2024/subclasses', () => { it('should list subclasses', async () => { const res = await request(app).get('/api/2024/subclasses') expect(res.statusCode).toEqual(200) expect(res.body.results.length).not.toEqual(0) }) describe('with name query', () => { it('returns the named object', async () => { const indexRes = await request(app).get('/api/2024/subclasses') const name = indexRes.body.results[0].name const res = await request(app).get(`/api/2024/subclasses?name=${name}`) expect(res.statusCode).toEqual(200) expect(res.body.results[0].name).toEqual(name) }) it('is case insensitive', async () => { const indexRes = await request(app).get('/api/2024/subclasses') const name = indexRes.body.results[0].name const res = await request(app).get(`/api/2024/subclasses?name=${name.toLowerCase()}`) expect(res.statusCode).toEqual(200) expect(res.body.results[0].name).toEqual(name) }) }) describe('/api/2024/subclasses/:index', () => { it('should return one object', async () => { const indexRes = await request(app).get('/api/2024/subclasses') const index = indexRes.body.results[0].index const showRes = await request(app).get(`/api/2024/subclasses/${index}`) expect(showRes.statusCode).toEqual(200) expect(showRes.body.index).toEqual(index) }) describe('with an invalid index', () => { it('should return 404', async () => { const showRes = await request(app).get('/api/2024/subclasses/invalid-index') expect(showRes.statusCode).toEqual(404) }) }) }) }) ================================================ FILE: src/tests/integration/api/2024/subspecies.itest.ts ================================================ import { Application } from 'express' import mongoose from 'mongoose' import request from 'supertest' import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from 'vitest' import createApp from '@/server' import { mongodbUri, redisClient } from '@/util' let app: Application let server: any afterEach(() => { vi.clearAllMocks() }) beforeAll(async () => { await mongoose.connect(mongodbUri) await redisClient.connect() app = await createApp() server = app.listen() }) afterAll(async () => { await mongoose.disconnect() await redisClient.quit() server.close() }) describe('/api/2024/subspecies', () => { it('should list subspecies', async () => { const res = await request(app).get('/api/2024/subspecies') expect(res.statusCode).toEqual(200) expect(res.body.results.length).not.toEqual(0) }) describe('with name query', () => { it('returns the named object', async () => { const indexRes = await request(app).get('/api/2024/subspecies') const name = indexRes.body.results[1].name const res = await request(app).get(`/api/2024/subspecies?name=${name}`) expect(res.statusCode).toEqual(200) expect(res.body.results[0].name).toEqual(name) }) it('is case insensitive', async () => { const indexRes = await request(app).get('/api/2024/subspecies') const name = indexRes.body.results[1].name const queryName = name.toLowerCase() const res = await request(app).get(`/api/2024/subspecies?name=${queryName}`) expect(res.statusCode).toEqual(200) expect(res.body.results[0].name).toEqual(name) }) }) describe('/api/2024/subspecies/:index', () => { it('should return one object', async () => { const indexRes = await request(app).get('/api/2024/subspecies') const index = indexRes.body.results[0].index const showRes = await request(app).get(`/api/2024/subspecies/${index}`) expect(showRes.statusCode).toEqual(200) expect(showRes.body.index).toEqual(index) }) describe('with an invalid index', () => { it('should return 404', async () => { const showRes = await request(app).get('/api/2024/subspecies/invalid-index') expect(showRes.statusCode).toEqual(404) }) }) }) describe('/api/2024/subspecies/:index/traits', () => { it('should return a list of traits for a subspecies', async () => { const indexRes = await request(app).get('/api/2024/subspecies') const index = indexRes.body.results[0].index const res = await request(app).get(`/api/2024/subspecies/${index}/traits`) expect(res.statusCode).toEqual(200) expect(res.body.results).toBeDefined() }) }) }) ================================================ FILE: src/tests/integration/api/2024/traits.itest.ts ================================================ import { Application } from 'express' import mongoose from 'mongoose' import request from 'supertest' import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from 'vitest' import createApp from '@/server' import { mongodbUri, redisClient } from '@/util' let app: Application let server: any afterEach(() => { vi.clearAllMocks() }) beforeAll(async () => { await mongoose.connect(mongodbUri) await redisClient.connect() app = await createApp() server = app.listen() }) afterAll(async () => { await mongoose.disconnect() await redisClient.quit() server.close() }) describe('/api/2024/traits', () => { it('should list traits', async () => { const res = await request(app).get('/api/2024/traits') expect(res.statusCode).toEqual(200) expect(res.body.results.length).not.toEqual(0) }) describe('with name query', () => { it('returns the named object', async () => { const indexRes = await request(app).get('/api/2024/traits') const name = indexRes.body.results[1].name const res = await request(app).get(`/api/2024/traits?name=${name}`) expect(res.statusCode).toEqual(200) expect(res.body.results[0].name).toEqual(name) }) it('is case insensitive', async () => { const indexRes = await request(app).get('/api/2024/traits') const name = indexRes.body.results[1].name const queryName = name.toLowerCase() const res = await request(app).get(`/api/2024/traits?name=${queryName}`) expect(res.statusCode).toEqual(200) expect(res.body.results[0].name).toEqual(name) }) }) describe('/api/2024/traits/:index', () => { it('should return one object', async () => { const indexRes = await request(app).get('/api/2024/traits') const index = indexRes.body.results[0].index const showRes = await request(app).get(`/api/2024/traits/${index}`) expect(showRes.statusCode).toEqual(200) expect(showRes.body.index).toEqual(index) }) describe('with an invalid index', () => { it('should return 404', async () => { const showRes = await request(app).get('/api/2024/traits/invalid-index') expect(showRes.statusCode).toEqual(404) }) }) }) }) ================================================ FILE: src/tests/integration/api/2024/weaponMasteryProperty.itest.ts ================================================ import { Application } from 'express' import mongoose from 'mongoose' import request from 'supertest' import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from 'vitest' import createApp from '@/server' import { mongodbUri, redisClient } from '@/util' let app: Application let server: any afterEach(() => { vi.clearAllMocks() }) beforeAll(async () => { await mongoose.connect(mongodbUri) await redisClient.connect() app = await createApp() server = app.listen() }) afterAll(async () => { await mongoose.disconnect() await redisClient.quit() server.close() }) describe('/api/2024/weapon-mastery-properties', () => { it('should list weapon mastery properties', async () => { const res = await request(app).get('/api/2024/weapon-mastery-properties') expect(res.statusCode).toEqual(200) expect(res.body.results.length).not.toEqual(0) }) describe('with name query', () => { it('returns the named object', async () => { const indexRes = await request(app).get('/api/2024/weapon-mastery-properties') const name = indexRes.body.results[1].name const res = await request(app).get(`/api/2024/weapon-mastery-properties?name=${name}`) expect(res.statusCode).toEqual(200) expect(res.body.results[0].name).toEqual(name) }) it('is case insensitive', async () => { const indexRes = await request(app).get('/api/2024/weapon-mastery-properties') const name = indexRes.body.results[1].name const queryName = name.toLowerCase() const res = await request(app).get(`/api/2024/weapon-mastery-properties?name=${queryName}`) expect(res.statusCode).toEqual(200) expect(res.body.results[0].name).toEqual(name) }) }) describe('/api/2024/weapon-mastery-properties/:index', () => { it('should return one object', async () => { const indexRes = await request(app).get('/api/2024/weapon-mastery-properties') const index = indexRes.body.results[0].index const showRes = await request(app).get(`/api/2024/weapon-mastery-properties/${index}`) expect(showRes.statusCode).toEqual(200) expect(showRes.body.index).toEqual(index) }) describe('with an invalid index', () => { it('should return 404', async () => { const invalidIndex = 'invalid-index' const showRes = await request(app).get( `/api/2024/weapon-mastery-properties/${invalidIndex}` ) expect(showRes.statusCode).toEqual(404) }) }) }) }) ================================================ FILE: src/tests/integration/api/2024/weaponProperty.itest.ts ================================================ import { Application } from 'express' import mongoose from 'mongoose' import request from 'supertest' import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from 'vitest' import createApp from '@/server' import { mongodbUri, redisClient } from '@/util' let app: Application let server: any afterEach(() => { vi.clearAllMocks() }) beforeAll(async () => { await mongoose.connect(mongodbUri) await redisClient.connect() app = await createApp() server = app.listen() }) afterAll(async () => { await mongoose.disconnect() await redisClient.quit() server.close() }) describe('/api/2024/weapon-properties', () => { it('should list weapon properties', async () => { const res = await request(app).get('/api/2024/weapon-properties') expect(res.statusCode).toEqual(200) expect(res.body.results.length).not.toEqual(0) }) describe('with name query', () => { it('returns the named object', async () => { const indexRes = await request(app).get('/api/2024/weapon-properties') const name = indexRes.body.results[1].name const res = await request(app).get(`/api/2024/weapon-properties?name=${name}`) expect(res.statusCode).toEqual(200) expect(res.body.results[0].name).toEqual(name) }) it('is case insensitive', async () => { const indexRes = await request(app).get('/api/2024/weapon-properties') const name = indexRes.body.results[1].name const queryName = name.toLowerCase() const res = await request(app).get(`/api/2024/weapon-properties?name=${queryName}`) expect(res.statusCode).toEqual(200) expect(res.body.results[0].name).toEqual(name) }) }) describe('/api/2024/weapon-properties/:index', () => { it('should return one object', async () => { const indexRes = await request(app).get('/api/2024/weapon-properties') const index = indexRes.body.results[0].index const showRes = await request(app).get(`/api/2024/weapon-properties/${index}`) expect(showRes.statusCode).toEqual(200) expect(showRes.body.index).toEqual(index) }) describe('with an invalid index', () => { it('should return 404', async () => { const invalidIndex = 'invalid-index' const showRes = await request(app).get(`/api/2024/weapon-properties/${invalidIndex}`) expect(showRes.statusCode).toEqual(404) }) }) }) }) ================================================ FILE: src/tests/integration/api/abilityScores.itest.ts ================================================ import { Application } from 'express' import mongoose from 'mongoose' import request from 'supertest' import { afterAll, afterEach, beforeAll, describe, it, vi } from 'vitest' import createApp from '@/server' import { mongodbUri, redisClient } from '@/util' let app: Application let server: any afterEach(() => { vi.clearAllMocks() }) beforeAll(async () => { await mongoose.connect(mongodbUri) await redisClient.connect() app = await createApp() server = app.listen() // Start the server and store the instance }) afterAll(async () => { await mongoose.disconnect() await redisClient.quit() server.close() }) describe('/api/ability-scores', () => { it('redirects to /api/2014/ability-scores', async () => { await request(app) .get('/api/ability-scores') .expect(301) .expect('Location', '/api/2014/ability-scores') }) it('redirects preserving query parameters', async () => { const name = 'CHA' await request(app) .get(`/api/ability-scores?name=${name}`) .expect(301) .expect('Location', `/api/2014/ability-scores?name=${name}`) }) it('redirects to /api/2014/ability-scores/{index}', async () => { const index = 'strength' await request(app) .get(`/api/ability-scores/${index}`) .expect(301) .expect('Location', `/api/2014/ability-scores/${index}`) }) }) ================================================ FILE: src/tests/integration/api/classes.itest.ts ================================================ import { Application } from 'express' import mongoose from 'mongoose' import request from 'supertest' import { afterAll, afterEach, beforeAll, describe, it, vi } from 'vitest' import createApp from '@/server' import { mongodbUri, redisClient } from '@/util' let app: Application let server: any afterEach(() => { vi.clearAllMocks() }) beforeAll(async () => { await mongoose.connect(mongodbUri) await redisClient.connect() app = await createApp() server = app.listen() // Start the server and store the instance }) afterAll(async () => { await mongoose.disconnect() await redisClient.quit() server.close() }) describe('/api/classes', () => { it('redirects to /api/2014/classes', async () => { await request(app).get('/api/classes').expect(301).expect('Location', '/api/2014/classes') }) it('redirects preserving query parameters', async () => { const name = 'Cleric' await request(app) .get(`/api/classes?name=${name}`) .expect(301) .expect('Location', `/api/2014/classes?name=${name}`) }) it('redirects to /api/2014/classes/{index}', async () => { const index = 'bard' await request(app) .get(`/api/classes/${index}`) .expect(301) .expect('Location', `/api/2014/classes/${index}`) }) }) ================================================ FILE: src/tests/integration/api/conditions.itest.ts ================================================ import { Application } from 'express' import mongoose from 'mongoose' import request from 'supertest' import { afterAll, afterEach, beforeAll, describe, it, vi } from 'vitest' import createApp from '@/server' import { mongodbUri, redisClient } from '@/util' let app: Application let server: any afterEach(() => { vi.clearAllMocks() }) beforeAll(async () => { await mongoose.connect(mongodbUri) await redisClient.connect() app = await createApp() server = app.listen() // Start the server and store the instance }) afterAll(async () => { await mongoose.disconnect() await redisClient.quit() server.close() }) describe('/api/conditions', () => { it('redirects to /api/2014/conditions', async () => { await request(app).get('/api/conditions').expect(301).expect('Location', '/api/2014/conditions') }) it('redirects preserving query parameters', async () => { const name = 'Blinded' await request(app) .get(`/api/conditions?name=${name}`) .expect(301) .expect('Location', `/api/2014/conditions?name=${name}`) }) it('redirects to /api/2014/conditions/{index}', async () => { const index = 'charmed' await request(app) .get(`/api/conditions/${index}`) .expect(301) .expect('Location', `/api/2014/conditions/${index}`) }) }) ================================================ FILE: src/tests/integration/api/damageTypes.itest.ts ================================================ import { Application } from 'express' import mongoose from 'mongoose' import request from 'supertest' import { afterAll, afterEach, beforeAll, describe, it, vi } from 'vitest' import createApp from '@/server' import { mongodbUri, redisClient } from '@/util' let app: Application let server: any afterEach(() => { vi.clearAllMocks() }) beforeAll(async () => { await mongoose.connect(mongodbUri) await redisClient.connect() app = await createApp() server = app.listen() // Start the server and store the instance }) afterAll(async () => { await mongoose.disconnect() await redisClient.quit() server.close() }) describe('/api/damage-types', () => { it('redirects to /api/2014/damage-types', async () => { await request(app) .get('/api/damage-types') .expect(301) .expect('Location', '/api/2014/damage-types') }) it('redirects preserving query parameters', async () => { const name = 'Acid' await request(app) .get(`/api/damage-types?name=${name}`) .expect(301) .expect('Location', `/api/2014/damage-types?name=${name}`) }) it('redirects to /api/2014/damage-types/{index}', async () => { const index = 'cold' await request(app) .get(`/api/damage-types/${index}`) .expect(301) .expect('Location', `/api/2014/damage-types/${index}`) }) }) ================================================ FILE: src/tests/integration/api/equipment.itest.ts ================================================ import { Application } from 'express' import mongoose from 'mongoose' import request from 'supertest' import { afterAll, afterEach, beforeAll, describe, it, vi } from 'vitest' import createApp from '@/server' import { mongodbUri, redisClient } from '@/util' let app: Application let server: any afterEach(() => { vi.clearAllMocks() }) beforeAll(async () => { await mongoose.connect(mongodbUri) await redisClient.connect() app = await createApp() server = app.listen() // Start the server and store the instance }) afterAll(async () => { await mongoose.disconnect() await redisClient.quit() server.close() }) describe('/api/equipment', () => { it('redirects to /api/2014/equipment', async () => { await request(app).get('/api/equipment').expect(301).expect('Location', '/api/2014/equipment') }) it('redirects preserving query parameters', async () => { const name = 'Abacus' await request(app) .get(`/api/equipment?name=${name}`) .expect(301) .expect('Location', `/api/2014/equipment?name=${name}`) }) it('redirects to /api/2014/equipment/{index}', async () => { const index = 'acid-vial' await request(app) .get(`/api/equipment/${index}`) .expect(301) .expect('Location', `/api/2014/equipment/${index}`) }) }) ================================================ FILE: src/tests/integration/api/equipmentCategories.itest.ts ================================================ import { Application } from 'express' import mongoose from 'mongoose' import request from 'supertest' import { afterAll, afterEach, beforeAll, describe, it, vi } from 'vitest' import createApp from '@/server' import { mongodbUri, redisClient } from '@/util' let app: Application let server: any afterEach(() => { vi.clearAllMocks() }) beforeAll(async () => { await mongoose.connect(mongodbUri) await redisClient.connect() app = await createApp() server = app.listen() // Start the server and store the instance }) afterAll(async () => { await mongoose.disconnect() await redisClient.quit() server.close() }) describe('/api/equipment-categories', () => { it('redirects to /api/2014/equipment-categories', async () => { await request(app) .get('/api/equipment-categories') .expect(301) .expect('Location', '/api/2014/equipment-categories') }) it('redirects preserving query parameters', async () => { const name = 'Adventuring%20Gear' await request(app) .get(`/api/equipment-categories?name=${name}`) .expect(301) .expect('Location', `/api/2014/equipment-categories?name=${name}`) }) it('redirects to /api/2014/equipment-categories/{index}', async () => { const index = 'ammunition' await request(app) .get(`/api/equipment-categories/${index}`) .expect(301) .expect('Location', `/api/2014/equipment-categories/${index}`) }) }) ================================================ FILE: src/tests/integration/api/feats.itest.ts ================================================ import { Application } from 'express' import mongoose from 'mongoose' import request from 'supertest' import { afterAll, afterEach, beforeAll, describe, it, vi } from 'vitest' import createApp from '@/server' import { mongodbUri, redisClient } from '@/util' let app: Application let server: any afterEach(() => { vi.clearAllMocks() }) beforeAll(async () => { await mongoose.connect(mongodbUri) await redisClient.connect() app = await createApp() server = app.listen() // Start the server and store the instance }) afterAll(async () => { await mongoose.disconnect() await redisClient.quit() server.close() }) describe('/api/feats', () => { it('redirects to /api/2014/feats', async () => { await request(app).get('/api/feats').expect(301).expect('Location', '/api/2014/feats') }) it('redirects preserving query parameters', async () => { const name = 'Grappler' await request(app) .get(`/api/feats?name=${name}`) .expect(301) .expect('Location', `/api/2014/feats?name=${name}`) }) it('redirects to /api/2014/feats/{index}', async () => { const index = 'grappler' await request(app) .get(`/api/feats/${index}`) .expect(301) .expect('Location', `/api/2014/feats/${index}`) }) }) ================================================ FILE: src/tests/integration/api/features.itest.ts ================================================ import { Application } from 'express' import mongoose from 'mongoose' import request from 'supertest' import { afterAll, afterEach, beforeAll, describe, it, vi } from 'vitest' import createApp from '@/server' import { mongodbUri, redisClient } from '@/util' let app: Application let server: any afterEach(() => { vi.clearAllMocks() }) beforeAll(async () => { await mongoose.connect(mongodbUri) await redisClient.connect() app = await createApp() server = app.listen() // Start the server and store the instance }) afterAll(async () => { await mongoose.disconnect() await redisClient.quit() server.close() }) describe('/api/features', () => { it('redirects to /api/2014/features', async () => { await request(app).get('/api/features').expect(301).expect('Location', '/api/2014/features') }) it('redirects preserving query parameters', async () => { const name = 'Action%20Surge' await request(app) .get(`/api/features?name=${name}`) .expect(301) .expect('Location', `/api/2014/features?name=${name}`) }) it('redirects to /api/2014/features/{index}', async () => { const index = 'arcane-recovery' await request(app) .get(`/api/features/${index}`) .expect(301) .expect('Location', `/api/2014/features/${index}`) }) }) ================================================ FILE: src/tests/integration/api/languages.itest.ts ================================================ import { Application } from 'express' import mongoose from 'mongoose' import request from 'supertest' import { afterAll, afterEach, beforeAll, describe, it, vi } from 'vitest' import createApp from '@/server' import { mongodbUri, redisClient } from '@/util' let app: Application let server: any afterEach(() => { vi.clearAllMocks() }) beforeAll(async () => { await mongoose.connect(mongodbUri) await redisClient.connect() app = await createApp() server = app.listen() // Start the server and store the instance }) afterAll(async () => { await mongoose.disconnect() await redisClient.quit() server.close() }) describe('/api/languages', () => { it('redirects to /api/2014/languages', async () => { await request(app).get('/api/languages').expect(301).expect('Location', '/api/2014/languages') }) it('redirects preserving query parameters', async () => { const name = 'Abyssal' await request(app) .get(`/api/languages?name=${name}`) .expect(301) .expect('Location', `/api/2014/languages?name=${name}`) }) it('redirects to /api/2014/languages/{index}', async () => { const index = 'celestial' await request(app) .get(`/api/languages/${index}`) .expect(301) .expect('Location', `/api/2014/languages/${index}`) }) }) ================================================ FILE: src/tests/integration/api/magicItems.itest.ts ================================================ import { Application } from 'express' import mongoose from 'mongoose' import request from 'supertest' import { afterAll, afterEach, beforeAll, describe, it, vi } from 'vitest' import createApp from '@/server' import { mongodbUri, redisClient } from '@/util' let app: Application let server: any afterEach(() => { vi.clearAllMocks() }) beforeAll(async () => { await mongoose.connect(mongodbUri) await redisClient.connect() app = await createApp() server = app.listen() // Start the server and store the instance }) afterAll(async () => { await mongoose.disconnect() await redisClient.quit() server.close() }) describe('/api/magic-items', () => { it('redirects to /api/2014/magic-items', async () => { await request(app) .get('/api/magic-items') .expect(301) .expect('Location', '/api/2014/magic-items') }) it('redirects preserving query parameters', async () => { const name = 'Adamantine%20Armor' await request(app) .get(`/api/magic-items?name=${name}`) .expect(301) .expect('Location', `/api/2014/magic-items?name=${name}`) }) it('redirects to /api/2014/magic-items/{index}', async () => { const index = 'ammunition' await request(app) .get(`/api/magic-items/${index}`) .expect(301) .expect('Location', `/api/2014/magic-items/${index}`) }) }) ================================================ FILE: src/tests/integration/api/magicSchools.itest.ts ================================================ import { Application } from 'express' import mongoose from 'mongoose' import request from 'supertest' import { afterAll, afterEach, beforeAll, describe, it, vi } from 'vitest' import createApp from '@/server' import { mongodbUri, redisClient } from '@/util' let app: Application let server: any afterEach(() => { vi.clearAllMocks() }) beforeAll(async () => { await mongoose.connect(mongodbUri) await redisClient.connect() app = await createApp() server = app.listen() // Start the server and store the instance }) afterAll(async () => { await mongoose.disconnect() await redisClient.quit() server.close() }) describe('/api/magic-schools', () => { it('redirects to /api/2014/magic-schools', async () => { await request(app) .get('/api/magic-schools') .expect(301) .expect('Location', '/api/2014/magic-schools') }) it('redirects preserving query parameters', async () => { const name = 'Abjuration' await request(app) .get(`/api/magic-schools?name=${name}`) .expect(301) .expect('Location', `/api/2014/magic-schools?name=${name}`) }) it('redirects to /api/2014/magic-schools/{index}', async () => { const index = 'conjuration' await request(app) .get(`/api/magic-schools/${index}`) .expect(301) .expect('Location', `/api/2014/magic-schools/${index}`) }) }) ================================================ FILE: src/tests/integration/api/monsters.itest.ts ================================================ import { Application } from 'express' import mongoose from 'mongoose' import request from 'supertest' import { afterAll, afterEach, beforeAll, describe, it, vi } from 'vitest' import createApp from '@/server' import { mongodbUri, redisClient } from '@/util' let app: Application let server: any afterEach(() => { vi.clearAllMocks() }) beforeAll(async () => { await mongoose.connect(mongodbUri) await redisClient.connect() app = await createApp() server = app.listen() // Start the server and store the instance }) afterAll(async () => { await mongoose.disconnect() await redisClient.quit() server.close() }) describe('/api/monsters', () => { it('redirects to /api/2014/monsters', async () => { await request(app).get('/api/monsters').expect(301).expect('Location', '/api/2014/monsters') }) it('redirects preserving query parameters', async () => { const name = 'Aboleth' await request(app) .get(`/api/monsters?name=${name}`) .expect(301) .expect('Location', `/api/2014/monsters?name=${name}`) }) it('redirects to /api/2014/monsters/{index}', async () => { const index = 'acolyte' await request(app) .get(`/api/monsters/${index}`) .expect(301) .expect('Location', `/api/2014/monsters/${index}`) }) }) ================================================ FILE: src/tests/integration/api/proficiencies.itest.ts ================================================ import { Application } from 'express' import mongoose from 'mongoose' import request from 'supertest' import { afterAll, afterEach, beforeAll, describe, it, vi } from 'vitest' import createApp from '@/server' import { mongodbUri, redisClient } from '@/util' let app: Application let server: any afterEach(() => { vi.clearAllMocks() }) beforeAll(async () => { await mongoose.connect(mongodbUri) await redisClient.connect() app = await createApp() server = app.listen() // Start the server and store the instance }) afterAll(async () => { await mongoose.disconnect() await redisClient.quit() server.close() }) describe('/api/proficiencies', () => { it('redirects to /api/2014/proficiencies', async () => { await request(app) .get('/api/proficiencies') .expect(301) .expect('Location', '/api/2014/proficiencies') }) it('redirects preserving query parameters', async () => { const name = 'Bagpipes' await request(app) .get(`/api/proficiencies?name=${name}`) .expect(301) .expect('Location', `/api/2014/proficiencies?name=${name}`) }) it('redirects to /api/2014/proficiencies/{index}', async () => { const index = 'blowguns' await request(app) .get(`/api/proficiencies/${index}`) .expect(301) .expect('Location', `/api/2014/proficiencies/${index}`) }) }) ================================================ FILE: src/tests/integration/api/races.itest.ts ================================================ import { Application } from 'express' import mongoose from 'mongoose' import request from 'supertest' import { afterAll, afterEach, beforeAll, describe, it, vi } from 'vitest' import createApp from '@/server' import { mongodbUri, redisClient } from '@/util' let app: Application let server: any afterEach(() => { vi.clearAllMocks() }) beforeAll(async () => { await mongoose.connect(mongodbUri) await redisClient.connect() app = await createApp() server = app.listen() // Start the server and store the instance }) afterAll(async () => { await mongoose.disconnect() await redisClient.quit() server.close() }) describe('/api/races', () => { it('redirects to /api/2014/races', async () => { await request(app).get('/api/races').expect(301).expect('Location', '/api/2014/races') }) it('redirects preserving query parameters', async () => { const name = 'Dragonborn' await request(app) .get(`/api/races?name=${name}`) .expect(301) .expect('Location', `/api/2014/races?name=${name}`) }) it('redirects to /api/2014/races/{index}', async () => { const index = 'dwarf' await request(app) .get(`/api/races/${index}`) .expect(301) .expect('Location', `/api/2014/races/${index}`) }) }) ================================================ FILE: src/tests/integration/api/ruleSections.itest.ts ================================================ import { Application } from 'express' import mongoose from 'mongoose' import request from 'supertest' import { afterAll, afterEach, beforeAll, describe, it, vi } from 'vitest' import createApp from '@/server' import { mongodbUri, redisClient } from '@/util' let app: Application let server: any afterEach(() => { vi.clearAllMocks() }) beforeAll(async () => { await mongoose.connect(mongodbUri) await redisClient.connect() app = await createApp() server = app.listen() // Start the server and store the instance }) afterAll(async () => { await mongoose.disconnect() await redisClient.quit() server.close() }) describe('/api/rule-sections', () => { it('redirects to /api/2014/rule-sections', async () => { await request(app) .get('/api/rule-sections') .expect(301) .expect('Location', '/api/2014/rule-sections') }) it('redirects preserving query parameters', async () => { const name = 'Ability%20Checks' await request(app) .get(`/api/rule-sections?name=${name}`) .expect(301) .expect('Location', `/api/2014/rule-sections?name=${name}`) }) it('redirects to /api/2014/rule-sections/{index}', async () => { const index = 'actions-in-combat' await request(app) .get(`/api/rule-sections/${index}`) .expect(301) .expect('Location', `/api/2014/rule-sections/${index}`) }) }) ================================================ FILE: src/tests/integration/api/rules.itest.ts ================================================ import { Application } from 'express' import mongoose from 'mongoose' import request from 'supertest' import { afterAll, afterEach, beforeAll, describe, it, vi } from 'vitest' import createApp from '@/server' import { mongodbUri, redisClient } from '@/util' let app: Application let server: any afterEach(() => { vi.clearAllMocks() }) beforeAll(async () => { await mongoose.connect(mongodbUri) await redisClient.connect() app = await createApp() server = app.listen() // Start the server and store the instance }) afterAll(async () => { await mongoose.disconnect() await redisClient.quit() server.close() }) describe('/api/rules', () => { it('redirects to /api/2014/rules', async () => { await request(app).get('/api/rules').expect(301).expect('Location', '/api/2014/rules') }) it('redirects preserving query parameters', async () => { const name = 'Adventuring' await request(app) .get(`/api/rules?name=${name}`) .expect(301) .expect('Location', `/api/2014/rules?name=${name}`) }) it('redirects to /api/2014/rules/{index}', async () => { const index = 'appendix' await request(app) .get(`/api/rules/${index}`) .expect(301) .expect('Location', `/api/2014/rules/${index}`) }) }) ================================================ FILE: src/tests/integration/api/skills.itest.ts ================================================ import { Application } from 'express' import mongoose from 'mongoose' import request from 'supertest' import { afterAll, afterEach, beforeAll, describe, it, vi } from 'vitest' import createApp from '@/server' import { mongodbUri, redisClient } from '@/util' let app: Application let server: any afterEach(() => { vi.clearAllMocks() }) beforeAll(async () => { await mongoose.connect(mongodbUri) await redisClient.connect() app = await createApp() server = app.listen() // Start the server and store the instance }) afterAll(async () => { await mongoose.disconnect() await redisClient.quit() server.close() }) describe('/api/skills', () => { it('redirects to /api/2014/skills', async () => { await request(app).get('/api/skills').expect(301).expect('Location', '/api/2014/skills') }) it('redirects preserving query parameters', async () => { const name = 'Acrobatics' await request(app) .get(`/api/skills?name=${name}`) .expect(301) .expect('Location', `/api/2014/skills?name=${name}`) }) it('redirects to /api/2014/skills/{index}', async () => { const index = 'arcana' await request(app) .get(`/api/skills/${index}`) .expect(301) .expect('Location', `/api/2014/skills/${index}`) }) }) ================================================ FILE: src/tests/integration/api/spells.itest.ts ================================================ import { Application } from 'express' import mongoose from 'mongoose' import request from 'supertest' import { afterAll, afterEach, beforeAll, describe, it, vi } from 'vitest' import createApp from '@/server' import { mongodbUri, redisClient } from '@/util' let app: Application let server: any afterEach(() => { vi.clearAllMocks() }) beforeAll(async () => { await mongoose.connect(mongodbUri) await redisClient.connect() app = await createApp() server = app.listen() // Start the server and store the instance }) afterAll(async () => { await mongoose.disconnect() await redisClient.quit() server.close() }) describe('/api/spells', () => { it('redirects to /api/2014/spells', async () => { await request(app).get('/api/spells').expect(301).expect('Location', '/api/2014/spells') }) it('redirects preserving query parameters', async () => { const name = 'Acid%20Arrow' await request(app) .get(`/api/spells?name=${name}`) .expect(301) .expect('Location', `/api/2014/spells?name=${name}`) }) it('redirects to /api/2014/spells/{index}', async () => { const index = 'aid' await request(app) .get(`/api/spells/${index}`) .expect(301) .expect('Location', `/api/2014/spells/${index}`) }) }) ================================================ FILE: src/tests/integration/api/subclasses.itest.ts ================================================ import { Application } from 'express' import mongoose from 'mongoose' import request from 'supertest' import { afterAll, afterEach, beforeAll, describe, it, vi } from 'vitest' import createApp from '@/server' import { mongodbUri, redisClient } from '@/util' let app: Application let server: any afterEach(() => { vi.clearAllMocks() }) beforeAll(async () => { await mongoose.connect(mongodbUri) await redisClient.connect() app = await createApp() server = app.listen() // Start the server and store the instance }) afterAll(async () => { await mongoose.disconnect() await redisClient.quit() server.close() }) describe('/api/subclasses', () => { it('redirects to /api/2014/subclasses', async () => { await request(app).get('/api/subclasses').expect(301).expect('Location', '/api/2014/subclasses') }) it('redirects preserving query parameters', async () => { const name = 'Berserker' await request(app) .get(`/api/subclasses?name=${name}`) .expect(301) .expect('Location', `/api/2014/subclasses?name=${name}`) }) it('redirects to /api/2014/subclasses/{index}', async () => { const index = 'Champion' await request(app) .get(`/api/subclasses/${index}`) .expect(301) .expect('Location', `/api/2014/subclasses/${index}`) }) }) ================================================ FILE: src/tests/integration/api/subraces.itest.ts ================================================ import { Application } from 'express' import mongoose from 'mongoose' import request from 'supertest' import { afterAll, afterEach, beforeAll, describe, it, vi } from 'vitest' import createApp from '@/server' import { mongodbUri, redisClient } from '@/util' let app: Application let server: any afterEach(() => { vi.clearAllMocks() }) beforeAll(async () => { await mongoose.connect(mongodbUri) await redisClient.connect() app = await createApp() server = app.listen() // Start the server and store the instance }) afterAll(async () => { await mongoose.disconnect() await redisClient.quit() server.close() }) describe('/api/subraces', () => { it('redirects to /api/2014/subraces', async () => { await request(app).get('/api/subraces').expect(301).expect('Location', '/api/2014/subraces') }) it('redirects preserving query parameters', async () => { const name = 'High%20Elf' await request(app) .get(`/api/subraces?name=${name}`) .expect(301) .expect('Location', `/api/2014/subraces?name=${name}`) }) it('redirects to /api/2014/subraces/{index}', async () => { const index = 'hill-dwarf' await request(app) .get(`/api/subraces/${index}`) .expect(301) .expect('Location', `/api/2014/subraces/${index}`) }) }) ================================================ FILE: src/tests/integration/api/traits.itest.ts ================================================ import { Application } from 'express' import mongoose from 'mongoose' import request from 'supertest' import { afterAll, afterEach, beforeAll, describe, it, vi } from 'vitest' import createApp from '@/server' import { mongodbUri, redisClient } from '@/util' let app: Application let server: any afterEach(() => { vi.clearAllMocks() }) beforeAll(async () => { await mongoose.connect(mongodbUri) await redisClient.connect() app = await createApp() server = app.listen() // Start the server and store the instance }) afterAll(async () => { await mongoose.disconnect() await redisClient.quit() server.close() }) describe('/api/traits', () => { it('redirects to /api/2014/traits', async () => { await request(app).get('/api/traits').expect(301).expect('Location', '/api/2014/traits') }) it('redirects preserving query parameters', async () => { const name = 'Brave' await request(app) .get(`/api/traits?name=${name}`) .expect(301) .expect('Location', `/api/2014/traits?name=${name}`) }) it('redirects to /api/2014/traits/{index}', async () => { const index = 'darkvision' await request(app) .get(`/api/traits/${index}`) .expect(301) .expect('Location', `/api/2014/traits/${index}`) }) }) ================================================ FILE: src/tests/integration/api/weaponProperties.itest.ts ================================================ import { Application } from 'express' import mongoose from 'mongoose' import request from 'supertest' import { afterAll, afterEach, beforeAll, describe, it, vi } from 'vitest' import createApp from '@/server' import { mongodbUri, redisClient } from '@/util' let app: Application let server: any afterEach(() => { vi.clearAllMocks() }) beforeAll(async () => { await mongoose.connect(mongodbUri) await redisClient.connect() app = await createApp() server = app.listen() // Start the server and store the instance }) afterAll(async () => { await mongoose.disconnect() await redisClient.quit() server.close() }) describe('/api/weapon-properties', () => { it('redirects to /api/2014/weapon-properties', async () => { await request(app) .get('/api/weapon-properties') .expect(301) .expect('Location', '/api/2014/weapon-properties') }) it('redirects preserving query parameters', async () => { const name = 'Ammunition' await request(app) .get(`/api/weapon-properties?name=${name}`) .expect(301) .expect('Location', `/api/2014/weapon-properties?name=${name}`) }) it('redirects to /api/2014/weapon-properties/{index}', async () => { const index = 'finesse' await request(app) .get(`/api/weapon-properties/${index}`) .expect(301) .expect('Location', `/api/2014/weapon-properties/${index}`) }) }) ================================================ FILE: src/tests/integration/server.itest.ts ================================================ import { Application } from 'express' import mongoose from 'mongoose' import request from 'supertest' import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from 'vitest' import createApp from '@/server' import { mongodbUri, redisClient } from '@/util' let app: Application let server: any afterEach(() => { vi.clearAllMocks() }) beforeAll(async () => { await mongoose.connect(mongodbUri) await redisClient.connect() app = await createApp() server = app.listen() // Start the server and store the instance }) afterAll(async () => { await mongoose.disconnect() await redisClient.quit() server.close() }) describe('/', () => { it('should load the page', async () => { const res = await request(app).get('/') expect(res.statusCode).toEqual(200) }) }) describe('/docs', () => { it('should redirect', async () => { const res = await request(app).get('/docs') expect(res.statusCode).toEqual(302) }) }) describe('/bad-url', () => { it('404s', async () => { const res = await request(app).get('/bad-url') expect(res.statusCode).toEqual(404) }) }) describe('/api', () => { it('should redirect to /api/2014', async () => { await request(app).get('/api').expect(301).expect('Location', '/api/2014/') }) }) describe('/api/2014', () => { it('should list the endpoints', async () => { const res = await request(app).get('/api/2014') expect(res.statusCode).toEqual(200) expect(res.body).toHaveProperty('ability-scores') expect(res.body).not.toHaveProperty('levels') }) }) ================================================ FILE: src/tests/support/db.ts ================================================ import crypto from 'crypto' import mongoose, { type Model } from 'mongoose' import { afterAll, beforeAll, beforeEach, vi } from 'vitest' /** * Generates a unique MongoDB URI for test isolation. * Requires the TEST_MONGODB_URI_BASE environment variable to be set. * @param baseName - A descriptive name for the test suite (e.g., 'abilityscore'). * @returns A unique MongoDB URI string. */ export function generateUniqueDbUri(baseName: string): string { const baseUri = process.env.TEST_MONGODB_URI_BASE if (baseUri === undefined) { throw new Error( 'TEST_MONGODB_URI_BASE environment variable not set. Ensure global setup providing this variable ran before tests.' ) } // Ensure baseName is filesystem-friendly if used directly in DB name const safeBaseName = baseName.replace(/[^a-zA-Z0-9]/g, '_') const uniqueSuffix = crypto.randomBytes(4).toString('hex') const dbName = `test_${safeBaseName}_${uniqueSuffix}` return `${baseUri}${dbName}` // Assumes baseUri ends with '/' or is just the host part } /** * Registers vitest hooks to connect to a unique, isolated MongoDB database before all tests in a suite. * @param uri - The unique MongoDB URI generated by generateUniqueDbUri. */ export function setupIsolatedDatabase(uri: string): void { beforeAll(async () => { try { await mongoose.connect(uri) } catch (error) { console.error(`Failed to connect to MongoDB at ${uri}`, error) // Re-throw the error to fail the test suite explicitly throw new Error( `Database connection failed: ${error instanceof Error ? error.message : String(error)}`, { cause: error } ) } }) } /** * Registers vitest hooks to drop the database and disconnect Mongoose after all tests in a suite. */ export function teardownIsolatedDatabase(): void { afterAll(async () => { if (mongoose.connection.readyState === 1) { try { if (mongoose.connection.db) { await mongoose.connection.db.dropDatabase() } } catch (err) { console.error(`Error dropping database ${mongoose.connection.name}:`, err) // Decide if you want to throw here or just log } finally { await mongoose.disconnect() } } else { // Ensure disconnection even if connection failed or was already closed await mongoose.disconnect() } }) } /** * Registers vitest hooks to clear mocks and delete all documents from a specific model before each test. * @param model - The Mongoose model to clean up before each test. */ export function setupModelCleanup(model: Model): void { beforeEach(async () => { vi.clearAllMocks() try { // Ensure connection is ready before attempting cleanup if (mongoose.connection.readyState === 1) { await model.deleteMany({}) } else { console.warn(`Skipping cleanup for ${model.modelName}: Mongoose not connected.`) } } catch (error) { console.error(`Error cleaning up model ${model.modelName}:`, error) // Decide if you want to throw here or just log throw new Error( `Model cleanup failed: ${error instanceof Error ? error.message : String(error)}`, { cause: error } ) } }) } ================================================ FILE: src/tests/support/index.ts ================================================ export { mockNext } from './requestHelpers' ================================================ FILE: src/tests/support/requestHelpers.ts ================================================ import { vi } from 'vitest' export const mockNext = vi.fn() ================================================ FILE: src/tests/support/types.d.ts ================================================ import { vi } from 'vitest' export type MockResponse = { status?: vi.Mock json?: vi.Mock } ================================================ FILE: src/tests/util/data.test.ts ================================================ import { describe, expect, it } from 'vitest' import { ResourceList } from '@/util/data' describe('ResourceList', () => { it('returns a constructed hash from list', () => { const data = [ { index: 'test1', name: 'Test 1', url: '/made/up/url/test1' }, { index: 'test2', name: 'Test 2', url: '/made/up/url/test2' }, { index: 'test2', name: 'Test 2', url: '/made/up/url/test2' } ] const resource = ResourceList(data) expect(resource.count).toEqual(data.length) expect(resource.results).toEqual(data) }) }) ================================================ FILE: src/tests/vitest.setup.ts ================================================ import { vi } from 'vitest' // Mock the entire module that exports redisClient vi.mock('@/util', async (importOriginal) => { // Get the actual module contents const actual = await importOriginal() // Create mock functions for the redisClient methods used in the app const mockRedisClient = { get: vi.fn().mockResolvedValue(null), // Default mock: cache miss set: vi.fn().mockResolvedValue('OK'), // Default mock: successful set del: vi.fn().mockResolvedValue(1), // Default mock: successful delete on: vi.fn(), // Mock for event listener registration // Add common commands used in start.ts or elsewhere // Use flushDb usually, but include flushAll if specifically used flushDb: vi.fn().mockResolvedValue('OK'), flushAll: vi.fn().mockResolvedValue('OK'), // Ensure connect/quit are mocked if used during startup/shutdown logic connect: vi.fn().mockResolvedValue(undefined), quit: vi.fn().mockResolvedValue(undefined), isOpen: true // Mock the state property/getter // Add mocks for any other redisClient methods your application uses // e.g., connect: vi.fn().mockResolvedValue(undefined), // e.g., quit: vi.fn().mockResolvedValue(undefined), // e.g., isOpen: true, // Or a getter mock: get isOpen() { return true; } } return { ...actual, // Keep all other exports from the original module redisClient: mockRedisClient // Replace redisClient with our mock } }) // You can add other global setup logic here if needed, like clearing mocks // afterEach(() => { // vi.clearAllMocks(); // }); ================================================ FILE: src/util/RedisClient.ts ================================================ import { createClient } from 'redis' import { redisUrl } from './environmentVariables' const isTls = redisUrl.match(/rediss:/) != null const parsedUrl = new URL(redisUrl) export default createClient({ url: redisUrl, socket: isTls ? { tls: true, host: parsedUrl.hostname, rejectUnauthorized: false } : undefined }) ================================================ FILE: src/util/awsS3Client.ts ================================================ import { S3Client, S3ClientConfig } from '@aws-sdk/client-s3' import { awsAccessKeyId, awsConfigEnv, awsRegion, awsSecretAccessKey } from '@/util/environmentVariables' const awsS3ClientConfigs = { prod: { region: awsRegion, credentials: { accessKeyId: awsAccessKeyId, secretAccessKey: awsSecretAccessKey } } as S3ClientConfig, // Assumes localstack is running and s3 is available at http://localhost:4566 // with the bucket dnd-5e-api-images created with a folder named 'monsters' // containing image files. localstack_dev: { region: 'us-east-1', credentials: { accessKeyId: 'test', secretAccessKey: 'test' }, endpoint: 'http://s3.localhost.localstack.cloud:4566', forcePathStyle: true } as S3ClientConfig } export default new S3Client(awsS3ClientConfigs[awsConfigEnv as keyof typeof awsS3ClientConfigs]) ================================================ FILE: src/util/data.ts ================================================ export const ResourceList = (data: any[]) => { return { count: data.length, results: data } } ================================================ FILE: src/util/environmentVariables.ts ================================================ const isTestEnv = process.env.NODE_ENV === 'test' const redisUrl = process.env.HEROKU_REDIS_YELLOW_URL ?? process.env.REDIS_URL ?? 'redis://localhost:6379' const bugsnagApiKey = process.env.BUGSNAG_API_KEY ?? null const mongodbUri = (isTestEnv ? process.env.TEST_MONGODB_URI : process.env.MONGODB_URI) ?? 'mongodb://localhost/5e-database' const awsConfigEnv = process.env.AWS_CONFIG_ENV ?? 'prod' const awsRegion = process.env.AWS_REGION ?? 'us-west-1' const awsAccessKeyId = process.env.AWS_ACCESS_KEY_ID ?? '' const awsSecretAccessKey = process.env.AWS_SECRET_ACCESS_KEY ?? '' export { awsAccessKeyId, awsConfigEnv, awsRegion, awsSecretAccessKey, bugsnagApiKey, mongodbUri, redisUrl } ================================================ FILE: src/util/index.ts ================================================ import awsS3Client from './awsS3Client' import { ResourceList } from './data' import { bugsnagApiKey, mongodbUri } from './environmentVariables' import prewarmCache from './prewarmCache' import redisClient from './RedisClient' import { escapeRegExp } from './regex' export { awsS3Client, bugsnagApiKey, escapeRegExp, mongodbUri, prewarmCache, redisClient, ResourceList } ================================================ FILE: src/util/modelOptions.ts ================================================ import { modelOptions, Severity } from '@typegoose/typegoose' /** * Helper function to recursively remove _id and __v fields. * @param obj The object or array to process. */ function removeInternalFields(obj: any): any { if (Array.isArray(obj)) { return obj.map(removeInternalFields) } else if (obj !== null && typeof obj === 'object') { // Mongoose documents might have a toObject method, use it if available const plainObject = typeof obj.toObject === 'function' ? obj.toObject() : obj // Create a new object to avoid modifying the original Mongoose document directly if necessary const newObj: any = {} for (const key in plainObject) { if (key !== '_id' && key !== '__v' && key !== 'id') { newObj[key] = removeInternalFields(plainObject[key]) } } return newObj } return obj // Return primitives/unhandled types as is } /** * Creates common Typegoose model options for SRD API models. * - Sets the collection name. * - Disables the default _id field. * - Enables timestamps (createdAt, updatedAt). * - Configures toJSON and toObject transforms to remove _id and __v recursively. * * @param collectionName The name of the MongoDB collection. * @returns A Typegoose ClassDecorator. */ export function srdModelOptions(collectionName: string): ClassDecorator { return modelOptions({ // It's often good practice to explicitly set allowMixed options: { allowMixed: Severity.ALLOW }, schemaOptions: { collection: collectionName, // Prevent Mongoose from managing the _id field directly _id: false, // Automatically add createdAt and updatedAt timestamps timestamps: true, // Modify the object when converting to JSON (e.g., for API responses) toJSON: { virtuals: true, // Ensure virtuals are included if you add any transform: (doc, ret) => { return removeInternalFields(ret) } }, // Also apply the same transform when converting to a plain object toObject: { virtuals: true, transform: (doc, ret) => { return removeInternalFields(ret) } } } }) } ================================================ FILE: src/util/prewarmCache.ts ================================================ import { getModelForClass } from '@typegoose/typegoose' import MagicItem from '@/models/2014/magicItem' import Monster from '@/models/2014/monster' import Rule from '@/models/2014/rule' import RuleSection from '@/models/2014/ruleSection' import Spell from '@/models/2014/spell' import { ResourceList } from './data' import redisClient from './RedisClient' type PrewarmData = { Schema: ReturnType endpoint: string } const prewarmCache = async () => { const toPrewarm: PrewarmData[] = [ { Schema: MagicItem, endpoint: '/api/2014/magic-items' }, { Schema: Spell, endpoint: '/api/2014/spells' }, { Schema: Monster, endpoint: '/api/2014/monsters' }, { Schema: Rule, endpoint: '/api/2014/rules' }, { Schema: RuleSection, endpoint: '/api/2014/rule-sections' } ] for (const element of toPrewarm) { const data = await element.Schema.find() .select({ index: 1, level: 1, name: 1, url: 1, _id: 0 }) .sort({ index: 'asc' }) const jsonData = ResourceList(data) await redisClient.set(element.endpoint, JSON.stringify(jsonData)) } } export default prewarmCache ================================================ FILE: src/util/regex.ts ================================================ export const escapeRegExp = (string: string) => { return string.toString().replace(/[.*+?^${}()|[\]\\]/g, '\\$&') // $& means the whole matched string } ================================================ FILE: tsconfig.json ================================================ { "compilerOptions": { "target": "ES2022", "module": "ESNext", "moduleResolution": "bundler", "allowJs": true, "outDir": "./dist", "allowSyntheticDefaultImports": true, "esModuleInterop": true, "forceConsistentCasingInFileNames": true, "strict": true, "skipLibCheck": true, "baseUrl": ".", "experimentalDecorators": true, "emitDecoratorMetadata": true, "paths": { "@/*": ["src/*"] } }, "include": [ "src/js/**/*.js", "src/**/*.ts", "tools/**/*.ts", "vitest.config.ts", "vitest.config.integration.ts", "eslint.config.js" ], "exclude": ["node_modules"], "ts-node": { "esm": true }, "typedocOptions": { "entryPoints": ["src/models/index.ts"], "out": "docs" }, "tsc-alias": { "resolveFullPaths": true } } ================================================ FILE: vitest.config.integration.ts ================================================ import path from 'path' import { defineConfig } from 'vitest/config' export default defineConfig({ test: { // Specify options specific to integration tests globals: true, // Keep or remove based on preference environment: 'node', include: ['src/tests/integration/**/*.itest.ts'], // Consider longer timeouts for integration tests involving DB/network/server startup testTimeout: 20000, hookTimeout: 30000 // Timeout for globalSetup/teardown // Any other specific integration test settings... }, // Copy essential options from base config, like resolve.alias resolve: { alias: { '@': path.resolve(__dirname, './src') } } }) ================================================ FILE: vitest.config.ts ================================================ import path from 'path' import { defineConfig } from 'vitest/config' export default defineConfig({ test: { globals: true, // Optional: Use Vitest global APIs (describe, it, etc.) without importing environment: 'node', // Specify Node.js environment globalSetup: ['./src/tests/controllers/globalSetup.ts'], // Path to the global setup file // maxConcurrency: 1, // Removed - Use DB isolation for parallelism // Optional: Increase timeouts if global setup takes longer setupFiles: ['./src/tests/vitest.setup.ts'], // testTimeout: 30000, // hookTimeout: 30000, deps: { // Remove optimizer settings related to factory-js optimizer: { ssr: { include: [] // Keep the structure but empty the array } } } }, resolve: { alias: { '@': path.resolve(__dirname, './src') } } })