Skip to content
Draft

WIP #300

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
12 changes: 12 additions & 0 deletions .claude/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"permissions": {
"allow": [
"Read(//Users/ian/Code/combat-command-components/src/components/Wizard/**)",
"Read(//Users/ian/Code/combat-command-components/**)",
"Bash(npm run lint:*)"
],
"additionalDirectories": [
"/Users/ian/Code/combat-command-components/src/components/Wizard"
]
}
}
12 changes: 10 additions & 2 deletions convex/_generated/api.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ import type * as _model_common_tournamentPairingConfig from "../_model/common/to
import type * as _model_common_tournamentStatus from "../_model/common/tournamentStatus.js";
import type * as _model_common_types from "../_model/common/types.js";
import type * as _model_files_index from "../_model/files/index.js";
import type * as _model_files_queries_getFileMetadata from "../_model/files/queries/getFileMetadata.js";
import type * as _model_files_queries_getFileUrl from "../_model/files/queries/getFileUrl.js";
import type * as _model_friendships__helpers_deepenFriendship from "../_model/friendships/_helpers/deepenFriendship.js";
import type * as _model_friendships_index from "../_model/friendships/index.js";
Expand Down Expand Up @@ -81,8 +82,10 @@ import type * as _model_leagues_queries_getLeagues from "../_model/leagues/queri
import type * as _model_leagues_table from "../_model/leagues/table.js";
import type * as _model_leagues_types from "../_model/leagues/types.js";
import type * as _model_lists__helpers_deepenList from "../_model/lists/_helpers/deepenList.js";
import type * as _model_lists_actions_extractListData from "../_model/lists/actions/extractListData.js";
import type * as _model_lists_actions_importList from "../_model/lists/actions/importList.js";
import type * as _model_lists_index from "../_model/lists/index.js";
import type * as _model_lists_mutations_importListData from "../_model/lists/mutations/importListData.js";
import type * as _model_lists_mutations_createList from "../_model/lists/mutations/createList.js";
import type * as _model_lists_queries_getList from "../_model/lists/queries/getList.js";
import type * as _model_lists_table from "../_model/lists/table.js";
import type * as _model_matchResultComments__helpers_deepenMatchResultComment from "../_model/matchResultComments/_helpers/deepenMatchResultComment.js";
Expand Down Expand Up @@ -132,6 +135,7 @@ import type * as _model_tournamentCompetitors_mutations_createTournamentCompetit
import type * as _model_tournamentCompetitors_mutations_deleteTournamentCompetitor from "../_model/tournamentCompetitors/mutations/deleteTournamentCompetitor.js";
import type * as _model_tournamentCompetitors_mutations_toggleTournamentCompetitorActive from "../_model/tournamentCompetitors/mutations/toggleTournamentCompetitorActive.js";
import type * as _model_tournamentCompetitors_mutations_updateTournamentCompetitor from "../_model/tournamentCompetitors/mutations/updateTournamentCompetitor.js";
import type * as _model_tournamentCompetitors_queries_getAvailableTournamentCompetitorActions from "../_model/tournamentCompetitors/queries/getAvailableTournamentCompetitorActions.js";
import type * as _model_tournamentCompetitors_queries_getTournamentCompetitor from "../_model/tournamentCompetitors/queries/getTournamentCompetitor.js";
import type * as _model_tournamentCompetitors_queries_getTournamentCompetitors from "../_model/tournamentCompetitors/queries/getTournamentCompetitors.js";
import type * as _model_tournamentCompetitors_queries_getTournamentCompetitorsByTournament from "../_model/tournamentCompetitors/queries/getTournamentCompetitorsByTournament.js";
Expand Down Expand Up @@ -356,6 +360,7 @@ declare const fullApi: ApiFromModules<{
"_model/common/tournamentStatus": typeof _model_common_tournamentStatus;
"_model/common/types": typeof _model_common_types;
"_model/files/index": typeof _model_files_index;
"_model/files/queries/getFileMetadata": typeof _model_files_queries_getFileMetadata;
"_model/files/queries/getFileUrl": typeof _model_files_queries_getFileUrl;
"_model/friendships/_helpers/deepenFriendship": typeof _model_friendships__helpers_deepenFriendship;
"_model/friendships/index": typeof _model_friendships_index;
Expand Down Expand Up @@ -395,8 +400,10 @@ declare const fullApi: ApiFromModules<{
"_model/leagues/table": typeof _model_leagues_table;
"_model/leagues/types": typeof _model_leagues_types;
"_model/lists/_helpers/deepenList": typeof _model_lists__helpers_deepenList;
"_model/lists/actions/extractListData": typeof _model_lists_actions_extractListData;
"_model/lists/actions/importList": typeof _model_lists_actions_importList;
"_model/lists/index": typeof _model_lists_index;
"_model/lists/mutations/importListData": typeof _model_lists_mutations_importListData;
"_model/lists/mutations/createList": typeof _model_lists_mutations_createList;
"_model/lists/queries/getList": typeof _model_lists_queries_getList;
"_model/lists/table": typeof _model_lists_table;
"_model/matchResultComments/_helpers/deepenMatchResultComment": typeof _model_matchResultComments__helpers_deepenMatchResultComment;
Expand Down Expand Up @@ -446,6 +453,7 @@ declare const fullApi: ApiFromModules<{
"_model/tournamentCompetitors/mutations/deleteTournamentCompetitor": typeof _model_tournamentCompetitors_mutations_deleteTournamentCompetitor;
"_model/tournamentCompetitors/mutations/toggleTournamentCompetitorActive": typeof _model_tournamentCompetitors_mutations_toggleTournamentCompetitorActive;
"_model/tournamentCompetitors/mutations/updateTournamentCompetitor": typeof _model_tournamentCompetitors_mutations_updateTournamentCompetitor;
"_model/tournamentCompetitors/queries/getAvailableTournamentCompetitorActions": typeof _model_tournamentCompetitors_queries_getAvailableTournamentCompetitorActions;
"_model/tournamentCompetitors/queries/getTournamentCompetitor": typeof _model_tournamentCompetitors_queries_getTournamentCompetitor;
"_model/tournamentCompetitors/queries/getTournamentCompetitors": typeof _model_tournamentCompetitors_queries_getTournamentCompetitors;
"_model/tournamentCompetitors/queries/getTournamentCompetitorsByTournament": typeof _model_tournamentCompetitors_queries_getTournamentCompetitorsByTournament;
Expand Down
27 changes: 27 additions & 0 deletions convex/_model/common/_helpers/getDocStrict.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { GenericDatabaseReader } from 'convex/server';
import { ConvexError, GenericId } from 'convex/values';

import {
DataModel,
Doc,
TableNames,
} from '../../../_generated/dataModel';

/**
* Fetches a document by ID, throwing a `ConvexError` if it doesn't exist.
*
* @param ctx - A context object with a database reader.
* @param id - The ID of the document to fetch.
* @returns The document.
* @throws {ConvexError} If no document exists with the given ID.
*/
export const getDocStrict = async <T extends TableNames>(
ctx: { db: GenericDatabaseReader<DataModel> },
id: GenericId<T>,
): Promise<Doc<T>> => {
const doc = await ctx.db.get(id);
if (!doc) {
throw new ConvexError({ message: `Document not found: ${id}`, code: 'DOCUMENT_NOT_FOUND' });
}
return doc;
};
25 changes: 25 additions & 0 deletions convex/_model/files/queries/getFileMetadata.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { GenericDoc } from '@convex-dev/auth/server';
import { SystemDataModel } from 'convex/server';
import {
ConvexError,
Infer,
v,
} from 'convex/values';

import { QueryCtx } from '../../../_generated/server';
import { getErrorMessage } from '../../common/errors';

export const getFileMetadataArgs = v.object({
id: v.id('_storage'),
});

export const getFileMetadata = async (
ctx: QueryCtx,
args: Infer<typeof getFileMetadataArgs>,
): Promise<GenericDoc<SystemDataModel,'_storage'> | null> => {
const fileMetadata = await ctx.db.system.get(args.id);
if (!fileMetadata) {
throw new ConvexError(getErrorMessage('FILE_NOT_FOUND'));
}
return fileMetadata;
};
24 changes: 24 additions & 0 deletions convex/_model/lists/_helpers/checkListVisibility.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { getAuthUserId } from '@convex-dev/auth/server';

import { Doc } from '../../../_generated/dataModel';
import { QueryCtx } from '../../../_generated/server';
import { getDocStrict } from '../../common/_helpers/getDocStrict';
import { checkUsersAreTeammates } from '../../tournamentCompetitors/_helpers/checkUsersAreTeammates';
import { checkUserIsTournamentOrganizer } from '../../tournamentOrganizers';

export const checkListVisibility = async (
ctx: QueryCtx,
doc: Doc<'lists'>,
): Promise<boolean> => {
const userId = await getAuthUserId(ctx);
const tournamentRegistration = await getDocStrict(ctx, doc.tournamentRegistrationId);
const tournament = await getDocStrict(ctx, tournamentRegistration.tournamentId);
const isOrganizer = await checkUserIsTournamentOrganizer(ctx, tournament._id, userId);
const isTeammate = await checkUsersAreTeammates(ctx, tournamentRegistration.userId, userId);

if (isOrganizer || isTeammate || tournament.listsRevealed) {
return true;
}

return false;
};
38 changes: 23 additions & 15 deletions convex/_model/lists/_helpers/deepenList.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { GameSystem } from '@ianpaschal/combat-command-game-systems/common';
import { ConvexError } from 'convex/values';
/* eslint-disable arrow-body-style */
// import { GameSystem } from '@ianpaschal/combat-command-game-systems/common';
// import { ConvexError } from 'convex/values';

import { Doc } from '../../../_generated/dataModel';
import { QueryCtx } from '../../../_generated/server';
import { getErrorMessage } from '../../common/errors';
import { FlamesOfWarV4 } from '../../gameSystems';
// import { getErrorMessage } from '../../common/errors';
// import { FlamesOfWarV4 } from '../../gameSystems';

/* eslint-disable @typescript-eslint/explicit-function-return-type */
/**
Expand All @@ -19,19 +20,26 @@ import { FlamesOfWarV4 } from '../../gameSystems';
*/
export const deepenList = async (
ctx: QueryCtx,
list: Doc<'lists'>,
doc: Doc<'lists'>,
) => {
// TODO-250: Add Team Yankee support here.
if (list.gameSystem === GameSystem.FlamesOfWarV4) {
return {
...list,
data: FlamesOfWarV4.deepenListData(list.data),
};
}

// If no matcher found, throw an error:
throw new ConvexError(getErrorMessage('CANNOT_ADD_ANOTHER_PLAYER'));
// // TODO-250: Add Team Yankee support here.
// if (list.gameSystem === GameSystem.FlamesOfWarV4) {
// return {
// ...list,
// data: FlamesOfWarV4.deepenListData(list.data),
// };
// }

// if (list.gameSystem === GameSystem.TeamYankeeV2) {
// return {
// ...list,
// data: TeamYankeeV2.deepenListData(list.data),
// };
// }

// // If no matcher found, throw an error:
// throw new ConvexError(getErrorMessage('CANNOT_ADD_ANOTHER_PLAYER'));
return doc;
};

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { factions, forceDiagrams } from '@ianpaschal/combat-command-game-systems/flamesOfWarV4';

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

export type DeepFowV4ListData = ReturnType<typeof deepenListData>;

Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
import { ForceDiagram, Unit } from '@ianpaschal/combat-command-game-systems/flamesOfWarV4';
import { Infer, v } from 'convex/values';

import { getStaticEnumConvexValidator } from '../../../../common/_helpers/getStaticEnumConvexValidator';
import { getStaticEnumConvexValidator } from '../../../../../common/_helpers/getStaticEnumConvexValidator';

const forceDiagram = getStaticEnumConvexValidator(ForceDiagram);
const unit = getStaticEnumConvexValidator(Unit);

export const listData = v.object({
tournamentRegistrationId: v.optional(v.id('tournamentRegistrations')),
meta: v.object({
forceDiagram,
pointsLimit: v.number(),
Expand Down
13 changes: 10 additions & 3 deletions convex/_model/lists/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,17 @@ import { Id } from '../../_generated/dataModel';
export type ListId = Id<'lists'>;

export {
importListData,
importListDataArgs,
} from './mutations/importListData';
type DeepList,
} from './_helpers/deepenList';
export {
createList,
createListArgs,
} from './mutations/createList';
export {
getList,
getListArgs,
} from './queries/getList';
export {
getListsByTournamentRegistration,
getListsByTournamentRegistrationArgs,
} from './queries/getListsByTournamentRegistration';
41 changes: 41 additions & 0 deletions convex/_model/lists/mutations/createList.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import {
ConvexError,
Infer,
v,
} from 'convex/values';

import { Id } from '../../../_generated/dataModel';
import { MutationCtx } from '../../../_generated/server';
import { getErrorMessage } from '../../common/errors';
import { editableFields } from '../table';

// const forceDiagram = getStaticEnumConvexValidator(ForceDiagram);
// const formation = getStaticEnumConvexValidator(Unit);

// const nanoid = customAlphabet('1234567890abcdefghijklmnopqrstuvwxyz', 6);

export const createListArgs = v.object(editableFields);

export const createList = async (
ctx: MutationCtx,
args: Infer<typeof createListArgs>,
): Promise<Id<'lists'>> => {
const tournamentRegistration = await ctx.db.get(args.tournamentRegistrationId);
if (!tournamentRegistration) {
throw new ConvexError(getErrorMessage('TOURNAMENT_REGISTRATION_NOT_FOUND'));
}
const existingLists = await ctx.db.query('lists')
.withIndex('by_tournament_registration', (q) => q.eq('tournamentRegistrationId', args.tournamentRegistrationId))
.collect();
const tournament = await ctx.db.get(tournamentRegistration.tournamentId);
if (!tournament) {
throw new ConvexError(getErrorMessage('TOURNAMENT_NOT_FOUND'));
}
const { listSubmissionClosesAt } = tournament;

return await ctx.db.insert('lists', {
...args,
locked: Date.now() > listSubmissionClosesAt,
primary: existingLists.length === 0,
});
};
51 changes: 0 additions & 51 deletions convex/_model/lists/mutations/importListData.ts

This file was deleted.

10 changes: 7 additions & 3 deletions convex/_model/lists/queries/getList.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Infer, v } from 'convex/values';

import { QueryCtx } from '../../../_generated/server';
import { checkListVisibility } from '../_helpers/checkListVisibility';
import { deepenList, DeepList } from '../_helpers/deepenList';

export const getListArgs = v.object({
Expand All @@ -19,9 +20,12 @@ export const getList = async (
ctx: QueryCtx,
args: Infer<typeof getListArgs>,
): Promise<DeepList | null> => {
const list = await ctx.db.get(args.id);
if (!list) {
const result = await ctx.db.get(args.id);
if (!result) {
return null;
}
return await deepenList(ctx, list);
if (await checkListVisibility(ctx, result)) {
return await deepenList(ctx, result);
}
return null;
};
34 changes: 34 additions & 0 deletions convex/_model/lists/queries/getListsByTournamentRegistration.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { Infer, v } from 'convex/values';

import { QueryCtx } from '../../../_generated/server';
import { notNullOrUndefined } from '../../common/_helpers/notNullOrUndefined';
import { checkListVisibility } from '../_helpers/checkListVisibility';
import { deepenList, DeepList } from '../_helpers/deepenList';

export const getListsByTournamentRegistrationArgs = v.object({
tournamentRegistrationId: v.id('tournamentRegistrations'),
});

/**
* Gets a list by tournament registration.
*
* @param ctx - Convex query context
* @param args - Convex query args
* @param args.id - ID of the tournament registration
* @returns - An array of deepened lists
*/
export const getListsByTournamentRegistration = async (
ctx: QueryCtx,
args: Infer<typeof getListsByTournamentRegistrationArgs>,
): Promise<DeepList[]> => {
const results = await ctx.db.query('lists')
.withIndex('by_tournament_registration', (q) => q.eq('tournamentRegistrationId', args.tournamentRegistrationId))
.collect();
const deepResults = await Promise.all(results.map(async (r) => {
if (await checkListVisibility(ctx, r)) {
return await deepenList(ctx, r);
}
return null;
}));
return deepResults.filter(notNullOrUndefined);
};
Loading