Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .git-blame-ignore-revs
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Enable Biome formatter and format the codebase (bulk reformat — not a logic change)
4cae2a854b235f35cd80befa9b7450dc71f5dd97
2 changes: 1 addition & 1 deletion biome.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"ignoreUnknown": false,
"includes": ["**", "!build/**", "!.agent/**", "!src/evolution-types/**"]
},
"formatter": { "enabled": false },
"formatter": { "enabled": true, "indentStyle": "tab", "lineWidth": 100 },
"assist": { "enabled": false },
"linter": {
"enabled": true,
Expand Down
2 changes: 1 addition & 1 deletion commitlint.config.js
Original file line number Diff line number Diff line change
@@ -1 +1 @@
export default { extends: ['@commitlint/config-conventional'] };
export default { extends: ["@commitlint/config-conventional"] };
109 changes: 55 additions & 54 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,56 +1,57 @@
{
"name": "evolution-api",
"version": "1.0.50",
"type": "module",
"scripts": {
"test": "bun test",
"test:coverage": "bun test --coverage",
"dev": "bun run --watch src/index.ts",
"migration:cosmetics:run": "bun run src/scripts/run-cosmetics-migrations.ts",
"migration:cosmetics:revert": "bun run src/scripts/run-cosmetics-migrations.ts --revert",
"seed:cosmetics": "bun run src/scripts/seed-cosmetics.ts",
"lint": "biome lint .",
"lint:fix": "biome lint --write .",
"prepare": "if [ \"$NODE_ENV\" != \"production\" ]; then husky install; fi",
"build": "tsc"
},
"dependencies": {
"@elysiajs/bearer": "^1.4.4",
"@elysiajs/cors": "^1.4.2",
"@elysiajs/swagger": "^1.3.1",
"@sendgrid/mail": "^8.1.6",
"bcrypt": "^6.0.0",
"dotenv": "^17.2.3",
"elysia": "^1.4.29",
"elysia-rate-limit": "^4.5.0",
"jsonwebtoken": "^9.0.3",
"pg": "^8.18.0",
"pino": "^10.3.0",
"pino-pretty": "^13.1.3",
"resend": "6.4.2",
"typeorm": "^0.3.28"
},
"devDependencies": {
"@biomejs/biome": "2.5.0",
"@commitlint/cli": "^20.4.1",
"@commitlint/config-conventional": "^20.4.1",
"@faker-js/faker": "^10.2.0",
"@types/autocannon": "^7.12.7",
"@types/jsonwebtoken": "^9.0.10",
"autocannon": "^8.0.0",
"bun-types": "^1.3.8",
"husky": "^9.1.7",
"install": "^0.13.0",
"lint-staged": "^16.2.7",
"typescript": "5.9.3"
},
"overrides": {
"axios": "^1.18.0",
"form-data": "^4.0.6",
"follow-redirects": "^1.16.0"
},
"module": "src/index.js",
"lint-staged": {
"*.{js,ts}": "biome lint --write --no-errors-on-unmatched"
}
"name": "evolution-api",
"version": "1.0.50",
"type": "module",
"scripts": {
"test": "bun test",
"test:coverage": "bun test --coverage",
"dev": "bun run --watch src/index.ts",
"migration:cosmetics:run": "bun run src/scripts/run-cosmetics-migrations.ts",
"migration:cosmetics:revert": "bun run src/scripts/run-cosmetics-migrations.ts --revert",
"seed:cosmetics": "bun run src/scripts/seed-cosmetics.ts",
"lint": "biome check .",
"lint:fix": "biome check --write .",
"format": "biome format --write .",
"prepare": "if [ \"$NODE_ENV\" != \"production\" ]; then husky install; fi",
"build": "tsc"
},
"dependencies": {
"@elysiajs/bearer": "^1.4.4",
"@elysiajs/cors": "^1.4.2",
"@elysiajs/swagger": "^1.3.1",
"@sendgrid/mail": "^8.1.6",
"bcrypt": "^6.0.0",
"dotenv": "^17.2.3",
"elysia": "^1.4.29",
"elysia-rate-limit": "^4.5.0",
"jsonwebtoken": "^9.0.3",
"pg": "^8.18.0",
"pino": "^10.3.0",
"pino-pretty": "^13.1.3",
"resend": "6.4.2",
"typeorm": "^0.3.28"
},
"devDependencies": {
"@biomejs/biome": "2.5.0",
"@commitlint/cli": "^20.4.1",
"@commitlint/config-conventional": "^20.4.1",
"@faker-js/faker": "^10.2.0",
"@types/autocannon": "^7.12.7",
"@types/jsonwebtoken": "^9.0.10",
"autocannon": "^8.0.0",
"bun-types": "^1.3.8",
"husky": "^9.1.7",
"install": "^0.13.0",
"lint-staged": "^16.2.7",
"typescript": "5.9.3"
},
"overrides": {
"axios": "^1.18.0",
"form-data": "^4.0.6",
"follow-redirects": "^1.16.0"
},
"module": "src/index.js",
"lint-staged": {
"*.{js,ts}": "biome check --write --no-errors-on-unmatched"
}
}
43 changes: 29 additions & 14 deletions src/config/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,10 @@ export const config = {
sendgrid: {
apiKey: ensureEnvVariable(process.env.SENDGRID_API_KEY as string, "SENDGRID_API_KEY"),
from: ensureEnvVariable(process.env.SENDGRID_FROM_EMAIL as string, "SENDGRID_FROM_EMAIL"),
templateId: ensureEnvVariable(process.env.SENDGRID_TEMPLATE_ID as string, "SENDGRID_TEMPLATE_ID"),
templateId: ensureEnvVariable(
process.env.SENDGRID_TEMPLATE_ID as string,
"SENDGRID_TEMPLATE_ID",
),
},
resend: {
apiKey: ensureEnvVariable(process.env.RESEND_API_KEY as string, "RESEND_API_KEY"),
Expand All @@ -31,33 +34,45 @@ export const config = {
},
r2: {
accessKeyId: ensureEnvVariable(process.env.R2_ACCESS_KEY_ID as string, "R2_ACCESS_KEY_ID"),
secretAccessKey: ensureEnvVariable(process.env.R2_SECRET_ACCESS_KEY as string, "R2_SECRET_ACCESS_KEY"),
secretAccessKey: ensureEnvVariable(
process.env.R2_SECRET_ACCESS_KEY as string,
"R2_SECRET_ACCESS_KEY",
),
bucket: ensureEnvVariable(process.env.R2_BUCKET as string, "R2_BUCKET"),
endpoint: ensureEnvVariable(process.env.R2_ENDPOINT as string, "R2_ENDPOINT"),
signedUrlTtlSeconds: Number(process.env.R2_SIGNED_URL_TTL ?? "600"),
},
season: Number(ensureEnvVariable(process.env.SEASON as string, "SEASON")),
tournaments: {
apiUrl: ensureEnvVariable(process.env.TOURNAMENTS_API_URL as string, "TOURNAMENTS_API_URL"),
webhookUrl: ensureEnvVariable(process.env.TOURNAMENTS_WEBHOOK_URL as string, "TOURNAMENTS_WEBHOOK_URL"),
webhookUrl: ensureEnvVariable(
process.env.TOURNAMENTS_WEBHOOK_URL as string,
"TOURNAMENTS_WEBHOOK_URL",
),
},
passwordRecovery: {
defaultResetUrl: "https://evolutionygo.com/reset-account-password?token={token}",
frontends: [
{ origin: "https://evolutionygo.com", template: "https://evolutionygo.com/reset-account-password?token={token}" },
{ origin: "https://evoduel.com", template: "https://evoduel.com/#/reset-account-password?token={token}" },
{
origin: "https://evolutionygo.com",
template: "https://evolutionygo.com/reset-account-password?token={token}",
},
{
origin: "https://evoduel.com",
template: "https://evoduel.com/#/reset-account-password?token={token}",
},
...(isProduction
? []
: [
{
origin: "http://localhost:5173",
template: "http://localhost:5173/#/reset-account-password?token={token}",
},
{
origin: "http://localhost:4321",
template: "http://localhost:4321/reset-account-password?token={token}",
}
]),
{
origin: "http://localhost:5173",
template: "http://localhost:5173/#/reset-account-password?token={token}",
},
{
origin: "http://localhost:4321",
template: "http://localhost:4321/reset-account-password?token={token}",
},
]),
],
},
};
22 changes: 11 additions & 11 deletions src/migrations/1780873543822-create_cosmetics_tables.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,49 +7,49 @@ export class CreateCosmeticsTables1780873543822 implements MigrationInterface {
await queryRunner.query(`CREATE EXTENSION IF NOT EXISTS "uuid-ossp"`);

await queryRunner.query(
`CREATE TYPE "cosmetic_type_enum" AS ENUM('SLEEVE', 'PLAYMAT', 'CARD_BACK', 'AVATAR', 'SUMMON_EFFECT', 'MUSIC', 'TITLE')`
`CREATE TYPE "cosmetic_type_enum" AS ENUM('SLEEVE', 'PLAYMAT', 'CARD_BACK', 'AVATAR', 'SUMMON_EFFECT', 'MUSIC', 'TITLE')`,
);
await queryRunner.query(
`CREATE TYPE "cosmetic_tier_enum" AS ENUM('STANDARD', 'REGISTERED', 'DONOR')`
`CREATE TYPE "cosmetic_tier_enum" AS ENUM('STANDARD', 'REGISTERED', 'DONOR')`,
);
await queryRunner.query(`CREATE TYPE "grant_type_enum" AS ENUM('TIER', 'COSMETIC')`);
await queryRunner.query(
`CREATE TYPE "entitlement_source_enum" AS ENUM('REGISTRATION', 'DONATION', 'PURCHASE', 'CAMPAIGN')`
`CREATE TYPE "entitlement_source_enum" AS ENUM('REGISTRATION', 'DONATION', 'PURCHASE', 'CAMPAIGN')`,
);

await queryRunner.query(
`CREATE TABLE "cosmetics" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "type" "cosmetic_type_enum" NOT NULL, "tier" "cosmetic_tier_enum" NOT NULL, "asset_ref" character varying NOT NULL, "display_name" character varying NOT NULL, "active" boolean NOT NULL DEFAULT true, "created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updated_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), CONSTRAINT "PK_cosmetics_id" PRIMARY KEY ("id"))`
`CREATE TABLE "cosmetics" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "type" "cosmetic_type_enum" NOT NULL, "tier" "cosmetic_tier_enum" NOT NULL, "asset_ref" character varying NOT NULL, "display_name" character varying NOT NULL, "active" boolean NOT NULL DEFAULT true, "created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updated_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), CONSTRAINT "PK_cosmetics_id" PRIMARY KEY ("id"))`,
);

await queryRunner.query(
`CREATE TABLE "user_loadouts" ("user_id" character varying NOT NULL, "cosmetic_type" "cosmetic_type_enum" NOT NULL, "cosmetic_id" uuid NOT NULL, "updated_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), CONSTRAINT "PK_user_loadouts" PRIMARY KEY ("user_id", "cosmetic_type"))`
`CREATE TABLE "user_loadouts" ("user_id" character varying NOT NULL, "cosmetic_type" "cosmetic_type_enum" NOT NULL, "cosmetic_id" uuid NOT NULL, "updated_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), CONSTRAINT "PK_user_loadouts" PRIMARY KEY ("user_id", "cosmetic_type"))`,
);

await queryRunner.query(
`CREATE TABLE "entitlements" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "user_id" character varying NOT NULL, "grant_type" "grant_type_enum" NOT NULL, "grant_value" character varying NOT NULL, "source" "entitlement_source_enum" NOT NULL, "expires_at" TIMESTAMP WITH TIME ZONE, "created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), CONSTRAINT "PK_entitlements_id" PRIMARY KEY ("id"))`
`CREATE TABLE "entitlements" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "user_id" character varying NOT NULL, "grant_type" "grant_type_enum" NOT NULL, "grant_value" character varying NOT NULL, "source" "entitlement_source_enum" NOT NULL, "expires_at" TIMESTAMP WITH TIME ZONE, "created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), CONSTRAINT "PK_entitlements_id" PRIMARY KEY ("id"))`,
);

await queryRunner.query(
`CREATE INDEX "IDX_entitlements_user_id" ON "entitlements" ("user_id")`
`CREATE INDEX "IDX_entitlements_user_id" ON "entitlements" ("user_id")`,
);

// Foreign keys to the shared users table (users.id is varchar). Declared in raw
// SQL so the cosmetics DataSource never needs to map the shared UserProfileEntity.
await queryRunner.query(
`ALTER TABLE "user_loadouts" ADD CONSTRAINT "FK_user_loadouts_user" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE NO ACTION`
`ALTER TABLE "user_loadouts" ADD CONSTRAINT "FK_user_loadouts_user" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
);
await queryRunner.query(
`ALTER TABLE "user_loadouts" ADD CONSTRAINT "FK_user_loadouts_cosmetic" FOREIGN KEY ("cosmetic_id") REFERENCES "cosmetics"("id") ON DELETE RESTRICT ON UPDATE NO ACTION`
`ALTER TABLE "user_loadouts" ADD CONSTRAINT "FK_user_loadouts_cosmetic" FOREIGN KEY ("cosmetic_id") REFERENCES "cosmetics"("id") ON DELETE RESTRICT ON UPDATE NO ACTION`,
);
await queryRunner.query(
`ALTER TABLE "entitlements" ADD CONSTRAINT "FK_entitlements_user" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE NO ACTION`
`ALTER TABLE "entitlements" ADD CONSTRAINT "FK_entitlements_user" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
);
}

public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "entitlements" DROP CONSTRAINT "FK_entitlements_user"`);
await queryRunner.query(
`ALTER TABLE "user_loadouts" DROP CONSTRAINT "FK_user_loadouts_cosmetic"`
`ALTER TABLE "user_loadouts" DROP CONSTRAINT "FK_user_loadouts_cosmetic"`,
);
await queryRunner.query(`ALTER TABLE "user_loadouts" DROP CONSTRAINT "FK_user_loadouts_user"`);

Expand Down
6 changes: 1 addition & 5 deletions src/migrations/1781016036903-set_sleeve_tiers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,7 @@ import { MigrationInterface, QueryRunner } from "typeorm";
export class SetSleeveTiers1781016036903 implements MigrationInterface {
name = "SetSleeveTiers1781016036903";

private readonly refs = [
"sleeves/baby-frog/",
"sleeves/kagura/",
"sleeves/mystical-witch/",
];
private readonly refs = ["sleeves/baby-frog/", "sleeves/kagura/", "sleeves/mystical-witch/"];

public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
Expand Down
105 changes: 90 additions & 15 deletions src/modules/catalog/application/standardCosmetics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,19 +16,94 @@ export interface StandardCosmeticSeed {
// tier of an already-seeded cosmetic, so changing an existing tier needs a
// data migration (see SetSleeveTiers).
export const STANDARD_COSMETICS: StandardCosmeticSeed[] = [
{ type: CosmeticType.SLEEVE, tier: CosmeticTier.REGISTERED, assetRef: "sleeves/baby-frog/", displayName: "Baby Frog" },
{ type: CosmeticType.SLEEVE, tier: CosmeticTier.STANDARD, assetRef: "sleeves/classic/", displayName: "Classic" },
{ type: CosmeticType.SLEEVE, tier: CosmeticTier.REGISTERED, assetRef: "sleeves/kagura/", displayName: "Kagura" },
{ type: CosmeticType.SLEEVE, tier: CosmeticTier.REGISTERED, assetRef: "sleeves/mystical-witch/", displayName: "Mystical Witch" },
{ type: CosmeticType.SLEEVE, tier: CosmeticTier.STANDARD, assetRef: "sleeves/evolution/", displayName: "Evolution" },
{ type: CosmeticType.SLEEVE, tier: CosmeticTier.STANDARD, assetRef: "sleeves/evolution-black/", displayName: "Evolution Black" },
{ type: CosmeticType.PLAYMAT, tier: CosmeticTier.STANDARD, assetRef: "playmats/pallet-covered-a/", displayName: "Pallet Covered A" },
{ type: CosmeticType.PLAYMAT, tier: CosmeticTier.STANDARD, assetRef: "playmats/pallet-covered-b/", displayName: "Pallet Covered B" },
{ type: CosmeticType.PLAYMAT, tier: CosmeticTier.STANDARD, assetRef: "playmats/pallet-wood/", displayName: "Pallet Wood" },
{ type: CosmeticType.PLAYMAT, tier: CosmeticTier.STANDARD, assetRef: "playmats/plaque/", displayName: "Plaque" },
{ type: CosmeticType.AVATAR, tier: CosmeticTier.REGISTERED, assetRef: "avatars/baby-frog/", displayName: "Baby Frog" },
{ type: CosmeticType.AVATAR, tier: CosmeticTier.REGISTERED, assetRef: "avatars/kagura/", displayName: "Kagura" },
{ type: CosmeticType.AVATAR, tier: CosmeticTier.REGISTERED, assetRef: "avatars/mystical-witch/", displayName: "Mystical Witch" },
{ type: CosmeticType.AVATAR, tier: CosmeticTier.STANDARD, assetRef: "avatars/evolution/", displayName: "Evolution" },
{ type: CosmeticType.AVATAR, tier: CosmeticTier.STANDARD, assetRef: "avatars/evolution-black/", displayName: "Evolution Black" },
{
type: CosmeticType.SLEEVE,
tier: CosmeticTier.REGISTERED,
assetRef: "sleeves/baby-frog/",
displayName: "Baby Frog",
},
{
type: CosmeticType.SLEEVE,
tier: CosmeticTier.STANDARD,
assetRef: "sleeves/classic/",
displayName: "Classic",
},
{
type: CosmeticType.SLEEVE,
tier: CosmeticTier.REGISTERED,
assetRef: "sleeves/kagura/",
displayName: "Kagura",
},
{
type: CosmeticType.SLEEVE,
tier: CosmeticTier.REGISTERED,
assetRef: "sleeves/mystical-witch/",
displayName: "Mystical Witch",
},
{
type: CosmeticType.SLEEVE,
tier: CosmeticTier.STANDARD,
assetRef: "sleeves/evolution/",
displayName: "Evolution",
},
{
type: CosmeticType.SLEEVE,
tier: CosmeticTier.STANDARD,
assetRef: "sleeves/evolution-black/",
displayName: "Evolution Black",
},
{
type: CosmeticType.PLAYMAT,
tier: CosmeticTier.STANDARD,
assetRef: "playmats/pallet-covered-a/",
displayName: "Pallet Covered A",
},
{
type: CosmeticType.PLAYMAT,
tier: CosmeticTier.STANDARD,
assetRef: "playmats/pallet-covered-b/",
displayName: "Pallet Covered B",
},
{
type: CosmeticType.PLAYMAT,
tier: CosmeticTier.STANDARD,
assetRef: "playmats/pallet-wood/",
displayName: "Pallet Wood",
},
{
type: CosmeticType.PLAYMAT,
tier: CosmeticTier.STANDARD,
assetRef: "playmats/plaque/",
displayName: "Plaque",
},
{
type: CosmeticType.AVATAR,
tier: CosmeticTier.REGISTERED,
assetRef: "avatars/baby-frog/",
displayName: "Baby Frog",
},
{
type: CosmeticType.AVATAR,
tier: CosmeticTier.REGISTERED,
assetRef: "avatars/kagura/",
displayName: "Kagura",
},
{
type: CosmeticType.AVATAR,
tier: CosmeticTier.REGISTERED,
assetRef: "avatars/mystical-witch/",
displayName: "Mystical Witch",
},
{
type: CosmeticType.AVATAR,
tier: CosmeticTier.STANDARD,
assetRef: "avatars/evolution/",
displayName: "Evolution",
},
{
type: CosmeticType.AVATAR,
tier: CosmeticTier.STANDARD,
assetRef: "avatars/evolution-black/",
displayName: "Evolution Black",
},
];
9 changes: 8 additions & 1 deletion src/modules/catalog/domain/Cosmetic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,14 @@ export class Cosmetic {
displayName: string;
active: boolean;
}): Cosmetic {
return new Cosmetic(data.id, data.type, data.tier, data.assetRef, data.displayName, data.active);
return new Cosmetic(
data.id,
data.type,
data.tier,
data.assetRef,
data.displayName,
data.active,
);
}

toPrimitives(): {
Expand Down
Loading
Loading