feat(pokemon): enforce bidirectional opposite sync and hide opposite text
Add unique indexes and transactional sync for opposite configurations Remove explicit opposite names and labels from Pokemon detail view
This commit is contained in:
@@ -1575,6 +1575,14 @@ BEGIN
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS environments_opposite_environment_unique_idx
|
||||
ON environments(opposite_environment_id)
|
||||
WHERE opposite_environment_id IS NOT NULL;
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS favorite_things_opposite_favorite_thing_unique_idx
|
||||
ON favorite_things(opposite_favorite_thing_id)
|
||||
WHERE opposite_favorite_thing_id IS NOT NULL;
|
||||
|
||||
UPDATE items
|
||||
SET dyeability = CASE
|
||||
WHEN dual_dyeable THEN 2
|
||||
|
||||
@@ -2718,10 +2718,8 @@ function pokemonProjection(locale: string): string {
|
||||
const pokemonDetails = localizedField('pokemon', 'p.id', 'p.details', 'details', locale);
|
||||
const typeName = localizedName('pokemon-types', 'pt', locale);
|
||||
const environmentName = localizedName('environments', 'e', locale);
|
||||
const environmentOppositeName = localizedName('environments', 'environment_opposite', locale);
|
||||
const skillName = localizedName('skills', 's', locale);
|
||||
const favoriteThingName = localizedName('favorite-things', 'ft', locale);
|
||||
const favoriteThingOppositeName = localizedName('favorite-things', 'opposite_ft', locale);
|
||||
|
||||
return `
|
||||
SELECT
|
||||
@@ -2754,12 +2752,7 @@ function pokemonProjection(locale: string): string {
|
||||
json_build_object(
|
||||
'id', e.id,
|
||||
'name', ${environmentName},
|
||||
'description', e.description,
|
||||
'opposite',
|
||||
CASE
|
||||
WHEN environment_opposite.id IS NULL THEN NULL
|
||||
ELSE json_build_object('id', environment_opposite.id, 'name', ${environmentOppositeName})
|
||||
END
|
||||
'description', e.description
|
||||
) AS environment,
|
||||
COALESCE((
|
||||
SELECT json_agg(json_build_object('id', pt.id, 'name', ${typeName}) ORDER BY ppt.slot_order)
|
||||
@@ -2777,23 +2770,16 @@ function pokemonProjection(locale: string): string {
|
||||
SELECT json_agg(
|
||||
json_build_object(
|
||||
'id', ft.id,
|
||||
'name', ${favoriteThingName},
|
||||
'opposite',
|
||||
CASE
|
||||
WHEN opposite_ft.id IS NULL THEN NULL
|
||||
ELSE json_build_object('id', opposite_ft.id, 'name', ${favoriteThingOppositeName})
|
||||
END
|
||||
'name', ${favoriteThingName}
|
||||
)
|
||||
ORDER BY ${orderByEntity('ft')}
|
||||
)
|
||||
FROM pokemon_favorite_things pft
|
||||
JOIN favorite_things ft ON ft.id = pft.favorite_thing_id
|
||||
LEFT JOIN favorite_things opposite_ft ON opposite_ft.id = ft.opposite_favorite_thing_id
|
||||
WHERE pft.pokemon_id = p.id
|
||||
), '[]'::json) AS favorite_things
|
||||
FROM pokemon p
|
||||
JOIN environments e ON e.id = p.environment_id
|
||||
LEFT JOIN environments environment_opposite ON environment_opposite.id = e.opposite_environment_id
|
||||
${auditJoins('p', 'pokemon_created_user', 'pokemon_updated_user')}
|
||||
`;
|
||||
}
|
||||
@@ -5415,6 +5401,69 @@ export function isConfigType(type: string): type is ConfigType {
|
||||
return Object.hasOwn(configDefinitions, type);
|
||||
}
|
||||
|
||||
async function syncConfigOpposite(
|
||||
client: DbClient,
|
||||
definition: ConfigDefinition,
|
||||
id: number,
|
||||
oppositeId: number | null,
|
||||
userId: number
|
||||
): Promise<void> {
|
||||
const oppositeColumn = definition.oppositeColumn;
|
||||
if (!oppositeColumn) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (oppositeId === id) {
|
||||
throw validationError('server.validation.invalidField');
|
||||
}
|
||||
|
||||
if (oppositeId !== null) {
|
||||
const opposite = await client.query<{ id: number }>(
|
||||
`SELECT id FROM ${definition.table} WHERE id = $1 FOR UPDATE`,
|
||||
[oppositeId]
|
||||
);
|
||||
if (opposite.rowCount === 0) {
|
||||
throw validationError('server.validation.selectRecord');
|
||||
}
|
||||
}
|
||||
|
||||
await client.query(
|
||||
`
|
||||
UPDATE ${definition.table}
|
||||
SET ${oppositeColumn} = NULL,
|
||||
updated_by_user_id = $2,
|
||||
updated_at = now()
|
||||
WHERE id <> $1
|
||||
AND (${oppositeColumn} = $1 OR (${oppositeId === null ? 'FALSE' : `${oppositeColumn} = $3`}))
|
||||
`,
|
||||
oppositeId === null ? [id, userId] : [id, userId, oppositeId]
|
||||
);
|
||||
|
||||
await client.query(
|
||||
`
|
||||
UPDATE ${definition.table}
|
||||
SET ${oppositeColumn} = $1,
|
||||
updated_by_user_id = $2,
|
||||
updated_at = now()
|
||||
WHERE id = $3
|
||||
`,
|
||||
[oppositeId, userId, id]
|
||||
);
|
||||
|
||||
if (oppositeId !== null) {
|
||||
await client.query(
|
||||
`
|
||||
UPDATE ${definition.table}
|
||||
SET ${oppositeColumn} = $1,
|
||||
updated_by_user_id = $2,
|
||||
updated_at = now()
|
||||
WHERE id = $3
|
||||
`,
|
||||
[id, userId, oppositeId]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function listConfig(type: ConfigType, locale = defaultLocale) {
|
||||
const definition = configDefinitions[type];
|
||||
return query(
|
||||
@@ -5460,10 +5509,6 @@ export async function createConfig(type: ConfigType, payload: Record<string, unk
|
||||
columns.push('description');
|
||||
values.push(description);
|
||||
}
|
||||
if (definition.oppositeColumn) {
|
||||
columns.push(definition.oppositeColumn);
|
||||
values.push(oppositeId);
|
||||
}
|
||||
if (definition.hasItemDrop) {
|
||||
columns.push('has_item_drop');
|
||||
values.push(hasItemDrop);
|
||||
@@ -5489,6 +5534,7 @@ export async function createConfig(type: ConfigType, payload: Record<string, unk
|
||||
);
|
||||
|
||||
const createdId = result.rows[0].id;
|
||||
await syncConfigOpposite(client, definition, createdId, oppositeId, userId);
|
||||
await replaceEntityTranslations(client, definition.entityType, createdId, translations, ['name']);
|
||||
await recordEditLog(client, type, createdId, 'create', userId);
|
||||
return createdId;
|
||||
@@ -5539,10 +5585,6 @@ export async function updateConfig(
|
||||
values.push(description);
|
||||
assignments.push(`description = $${values.length}`);
|
||||
}
|
||||
if (definition.oppositeColumn) {
|
||||
values.push(oppositeId);
|
||||
assignments.push(`${definition.oppositeColumn} = $${values.length}`);
|
||||
}
|
||||
if (definition.hasItemDrop) {
|
||||
values.push(hasItemDrop);
|
||||
assignments.push(`has_item_drop = $${values.length}`);
|
||||
@@ -5571,6 +5613,8 @@ export async function updateConfig(
|
||||
return false;
|
||||
}
|
||||
|
||||
await syncConfigOpposite(client, definition, id, oppositeId, userId);
|
||||
|
||||
if (definition.hasItemDrop && !hasItemDrop) {
|
||||
await client.query('DELETE FROM pokemon_skill_item_drops WHERE skill_id = $1', [id]);
|
||||
}
|
||||
@@ -5730,7 +5774,6 @@ export async function getPokemon(id: number, locale = defaultLocale) {
|
||||
const relatedEnvironmentName = localizedName('environments', 'related_environment', locale);
|
||||
const relatedSkillName = localizedName('skills', 'related_skill', locale);
|
||||
const relatedFavoriteThingName = localizedName('favorite-things', 'related_favorite_thing', locale);
|
||||
const relatedFavoriteThingOppositeName = localizedName('favorite-things', 'related_favorite_thing_opposite', locale);
|
||||
const tradingItemName = localizedName('items', 'trading_item', locale);
|
||||
|
||||
const [habitats, itemDrops, favoriteThingItems, tradingItems, relatedPokemon, editHistory, imageHistory] = await Promise.all([
|
||||
@@ -5894,11 +5937,6 @@ export async function getPokemon(id: number, locale = defaultLocale) {
|
||||
json_build_object(
|
||||
'id', related_favorite_thing.id,
|
||||
'name', ${relatedFavoriteThingName},
|
||||
'opposite',
|
||||
CASE
|
||||
WHEN related_favorite_thing_opposite.id IS NULL THEN NULL
|
||||
ELSE json_build_object('id', related_favorite_thing_opposite.id, 'name', ${relatedFavoriteThingOppositeName})
|
||||
END,
|
||||
'matches', EXISTS (
|
||||
SELECT 1
|
||||
FROM current_favourites
|
||||
@@ -5915,8 +5953,6 @@ export async function getPokemon(id: number, locale = defaultLocale) {
|
||||
)
|
||||
FROM pokemon_favorite_things related_pokemon_favourite
|
||||
JOIN favorite_things related_favorite_thing ON related_favorite_thing.id = related_pokemon_favourite.favorite_thing_id
|
||||
LEFT JOIN favorite_things related_favorite_thing_opposite
|
||||
ON related_favorite_thing_opposite.id = related_favorite_thing.opposite_favorite_thing_id
|
||||
WHERE related_pokemon_favourite.pokemon_id = related_pokemon.id
|
||||
), '[]'::json) AS favorite_things
|
||||
FROM scored_pokemon
|
||||
|
||||
Reference in New Issue
Block a user