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
69 changes: 69 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
name: CI

on:
pull_request:

permissions:
contents: read

jobs:
test:
runs-on: ubuntu-latest
timeout-minutes: 20

steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
persist-credentials: false

- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: '24'
cache: 'npm'

- name: Install dependencies
run: npm ci
env:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}

- name: Unit tests
run: npm test

- name: Deploy Convex preview
run: |
npx convex deploy \
--preview-create "pr-${{ github.event.pull_request.number }}-${{ github.run_attempt }}" \
--cmd-url-env-var-name VITE_CONVEX_URL \
--cmd 'echo "VITE_CONVEX_URL=$VITE_CONVEX_URL" >> "$GITHUB_ENV"'
env:
CONVEX_DEPLOY_KEY: ${{ secrets.CONVEX_DEPLOY_KEY }}

- name: Configure preview deployment environment
run: |
PREVIEW="pr-${{ github.event.pull_request.number }}-${{ github.run_attempt }}"
npx convex env set --preview-name "$PREVIEW" -- JWT_PRIVATE_KEY "$JWT_PRIVATE_KEY"
npx convex env set --preview-name "$PREVIEW" -- JWKS "$JWKS"
npx convex env set --preview-name "$PREVIEW" -- AUTH_RESEND_KEY "$AUTH_RESEND_KEY"
npx convex env set --preview-name "$PREVIEW" -- SITE_URL "http://localhost:5173"
env:
CONVEX_DEPLOY_KEY: ${{ secrets.CONVEX_DEPLOY_KEY }}
JWT_PRIVATE_KEY: ${{ secrets.CONVEX_JWT_PRIVATE_KEY }}
JWKS: ${{ secrets.CONVEX_JWKS }}
AUTH_RESEND_KEY: ${{ secrets.CONVEX_AUTH_RESEND_KEY }}

- name: Install Playwright browsers
run: npx playwright install firefox --with-deps

- name: E2E tests
run: npm run test:e2e
env:
CONVEX_DEPLOY_KEY: ${{ secrets.CONVEX_DEPLOY_KEY }}
CONVEX_PREVIEW_NAME: pr-${{ github.event.pull_request.number }}-${{ github.run_attempt }}

- name: Upload Playwright report
if: failure()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: playwright-report
path: playwright-report/
retention-days: 7
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ pnpm-debug.log*
lerna-debug.log*

coverage
playwright-report
test-results
test/.auth
node_modules
dist
dist-ssr
Expand Down
9 changes: 7 additions & 2 deletions convex/_fixtures/createMockTournament.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@ import {
CurrencyCode,
GameSystem,
getGameSystem,
tournamentPairingConfig,
} from '@ianpaschal/combat-command-game-systems/common';

import { Doc } from '../_generated/dataModel';
import {
defaultValues as tournamentPairingConfigDefaultValues,
} from '../_model/common/tournamentPairingConfig';
import { RankingFactor } from '../_model/common/types';

const DAY_LENGTH_MS = 172800000;
Expand Down Expand Up @@ -46,7 +48,10 @@ export const createMockTournament = (
currency: CurrencyCode.EUR,
},
roundCount: 5,
pairingConfig: tournamentPairingConfig.defaultValues,
pairingConfig: {
...tournamentPairingConfigDefaultValues,
tableCount: Math.ceil((overrides?.maxCompetitors ?? 48) / 2),
},
Comment thread
ianpaschal marked this conversation as resolved.
gameSystemConfig: gameSystemConfig.defaultValues,
startsAt: Date.now() + (DAY_LENGTH_MS * 3),
endsAt: Date.now() + (DAY_LENGTH_MS * 5),
Expand Down
24 changes: 20 additions & 4 deletions convex/_generated/api.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@
import type * as _fixtures_createMockTournament from "../_fixtures/createMockTournament.js";
import type * as _fixtures_createMockTournamentCompetitor from "../_fixtures/createMockTournamentCompetitor.js";
import type * as _fixtures_fowV4_createMockFowV4MatchResultData from "../_fixtures/fowV4/createMockFowV4MatchResultData.js";
import type * as _model__test__helpers_testUsers from "../_model/_test/_helpers/testUsers.js";
import type * as _model__test_actions_populateUsers from "../_model/_test/actions/populateUsers.js";
import type * as _model__test_index from "../_model/_test/index.js";
import type * as _model__test_mutations_cleanUp from "../_model/_test/mutations/cleanUp.js";
import type * as _model_common_VisibilityLevel from "../_model/common/VisibilityLevel.js";
import type * as _model_common__helpers_buildFilteredQuery from "../_model/common/_helpers/buildFilteredQuery.js";
import type * as _model_common__helpers_checkAuth from "../_model/common/_helpers/checkAuth.js";
Expand Down Expand Up @@ -192,6 +196,7 @@ import type * as _model_tournamentRegistrations__helpers_deepenTournamentRegistr
import type * as _model_tournamentRegistrations__helpers_getAvailableActions from "../_model/tournamentRegistrations/_helpers/getAvailableActions.js";
import type * as _model_tournamentRegistrations__helpers_getCreateSuccessMessage from "../_model/tournamentRegistrations/_helpers/getCreateSuccessMessage.js";
import type * as _model_tournamentRegistrations__helpers_getDeleteSuccessMessage from "../_model/tournamentRegistrations/_helpers/getDeleteSuccessMessage.js";
import type * as _model_tournamentRegistrations__helpers_sortTournamentCompetitorsByName from "../_model/tournamentRegistrations/_helpers/sortTournamentCompetitorsByName.js";
import type * as _model_tournamentRegistrations_index from "../_model/tournamentRegistrations/index.js";
import type * as _model_tournamentRegistrations_mutations_createTournamentRegistration from "../_model/tournamentRegistrations/mutations/createTournamentRegistration.js";
import type * as _model_tournamentRegistrations_mutations_deleteTournamentRegistration from "../_model/tournamentRegistrations/mutations/deleteTournamentRegistration.js";
Expand Down Expand Up @@ -233,6 +238,7 @@ import type * as _model_tournaments__helpers_deepenTournament from "../_model/to
import type * as _model_tournaments__helpers_extractSearchTokens from "../_model/tournaments/_helpers/extractSearchTokens.js";
import type * as _model_tournaments__helpers_getAvailableActions from "../_model/tournaments/_helpers/getAvailableActions.js";
import type * as _model_tournaments__helpers_getDisplayName from "../_model/tournaments/_helpers/getDisplayName.js";
import type * as _model_tournaments__helpers_getLastVisibleRound from "../_model/tournaments/_helpers/getLastVisibleRound.js";
import type * as _model_tournaments__helpers_getTournamentDeep from "../_model/tournaments/_helpers/getTournamentDeep.js";
import type * as _model_tournaments__helpers_getTournamentNextRound from "../_model/tournaments/_helpers/getTournamentNextRound.js";
import type * as _model_tournaments__helpers_getTournamentPlayerUserIds from "../_model/tournaments/_helpers/getTournamentPlayerUserIds.js";
Expand All @@ -249,6 +255,7 @@ import type * as _model_tournaments_mutations_startTournamentRound from "../_mod
import type * as _model_tournaments_mutations_toggleTournamentAlignmentsRevealed from "../_model/tournaments/mutations/toggleTournamentAlignmentsRevealed.js";
import type * as _model_tournaments_mutations_toggleTournamentListsRevealed from "../_model/tournaments/mutations/toggleTournamentListsRevealed.js";
import type * as _model_tournaments_mutations_updateTournament from "../_model/tournaments/mutations/updateTournament.js";
import type * as _model_tournaments_mutations_updateTournamentPairingConfig from "../_model/tournaments/mutations/updateTournamentPairingConfig.js";
import type * as _model_tournaments_queries_getTournament from "../_model/tournaments/queries/getTournament.js";
import type * as _model_tournaments_queries_getTournamentByTournamentPairing from "../_model/tournaments/queries/getTournamentByTournamentPairing.js";
import type * as _model_tournaments_queries_getTournamentOpenRound from "../_model/tournaments/queries/getTournamentOpenRound.js";
Expand Down Expand Up @@ -288,11 +295,11 @@ import type * as _model_users_queries_getUsers from "../_model/users/queries/get
import type * as _model_users_queries_internal_getUserByClaimToken from "../_model/users/queries/internal/getUserByClaimToken.js";
import type * as _model_users_queries_internal_getUserByEmail from "../_model/users/queries/internal/getUserByEmail.js";
import type * as _model_users_queries_internal_getUserById from "../_model/users/queries/internal/getUserById.js";
import type * as _model_users_queries_internal_getUserByUsername from "../_model/users/queries/internal/getUserByUsername.js";
import type * as _model_users_table from "../_model/users/table.js";
import type * as _model_utils__helpers_testUsers from "../_model/utils/_helpers/testUsers.js";
import type * as _model_utils_actions_populateUsers from "../_model/utils/actions/populateUsers.js";
import type * as _model_utils_createTestTournament from "../_model/utils/createTestTournament.js";
import type * as _model_utils_createTestTournamentMatchResults from "../_model/utils/createTestTournamentMatchResults.js";
import type * as _model_utils_createTestTournamentPairings from "../_model/utils/createTestTournamentPairings.js";
import type * as _model_utils_deleteTestTournament from "../_model/utils/deleteTestTournament.js";
import type * as _model_utils_getTournamentOrganizerList from "../_model/utils/getTournamentOrganizerList.js";
import type * as _model_utils_getTournamentRegistrationsCsv from "../_model/utils/getTournamentRegistrationsCsv.js";
Expand Down Expand Up @@ -321,6 +328,7 @@ import type * as matchResults from "../matchResults.js";
import type * as migrations from "../migrations.js";
import type * as photos from "../photos.js";
import type * as scheduledTasks from "../scheduledTasks.js";
import type * as test from "../test.js";
import type * as tournamentCompetitors from "../tournamentCompetitors.js";
import type * as tournamentPairings from "../tournamentPairings.js";
import type * as tournamentRegistrations from "../tournamentRegistrations.js";
Expand Down Expand Up @@ -349,6 +357,10 @@ declare const fullApi: ApiFromModules<{
"_fixtures/createMockTournament": typeof _fixtures_createMockTournament;
"_fixtures/createMockTournamentCompetitor": typeof _fixtures_createMockTournamentCompetitor;
"_fixtures/fowV4/createMockFowV4MatchResultData": typeof _fixtures_fowV4_createMockFowV4MatchResultData;
"_model/_test/_helpers/testUsers": typeof _model__test__helpers_testUsers;
"_model/_test/actions/populateUsers": typeof _model__test_actions_populateUsers;
"_model/_test/index": typeof _model__test_index;
"_model/_test/mutations/cleanUp": typeof _model__test_mutations_cleanUp;
"_model/common/VisibilityLevel": typeof _model_common_VisibilityLevel;
"_model/common/_helpers/buildFilteredQuery": typeof _model_common__helpers_buildFilteredQuery;
"_model/common/_helpers/checkAuth": typeof _model_common__helpers_checkAuth;
Expand Down Expand Up @@ -530,6 +542,7 @@ declare const fullApi: ApiFromModules<{
"_model/tournamentRegistrations/_helpers/getAvailableActions": typeof _model_tournamentRegistrations__helpers_getAvailableActions;
"_model/tournamentRegistrations/_helpers/getCreateSuccessMessage": typeof _model_tournamentRegistrations__helpers_getCreateSuccessMessage;
"_model/tournamentRegistrations/_helpers/getDeleteSuccessMessage": typeof _model_tournamentRegistrations__helpers_getDeleteSuccessMessage;
"_model/tournamentRegistrations/_helpers/sortTournamentCompetitorsByName": typeof _model_tournamentRegistrations__helpers_sortTournamentCompetitorsByName;
"_model/tournamentRegistrations/index": typeof _model_tournamentRegistrations_index;
"_model/tournamentRegistrations/mutations/createTournamentRegistration": typeof _model_tournamentRegistrations_mutations_createTournamentRegistration;
"_model/tournamentRegistrations/mutations/deleteTournamentRegistration": typeof _model_tournamentRegistrations_mutations_deleteTournamentRegistration;
Expand Down Expand Up @@ -571,6 +584,7 @@ declare const fullApi: ApiFromModules<{
"_model/tournaments/_helpers/extractSearchTokens": typeof _model_tournaments__helpers_extractSearchTokens;
"_model/tournaments/_helpers/getAvailableActions": typeof _model_tournaments__helpers_getAvailableActions;
"_model/tournaments/_helpers/getDisplayName": typeof _model_tournaments__helpers_getDisplayName;
"_model/tournaments/_helpers/getLastVisibleRound": typeof _model_tournaments__helpers_getLastVisibleRound;
"_model/tournaments/_helpers/getTournamentDeep": typeof _model_tournaments__helpers_getTournamentDeep;
"_model/tournaments/_helpers/getTournamentNextRound": typeof _model_tournaments__helpers_getTournamentNextRound;
"_model/tournaments/_helpers/getTournamentPlayerUserIds": typeof _model_tournaments__helpers_getTournamentPlayerUserIds;
Expand All @@ -587,6 +601,7 @@ declare const fullApi: ApiFromModules<{
"_model/tournaments/mutations/toggleTournamentAlignmentsRevealed": typeof _model_tournaments_mutations_toggleTournamentAlignmentsRevealed;
"_model/tournaments/mutations/toggleTournamentListsRevealed": typeof _model_tournaments_mutations_toggleTournamentListsRevealed;
"_model/tournaments/mutations/updateTournament": typeof _model_tournaments_mutations_updateTournament;
"_model/tournaments/mutations/updateTournamentPairingConfig": typeof _model_tournaments_mutations_updateTournamentPairingConfig;
"_model/tournaments/queries/getTournament": typeof _model_tournaments_queries_getTournament;
"_model/tournaments/queries/getTournamentByTournamentPairing": typeof _model_tournaments_queries_getTournamentByTournamentPairing;
"_model/tournaments/queries/getTournamentOpenRound": typeof _model_tournaments_queries_getTournamentOpenRound;
Expand Down Expand Up @@ -626,11 +641,11 @@ declare const fullApi: ApiFromModules<{
"_model/users/queries/internal/getUserByClaimToken": typeof _model_users_queries_internal_getUserByClaimToken;
"_model/users/queries/internal/getUserByEmail": typeof _model_users_queries_internal_getUserByEmail;
"_model/users/queries/internal/getUserById": typeof _model_users_queries_internal_getUserById;
"_model/users/queries/internal/getUserByUsername": typeof _model_users_queries_internal_getUserByUsername;
"_model/users/table": typeof _model_users_table;
"_model/utils/_helpers/testUsers": typeof _model_utils__helpers_testUsers;
"_model/utils/actions/populateUsers": typeof _model_utils_actions_populateUsers;
"_model/utils/createTestTournament": typeof _model_utils_createTestTournament;
"_model/utils/createTestTournamentMatchResults": typeof _model_utils_createTestTournamentMatchResults;
"_model/utils/createTestTournamentPairings": typeof _model_utils_createTestTournamentPairings;
"_model/utils/deleteTestTournament": typeof _model_utils_deleteTestTournament;
"_model/utils/getTournamentOrganizerList": typeof _model_utils_getTournamentOrganizerList;
"_model/utils/getTournamentRegistrationsCsv": typeof _model_utils_getTournamentRegistrationsCsv;
Expand Down Expand Up @@ -659,6 +674,7 @@ declare const fullApi: ApiFromModules<{
migrations: typeof migrations;
photos: typeof photos;
scheduledTasks: typeof scheduledTasks;
test: typeof test;
tournamentCompetitors: typeof tournamentCompetitors;
tournamentPairings: typeof tournamentPairings;
tournamentRegistrations: typeof tournamentRegistrations;
Expand Down
2 changes: 2 additions & 0 deletions convex/_model/_test/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './actions/populateUsers';
export * from './mutations/cleanUp';
24 changes: 24 additions & 0 deletions convex/_model/_test/mutations/cleanUp.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import type { TableNamesInDataModel } from 'convex/server';
import { Infer, v } from 'convex/values';

import type { DataModel } from '../../../_generated/dataModel';
import type { MutationCtx } from '../../../_generated/server';
import schema from '../../../schema';

type TableName = TableNamesInDataModel<DataModel>;

export const cleanUpArgs = v.object({
preservedTables: v.array(v.string()),
});

export const cleanUp = async (
ctx: MutationCtx,
args: Infer<typeof cleanUpArgs>,
): Promise<void> => {
const preserved = new Set(args.preservedTables);
const tables = (Object.keys(schema.tables) as TableName[]).filter((t) => !preserved.has(t));
for (const table of tables) {
const docs = await ctx.db.query(table).collect();
await Promise.all(docs.map((doc) => ctx.db.delete(doc._id)));
}
};
2 changes: 0 additions & 2 deletions convex/_model/common/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,11 +110,9 @@ export const errors = {
CANNOT_ADD_PAIRINGS_TO_ARCHIVED_TOURNAMENT: 'Cannot add pairings to an archived tournament.',
CANNOT_ADD_PAIRINGS_TO_DRAFT_TOURNAMENT: 'Cannot add pairings to a draft tournament.',
CANNOT_ADD_PAIRINGS_TO_IN_PROGRESS_ROUND: 'Cannot add pairings while round is already in-progress.',
CANNOT_ADD_PAIRINGS_TO_PUBLISHED_TOURNAMENT: 'Cannot add pairings to a tournament that hasn\'t started yet.',
CANNOT_DELETE_PAIRINGS_FROM_ARCHIVED_TOURNAMENT: 'Cannot delete pairings from an archived tournament.',
CANNOT_DELETE_PAIRINGS_FROM_DRAFT_TOURNAMENT: 'Cannot delete pairings from a draft tournament.',
CANNOT_DELETE_PAIRINGS_FROM_IN_PROGRESS_ROUND: 'Cannot delete pairings while round is in-progress.',
CANNOT_DELETE_PAIRINGS_FROM_PUBLISHED_TOURNAMENT: 'Cannot delete pairings from a tournament that hasn\'t started yet.',
CANNOT_ADD_TOO_MANY_PAIRINGS: 'Cannot add more pairings than the tournament is set-up for.',
TOURNAMENT_ALREADY_HAS_PAIRINGS_FOR_ROUND: 'Tournament already has pairings for this round.',

Expand Down
36 changes: 34 additions & 2 deletions convex/_model/common/tournamentPairingConfig.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,36 @@
import { tournamentPairingConfig as config } from '@ianpaschal/combat-command-game-systems/common';
import { TournamentPairingPolicy } from '@ianpaschal/combat-command-game-systems/common';
import { createEnumSchema } from '@ianpaschal/combat-command-game-systems/utils';
import { zodToConvex } from 'convex-helpers/server/zod';
import { z } from 'zod';

export const tournamentPairingConfig = zodToConvex(config.schema);
import { Id } from '../../_generated/dataModel';

export const schema = z.object({
orderBy: z.union([z.literal('ranking'), z.literal('random')]),
policies: z.object({
sameAlignment: createEnumSchema(TournamentPairingPolicy),
repeat: createEnumSchema(TournamentPairingPolicy),
}),
tableCount: z.optional(z.number().min(1)), // TODO: REMOVE POST MIGRATION
});

export const generationSchema = schema.extend({
include: z.array(z.string()).transform((val): Id<'tournamentCompetitors'>[] => val as Id<'tournamentCompetitors'>[]),
});

export type TournamentPairingConfig = z.infer<typeof schema>;

export type TournamentPairingPolicies = Partial<TournamentPairingConfig['policies']>;

export const defaultValues = {
orderBy: 'ranking',
policies: {
sameAlignment: TournamentPairingPolicy.Allow,
repeat: TournamentPairingPolicy.Block,
},
tableCount: 1,
} satisfies TournamentPairingConfig;

export const tournamentPairingConfig = zodToConvex(schema);

export const tournamentPairingInput = zodToConvex(generationSchema);
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ export const createTournamentCompetitor = async (
throw new ConvexError(getErrorMessage('CANNOT_MODIFY_ARCHIVED_TOURNAMENT'));
}
const tournamentCompetitors = await ctx.db.query('tournamentCompetitors')
.withIndex('by_tournament_id', (q) => q.eq('tournamentId', args.tournamentId))
.withIndex('by_tournament', (q) => q.eq('tournamentId', args.tournamentId))
.collect();
const existingTeamNames = tournamentCompetitors.map((item) => item.teamName);
if (args.teamName && existingTeamNames.includes(args.teamName)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,14 @@ export const getTournamentCompetitorsByTournament = async (
ctx: QueryCtx,
args: Infer<typeof getTournamentCompetitorsByTournamentArgs>,
): Promise<DeepTournamentCompetitor[]> => {
const tournamentCompetitors = await ctx.db.query('tournamentCompetitors')
.withIndex('by_tournament_id', (q) => q.eq('tournamentId', args.tournamentId))
const results = await ctx.db.query('tournamentCompetitors')
.withIndex('by_tournament', (q) => q.eq('tournamentId', args.tournamentId))
.collect();
return (await Promise.all(tournamentCompetitors.map(async (item) => (
const deepened = await Promise.all(results.map(async (item) => (
await deepenTournamentCompetitor(ctx, item, args.rankingRound)
)))).sort(sortTournamentCompetitorsByName);
)));
if (args.rankingRound !== undefined) {
return deepened.sort((a, b) => a.rank - b.rank);
}
return deepened.sort(sortTournamentCompetitorsByName);
};
2 changes: 1 addition & 1 deletion convex/_model/tournamentCompetitors/table.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,4 @@ export default defineTable({
...editableFields,
...computedFields,
})
.index('by_tournament_id', ['tournamentId']);
.index('by_tournament', ['tournamentId']);
Loading
Loading