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
2 changes: 1 addition & 1 deletion .github/workflows/pr-closed.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ jobs:

steps:
- name: Update project status
uses: actions/github-script@v7
uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7
with:
github-token: ${{ secrets.PROJECT_AUTOMATION_TOKEN }}
script: |
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/pr-opened.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ jobs:

steps:
- name: Extract issue number and update PR body
uses: actions/github-script@v7
uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7
with:
script: |
const branch = context.payload.pull_request.head.ref;
Expand All @@ -37,7 +37,7 @@ jobs:
console.log(`Replaced #??? with #${issueNumber}`);

- name: Update project status to In Review
uses: actions/github-script@v7
uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7
with:
github-token: ${{ secrets.PROJECT_AUTOMATION_TOKEN }}
script: |
Expand Down
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
16 changes: 12 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 @@ -292,10 +296,9 @@ import type * as _model_users_queries_internal_getUserByClaimToken from "../_mod
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_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_createTestUsers from "../_model/utils/createTestUsers.js";
import type * as _model_utils_deleteTestTournament from "../_model/utils/deleteTestTournament.js";
import type * as _model_utils_getTournamentOrganizerList from "../_model/utils/getTournamentOrganizerList.js";
Expand Down Expand Up @@ -325,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 @@ -353,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 @@ -634,10 +642,9 @@ declare const fullApi: ApiFromModules<{
"_model/users/queries/internal/getUserByEmail": typeof _model_users_queries_internal_getUserByEmail;
"_model/users/queries/internal/getUserById": typeof _model_users_queries_internal_getUserById;
"_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/createTestUsers": typeof _model_utils_createTestUsers;
"_model/utils/deleteTestTournament": typeof _model_utils_deleteTestTournament;
"_model/utils/getTournamentOrganizerList": typeof _model_utils_getTournamentOrganizerList;
Expand Down Expand Up @@ -667,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
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@ import { assignTables } from './assignTables';

const createMockTournament = (competitorCount: number): TournamentDeep => ({
competitorCount,
pairingConfig: {
tableCount: Math.ceil(competitorCount / 2),
orderBy: 'ranking',
policies: { repeat: 'block', sameAlignment: 'allow' },
},
}) as TournamentDeep;

const createMockData = (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,6 @@ export const createTournamentPairings = async (
if (tournament.status === 'draft') {
throw new ConvexError(getErrorMessage('CANNOT_ADD_PAIRINGS_TO_DRAFT_TOURNAMENT'));
}
if (tournament.status === 'published') {
throw new ConvexError(getErrorMessage('CANNOT_ADD_PAIRINGS_TO_PUBLISHED_TOURNAMENT'));
}
if (tournament.status === 'archived') {
throw new ConvexError(getErrorMessage('CANNOT_ADD_PAIRINGS_TO_ARCHIVED_TOURNAMENT'));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,6 @@ export const deleteTournamentPairings = async (
throw new ConvexError(getErrorMessage('CANNOT_DELETE_PAIRINGS_FROM_DRAFT_TOURNAMENT'));
}

// TODO: Technically not really needed...
if (tournament.status === 'published') {
throw new ConvexError(getErrorMessage('CANNOT_DELETE_PAIRINGS_FROM_PUBLISHED_TOURNAMENT'));
}

// TODO: Replace with generic 'archived' error:
if (tournament.status === 'archived') {
throw new ConvexError(getErrorMessage('CANNOT_DELETE_PAIRINGS_FROM_ARCHIVED_TOURNAMENT'));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,5 +19,9 @@ export const getTournamentRegistrationByTournamentUser = async (
if (!doc) {
return null;
}
const tournament = await ctx.db.get(doc.tournamentId);
if (!tournament) {
return null;
}
return await deepenTournamentRegistration(ctx, doc);
};
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ describe('getLastVisibleRound', () => {
const { userId, tournamentId } = await t.run(async (ctx) => {
const userId = await ctx.db.insert('users', {
email: 'to@test.com',
username: 'to_test',
locationVisibility: 0,
nameVisibility: 0,
});
Expand All @@ -59,6 +60,7 @@ describe('getLastVisibleRound', () => {
const { userId, tournamentId } = await t.run(async (ctx) => {
const userId = await ctx.db.insert('users', {
email: 'user@test.com',
username: 'user_test',
locationVisibility: 0,
nameVisibility: 0,
});
Expand Down
6 changes: 4 additions & 2 deletions convex/_model/tournaments/queries/getTournamentOpenRound.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { Infer, v } from 'convex/values';

import { QueryCtx } from '../../../_generated/server';
import { getTournamentShallow } from '../_helpers/getTournamentShallow';

export const getTournamentOpenRoundArgs = v.object({
id: v.id('tournaments'),
Expand All @@ -23,7 +22,10 @@ export const getTournamentOpenRound = async (
ctx: QueryCtx,
args: Infer<typeof getTournamentOpenRoundArgs>,
) => {
const tournament = await getTournamentShallow(ctx, args.id);
const tournament = await ctx.db.get(args.id);
if (!tournament) {
return null;
}

if (tournament.status !== 'active' || tournament.currentRound === undefined) {
return null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export const setUserDefaultAvatar = async (
}

// Fetch avatar
const avatarUrl = `https://api.dicebear.com/7.x/bottts-neutral/svg?seed=${Math.random()}&scale=75`;
const avatarUrl = `https://api.dicebear.com/7.x/bottts-neutral/svg?seed=${encodeURIComponent(user.username)}&scale=75`;
const avatarResponse = await fetch(avatarUrl);
if (!avatarResponse.ok) {
throw new Error('Failed to fetch avatar');
Expand Down
6 changes: 6 additions & 0 deletions convex/_model/users/actions/inviteUser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { checkAuth } from '../../common/_helpers/checkAuth';
import { getErrorMessage } from '../../common/errors';
import { VisibilityLevel } from '../../common/VisibilityLevel';
import { createClaimToken } from '../_helpers/createClaimToken';
import { generateUsername } from '../_helpers/generateUsername';
import { hashClaimToken } from '../_helpers/hashClaimToken';

export const inviteUserArgs = v.object({
Expand Down Expand Up @@ -55,12 +56,17 @@ export const inviteUser = async (
});
user = existingUser;
} else {
let username = generateUsername();
while (await ctx.runQuery(internal.users.getUserByUsername, { username })) {
username = generateUsername();
}
Comment thread
ianpaschal marked this conversation as resolved.
const { user: createdUser } = await createAccount(ctx, {
provider: 'password',
account: { id: email },
profile: {
...restArgs,
email,
username,
locationVisibility: VisibilityLevel.Hidden,
nameVisibility: VisibilityLevel.Tournaments,
claimTokenHash: await hashClaimToken(claimToken),
Expand Down
4 changes: 4 additions & 0 deletions convex/_model/users/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,3 +78,7 @@ export {
getUserById,
getUserByIdArgs,
} from './queries/internal/getUserById';
export {
getUserByUsername,
getUserByUsernameArgs,
} from './queries/internal/getUserByUsername';
15 changes: 15 additions & 0 deletions convex/_model/users/queries/internal/getUserByUsername.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { Infer, v } from 'convex/values';

import { Doc } from '../../../../_generated/dataModel';
import { QueryCtx } from '../../../../_generated/server';

export const getUserByUsernameArgs = v.object({
username: v.string(),
});

export const getUserByUsername = async (
ctx: QueryCtx,
args: Infer<typeof getUserByUsernameArgs>,
): Promise<Doc<'users'> | null> => await ctx.db.query('users')
.withIndex('by_username', (q) => q.eq('username', args.username))
.first();
2 changes: 1 addition & 1 deletion convex/_model/users/table.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ export const editableFields = {
givenName: v.optional(v.string()),
locationVisibility: v.number(),
nameVisibility: v.number(),
username: v.optional(v.string()),
username: v.string(),
};

export const computedFields = {
Expand Down
7 changes: 5 additions & 2 deletions convex/_model/utils/createTestTournament.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { GameSystem } from '@ianpaschal/combat-command-game-systems/common';
import { Infer, v } from 'convex/values';

import { createMockTournament } from '../../_fixtures/createMockTournament';
import { Id } from '../../_generated/dataModel';
import { MutationCtx } from '../../_generated/server';
import { getStaticEnumConvexValidator } from '../common/_helpers/getStaticEnumConvexValidator';
import { tournamentStatus } from '../common/tournamentStatus';
Expand Down Expand Up @@ -37,8 +38,8 @@ export const createTestTournament = async (
gameSystem,
competitorCount,
}: Infer<typeof createTestTournamentArgs>,
): Promise<void> => {
const maxCompetitors = competitorCount ?? useTeams ? 12 : 24;
): Promise<Id<'tournaments'>> => {
const maxCompetitors = competitorCount ?? (useTeams ? 12 : 24);
const competitorSize = useTeams ? 3 : 1;

// 1. Gather users
Expand Down Expand Up @@ -106,4 +107,6 @@ export const createTestTournament = async (
}
}
}

return tournamentId;
};
Loading
Loading