refactor(life): remove life categories and ratings

Drop life_tags and life_post_ratings tables and related schema
Remove category selection and rating UI from Life posts
Simplify Life feed filters and API endpoints
This commit is contained in:
2026-05-07 15:38:32 +08:00
parent e9d356a656
commit a781bc559b
11 changed files with 89 additions and 696 deletions

View File

@@ -136,7 +136,6 @@ type EntityType =
| 'maps'
| 'habitats'
| 'daily-checklist-items'
| 'life-tags'
| 'game-versions'
| 'dish-categories'
| 'dish-flavors'
@@ -149,7 +148,6 @@ type ConfigType =
| 'favorite-things'
| 'acquisition-methods'
| 'maps'
| 'life-tags'
| 'game-versions'
| 'dish-flavors';
@@ -158,8 +156,6 @@ type ConfigDefinition = {
entityType: EntityType;
hasItemDrop?: boolean;
hasTrading?: boolean;
hasDefault?: boolean;
hasRateable?: boolean;
hasChangeLog?: boolean;
};
type SortableContentType = 'items' | 'ancient-artifacts' | 'recipes' | 'habitats';
@@ -330,7 +326,6 @@ type DailyChecklistPayload = {
type LifePostPayload = {
body: string;
categoryId: number;
gameVersionId: number | null;
languageCode: string | null;
};
@@ -427,10 +422,7 @@ type LifePostRow = {
updatedAt: Date;
author: { id: number; displayName: string } | null;
updatedBy: { id: number; displayName: string } | null;
category: { id: number; name: string; isRateable: boolean } | null;
gameVersion: { id: number; name: string; changeLog: string } | null;
ratingAverage: number | null;
ratingCount: number;
};
type LifePost = Omit<LifePostRow, 'createdAtCursor'> & {
@@ -438,16 +430,14 @@ type LifePost = Omit<LifePostRow, 'createdAtCursor'> & {
commentCount: number;
reactionCounts: LifeReactionCounts;
myReaction: LifeReactionType | null;
myRating: number | null;
};
type LifePostCursor = {
createdAt: string;
id: number;
ratingAverage?: number;
};
type LifePostSort = 'latest' | 'oldest' | 'top-rated';
type LifePostSort = 'latest' | 'oldest';
type CommentSort = 'oldest' | 'latest' | 'most-liked' | 'most-replied';
type CommentCursor = {
createdAt: string;
@@ -668,8 +658,6 @@ type ConfigChangeSource = {
name: string;
hasItemDrop?: boolean;
hasTrading?: boolean;
isDefault?: boolean;
isRateable?: boolean;
changeLog?: string;
} & TranslationChangeSource;
@@ -738,7 +726,6 @@ const configDefinitions: Record<ConfigType, ConfigDefinition> = {
'favorite-things': { table: 'favorite_things', entityType: 'favorite-things' },
'acquisition-methods': { table: 'acquisition_methods', entityType: 'acquisition-methods' },
maps: { table: 'maps', entityType: 'maps' },
'life-tags': { table: 'life_tags', entityType: 'life-tags', hasDefault: true, hasRateable: true },
'game-versions': { table: 'game_versions', entityType: 'game-versions', hasChangeLog: true },
'dish-flavors': { table: 'dish_flavors', entityType: 'dish-flavors' }
};
@@ -1081,13 +1068,6 @@ function systemListJsonSql(expression: string, options: readonly SystemListOptio
return `json_build_object('id', ${systemListIdSql(expression, options)}, 'key', ${expression}, 'name', ${systemListNameSql(expression, options, locale)})`;
}
function lifeCategoryOptions(locale: string): Promise<Array<{ id: number; name: string; isDefault: boolean; isRateable: boolean }>> {
const name = localizedName('life-tags', 'lc', locale);
return query(
`SELECT lc.id, ${name} AS name, lc.is_default AS "isDefault", lc.is_rateable AS "isRateable" FROM life_tags lc ORDER BY ${orderByEntity('lc')}`
);
}
function gameVersionOptions(locale: string): Promise<Array<{ id: number; name: string; changeLog: string }>> {
const name = localizedName('game-versions', 'gv', locale);
return query(`SELECT gv.id, ${name} AS name, gv.change_log AS "changeLog" FROM game_versions gv ORDER BY ${orderByEntity('gv')}`);
@@ -1136,12 +1116,6 @@ function configSelect(definition: ConfigDefinition, locale: string): string {
if (definition.hasTrading) {
columns.push(`c.has_trading AS "hasTrading"`);
}
if (definition.hasDefault) {
columns.push(`c.is_default AS "isDefault"`);
}
if (definition.hasRateable) {
columns.push(`c.is_rateable AS "isRateable"`);
}
if (definition.hasChangeLog) {
columns.push(`c.change_log AS "changeLog"`);
}
@@ -2096,7 +2070,7 @@ async function ensurePokemonTypeCatalog(
const changes = configEditChanges(
{ table: 'pokemon_types', entityType: 'pokemon-types' },
existing.rows[0],
{ name, translations, hasItemDrop: false, hasTrading: false, isDefault: false, isRateable: false, changeLog: '' }
{ name, translations, hasItemDrop: false, hasTrading: false, changeLog: '' }
);
if (changes.length) {
await client.query(
@@ -2665,8 +2639,6 @@ function configEditChanges(
translations: TranslationInput;
hasItemDrop: boolean;
hasTrading: boolean;
isDefault: boolean;
isRateable: boolean;
changeLog: string;
}
): EditChange[] {
@@ -2679,12 +2651,6 @@ function configEditChanges(
if (definition.hasTrading) {
pushChange(changes, 'Has trading', boolValue(Boolean(before.hasTrading)), boolValue(after.hasTrading));
}
if (definition.hasDefault) {
pushChange(changes, 'Default category', boolValue(Boolean(before.isDefault)), boolValue(after.isDefault));
}
if (definition.hasRateable) {
pushChange(changes, 'Rateable', boolValue(Boolean(before.isRateable)), boolValue(after.isRateable));
}
if (definition.hasChangeLog) {
pushChange(changes, 'ChangeLog', before.changeLog, after.changeLog);
}
@@ -2782,7 +2748,6 @@ export async function getOptions(locale = defaultLocale) {
favoriteThings,
acquisitionMethods,
maps,
lifeCategories,
gameVersions,
dishFlavors
] = await Promise.all([
@@ -2792,7 +2757,6 @@ export async function getOptions(locale = defaultLocale) {
optionSelect('favorite_things', 'favorite-things', locale),
optionSelect('acquisition_methods', 'acquisition-methods', locale),
optionSelect('maps', 'maps', locale),
lifeCategoryOptions(locale),
gameVersionOptions(locale),
optionSelect('dish_flavors', 'dish-flavors', locale)
]);
@@ -2808,7 +2772,6 @@ export async function getOptions(locale = defaultLocale) {
acquisitionMethods,
itemTags: favoriteThings,
maps,
lifeCategories,
gameVersions,
dishFlavors
};
@@ -2851,8 +2814,6 @@ export async function globalSearch(paramsQuery: QueryParams = {}, locale = defau
const recipeItemName = localizedName('items', 'result_item', locale);
const recipeMaterialName = localizedName('items', 'material_item', locale);
const checklistTitle = localizedField('daily-checklist-items', 'c.id', 'c.title', 'title', locale);
const lifeCategoryName = localizedName('life-tags', 'lc', locale);
const [pokemon, habitats, items, artifacts, recipes, checklist, life, users] = await Promise.all([
query<GlobalSearchItem>(
`
@@ -2981,10 +2942,9 @@ export async function globalSearch(paramsQuery: QueryParams = {}, locale = defau
LEFT(lp.body, 120) AS title,
'/life/' || lp.id AS url,
NULL AS summary,
${lifeCategoryName} AS meta,
NULL AS meta,
NULL AS image
FROM life_posts lp
LEFT JOIN life_tags lc ON lc.id = lp.category_id
WHERE lp.deleted_at IS NULL
AND lp.ai_moderation_status = 'approved'
AND lp.body ILIKE $1
@@ -3153,12 +3113,10 @@ function cleanLifePostPayload(payload: Record<string, unknown>): LifePostPayload
if (body.length > 2000) {
throw validationError('server.validation.postTooLong');
}
const categoryId = requirePositiveInteger(payload.categoryId, 'server.validation.lifeCategoryRequired');
const gameVersionId = optionalPositiveInteger(payload.gameVersionId, 'server.validation.gameVersionInvalid');
return {
body,
categoryId,
gameVersionId,
languageCode: cleanModerationLanguageCode(payload.languageCode)
};
@@ -3203,15 +3161,6 @@ function cleanLifeReactionFilter(value: QueryValue): LifeReactionType | null {
return cleanLifeReactionType(reactionType);
}
function cleanLifeRating(value: unknown): number {
const rating = Number(value);
if (!Number.isInteger(rating) || rating < 1 || rating > 5) {
throw validationError('server.validation.ratingInvalid');
}
return rating;
}
function cleanUserCommentActivitySourceFilter(value: QueryValue): UserCommentActivitySource | null {
const source = asString(value);
if (!source) {
@@ -3279,7 +3228,6 @@ function addModerationLanguageCondition(
}
function lifePostProjection(locale = defaultLocale): string {
const categoryName = localizedName('life-tags', 'lc', locale);
const gameVersionName = localizedName('game-versions', 'gv', locale);
return `
@@ -3300,29 +3248,12 @@ function lifePostProjection(locale = defaultLocale): string {
WHEN updated_user.id IS NULL THEN NULL
ELSE json_build_object('id', updated_user.id, 'displayName', updated_user.display_name)
END AS "updatedBy",
CASE
WHEN lc.id IS NULL THEN NULL
ELSE json_build_object('id', lc.id, 'name', ${categoryName}, 'isRateable', lc.is_rateable)
END AS category,
CASE
WHEN gv.id IS NULL THEN NULL
ELSE json_build_object('id', gv.id, 'name', ${gameVersionName}, 'changeLog', gv.change_log)
END AS "gameVersion",
CASE
WHEN rating_stats.rating_count = 0 THEN NULL
ELSE rating_stats.rating_average::double precision
END AS "ratingAverage",
rating_stats.rating_count AS "ratingCount"
END AS "gameVersion"
FROM life_posts lp
LEFT JOIN life_tags lc ON lc.id = lp.category_id
LEFT JOIN game_versions gv ON gv.id = lp.game_version_id
LEFT JOIN LATERAL (
SELECT
ROUND(AVG(lpr.rating)::numeric, 2) AS rating_average,
COUNT(*)::integer AS rating_count
FROM life_post_ratings lpr
WHERE lpr.post_id = lp.id
) rating_stats ON true
LEFT JOIN users created_user ON created_user.id = lp.created_by_user_id
LEFT JOIN users updated_user ON updated_user.id = lp.updated_by_user_id
`;
@@ -3340,18 +3271,7 @@ function cleanLifePostLimit(value: QueryValue): number {
function cleanLifePostSort(value: QueryValue): LifePostSort {
const sort = asString(value);
return sort === 'oldest' || sort === 'top-rated' ? sort : 'latest';
}
function cleanRateableFilter(value: QueryValue): boolean | null {
const rateable = asString(value);
if (rateable === 'true') {
return true;
}
if (rateable === 'false') {
return false;
}
return null;
return sort === 'oldest' ? sort : 'latest';
}
function cleanCommentLimit(value: QueryValue): number {
@@ -3460,19 +3380,17 @@ function decodeLifePostCursor(value: QueryValue): LifePostCursor | null {
const cursor = JSON.parse(Buffer.from(rawCursor, 'base64url').toString('utf8')) as Partial<LifePostCursor>;
const createdAt = typeof cursor.createdAt === 'string' ? cursor.createdAt : '';
const id = Number(cursor.id);
const ratingAverage = cursor.ratingAverage === undefined ? undefined : Number(cursor.ratingAverage);
if (
!createdAt ||
Number.isNaN(new Date(createdAt).getTime()) ||
!Number.isInteger(id) ||
id <= 0 ||
(ratingAverage !== undefined && (Number.isNaN(ratingAverage) || ratingAverage < 0))
id <= 0
) {
throw validationError('server.validation.cursorInvalid');
}
return { createdAt, id, ratingAverage };
return { createdAt, id };
} catch (error) {
if (error instanceof Error && 'statusCode' in error) {
throw error;
@@ -3482,10 +3400,7 @@ function decodeLifePostCursor(value: QueryValue): LifePostCursor | null {
}
function encodeLifePostCursor(post: LifePostRow): string {
return Buffer.from(
JSON.stringify({ createdAt: post.createdAtCursor, id: post.id, ratingAverage: post.ratingAverage ?? 0 }),
'utf8'
).toString('base64url');
return Buffer.from(JSON.stringify({ createdAt: post.createdAtCursor, id: post.id }), 'utf8').toString('base64url');
}
function encodeProfileCursor(cursor: LifePostCursor): string {
@@ -3560,8 +3475,7 @@ function hydrateLifePost(
commentPreviewByPost: Map<number, LifeComment[]>,
commentCountsByPost: Map<number, number>,
countsByPost: Map<number, LifeReactionCounts>,
myReactionsByPost: Map<number, LifeReactionType>,
myRatingsByPost: Map<number, number>
myReactionsByPost: Map<number, LifeReactionType>
): LifePost {
return {
id: post.id,
@@ -3573,15 +3487,11 @@ function hydrateLifePost(
updatedAt: post.updatedAt,
author: post.author,
updatedBy: post.updatedBy,
category: post.category,
gameVersion: post.gameVersion,
ratingAverage: post.ratingAverage,
ratingCount: post.ratingCount,
commentPreview: commentPreviewByPost.get(post.id) ?? [],
commentCount: commentCountsByPost.get(post.id) ?? 0,
reactionCounts: countsByPost.get(post.id) ?? emptyLifeReactionCounts(),
myReaction: myReactionsByPost.get(post.id) ?? null,
myRating: myRatingsByPost.get(post.id) ?? null
myReaction: myReactionsByPost.get(post.id) ?? null
};
}
@@ -4004,30 +3914,6 @@ export async function listLifePostReactionUsers(
};
}
async function lifeRatingsForPosts(postIds: number[], userId: number | null): Promise<Map<number, number>> {
const myRatingsByPost = new Map<number, number>();
if (postIds.length === 0 || userId === null) {
return myRatingsByPost;
}
const rows = await query<{ postId: number; rating: number }>(
`
SELECT post_id AS "postId", rating
FROM life_post_ratings
WHERE post_id = ANY($1::integer[])
AND user_id = $2
`,
[postIds, userId]
);
for (const row of rows) {
myRatingsByPost.set(row.postId, row.rating);
}
return myRatingsByPost;
}
async function getLifeCommentById(id: number, userId: number | null = null, canViewAll = false): Promise<LifeComment | null> {
const row = await queryOne<LifeCommentRow>(
`
@@ -4055,9 +3941,7 @@ async function listLifePostsWithFilters(
const limit = cleanLifePostLimit(paramsQuery.limit);
const sort = cleanLifePostSort(paramsQuery.sort);
const search = asString(paramsQuery.search)?.trim();
const categoryIdValue = asString(paramsQuery.categoryId)?.trim();
const gameVersionIdValue = asString(paramsQuery.gameVersionId)?.trim();
const rateable = cleanRateableFilter(paramsQuery.rateable);
const languageCode = cleanModerationLanguageFilter(paramsQuery.language);
const params: unknown[] = [];
const conditions: string[] = ['lp.deleted_at IS NULL'];
@@ -4087,41 +3971,20 @@ async function listLifePostsWithFilters(
conditions.push(`lp.body ILIKE $${params.length}`);
}
if (categoryIdValue) {
const categoryId = requirePositiveInteger(categoryIdValue, 'server.validation.lifeCategoryInvalid');
params.push(categoryId);
conditions.push(`lp.category_id = $${params.length}`);
}
if (gameVersionIdValue && gameVersionIdValue !== 'all') {
const gameVersionId = requirePositiveInteger(gameVersionIdValue, 'server.validation.gameVersionInvalid');
params.push(gameVersionId);
conditions.push(`lp.game_version_id = $${params.length}`);
}
if (rateable !== null) {
params.push(rateable);
conditions.push(`lc.is_rateable = $${params.length}`);
}
if (cursor) {
if (sort === 'top-rated') {
params.push(cursor.ratingAverage ?? 0, cursor.createdAt, cursor.id);
conditions.push(
`(COALESCE(rating_stats.rating_average, 0), lp.created_at, lp.id) < ($${params.length - 2}::numeric, $${params.length - 1}::timestamptz, $${params.length}::integer)`
);
} else {
params.push(cursor.createdAt, cursor.id);
conditions.push(
`(lp.created_at, lp.id) ${sort === 'oldest' ? '>' : '<'} ($${params.length - 1}::timestamptz, $${params.length}::integer)`
);
}
params.push(cursor.createdAt, cursor.id);
conditions.push(
`(lp.created_at, lp.id) ${sort === 'oldest' ? '>' : '<'} ($${params.length - 1}::timestamptz, $${params.length}::integer)`
);
}
const orderClause =
sort === 'top-rated'
? 'ORDER BY COALESCE(rating_stats.rating_average, 0) DESC, lp.created_at DESC, lp.id DESC'
: `ORDER BY lp.created_at ${sort === 'oldest' ? 'ASC' : 'DESC'}, lp.id ${sort === 'oldest' ? 'ASC' : 'DESC'}`;
const orderClause = `ORDER BY lp.created_at ${sort === 'oldest' ? 'ASC' : 'DESC'}, lp.id ${sort === 'oldest' ? 'ASC' : 'DESC'}`;
const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
params.push(limit + 1);
const rows = await query<LifePostRow>(
@@ -4140,11 +4003,10 @@ async function listLifePostsWithFilters(
const commentPreviewByPost = await lifeCommentPreviewForPosts(postIds, userId, canViewAll);
const commentCountsByPost = await lifeCommentCountsForPosts(postIds, userId, canViewAll);
const { countsByPost, myReactionsByPost } = await lifeReactionsForPosts(postIds, userId);
const myRatingsByPost = await lifeRatingsForPosts(postIds, userId);
return {
items: posts.map((post) =>
hydrateLifePost(post, commentPreviewByPost, commentCountsByPost, countsByPost, myReactionsByPost, myRatingsByPost)
hydrateLifePost(post, commentPreviewByPost, commentCountsByPost, countsByPost, myReactionsByPost)
),
nextCursor: hasMore && posts.length > 0 ? encodeLifePostCursor(posts[posts.length - 1]) : null,
hasMore
@@ -4429,10 +4291,9 @@ async function hydrateLifePostsById(
const commentPreviewByPost = await lifeCommentPreviewForPosts(postIds, viewerUserId, canViewAll);
const commentCountsByPost = await lifeCommentCountsForPosts(postIds, viewerUserId, canViewAll);
const { countsByPost, myReactionsByPost } = await lifeReactionsForPosts(postIds, viewerUserId);
const myRatingsByPost = await lifeRatingsForPosts(postIds, viewerUserId);
for (const post of posts) {
postById.set(post.id, hydrateLifePost(post, commentPreviewByPost, commentCountsByPost, countsByPost, myReactionsByPost, myRatingsByPost));
postById.set(post.id, hydrateLifePost(post, commentPreviewByPost, commentCountsByPost, countsByPost, myReactionsByPost));
}
return postById;
@@ -4693,8 +4554,7 @@ async function getLifePostById(
const commentPreviewByPost = await lifeCommentPreviewForPosts([post.id], userId, canViewAll);
const commentCountsByPost = await lifeCommentCountsForPosts([post.id], userId, canViewAll);
const { countsByPost, myReactionsByPost } = await lifeReactionsForPosts([post.id], userId);
const myRatingsByPost = await lifeRatingsForPosts([post.id], userId);
return hydrateLifePost(post, commentPreviewByPost, commentCountsByPost, countsByPost, myReactionsByPost, myRatingsByPost);
return hydrateLifePost(post, commentPreviewByPost, commentCountsByPost, countsByPost, myReactionsByPost);
}
export async function getLifePost(
@@ -4707,13 +4567,6 @@ export async function getLifePost(
return getLifePostById(id, userId, locale, { enforceVisibility: true, canViewAll });
}
async function ensureLifeCategory(client: DbClient, categoryId: number): Promise<void> {
const result = await client.query<{ id: number }>('SELECT id FROM life_tags WHERE id = $1', [categoryId]);
if (result.rowCount === 0) {
throw validationError('server.validation.lifeCategoryInvalid');
}
}
async function ensureGameVersion(client: DbClient, gameVersionId: number | null): Promise<void> {
if (gameVersionId === null) {
return;
@@ -4725,43 +4578,28 @@ async function ensureGameVersion(client: DbClient, gameVersionId: number | null)
}
}
async function replaceLifePostCategoryLink(client: DbClient, postId: number, categoryId: number): Promise<void> {
await client.query('DELETE FROM life_post_tags WHERE post_id = $1', [postId]);
await client.query(
`
INSERT INTO life_post_tags (post_id, tag_id)
VALUES ($1, $2)
`,
[postId, categoryId]
);
}
export async function createLifePost(payload: Record<string, unknown>, userId: number, locale = defaultLocale) {
const cleanPayload = cleanLifePostPayload(payload);
const id = await withTransaction(async (client) => {
await ensureLifeCategory(client, cleanPayload.categoryId);
await ensureGameVersion(client, cleanPayload.gameVersionId);
const result = await client.query<{ id: number }>(
`
INSERT INTO life_posts (
body,
category_id,
game_version_id,
ai_moderation_status,
ai_moderation_language_code,
created_by_user_id,
updated_by_user_id
)
VALUES ($1, $2, $3, 'reviewing', NULL, $4, $4)
VALUES ($1, $2, 'reviewing', NULL, $3, $3)
RETURNING id
`,
[cleanPayload.body, cleanPayload.categoryId, cleanPayload.gameVersionId, userId]
[cleanPayload.body, cleanPayload.gameVersionId, userId]
);
const createdId = result.rows[0].id;
await replaceLifePostCategoryLink(client, createdId, cleanPayload.categoryId);
return createdId;
return result.rows[0].id;
});
await requestAiModerationReview({ type: 'life-post', id }, { languageCode: cleanPayload.languageCode, resetRetries: true });
@@ -4778,37 +4616,29 @@ export async function updateLifePost(
const cleanPayload = cleanLifePostPayload(payload);
const updatedId = await withTransaction(async (client) => {
await ensureLifeCategory(client, cleanPayload.categoryId);
await ensureGameVersion(client, cleanPayload.gameVersionId);
const result = await client.query<{ id: number }>(
`
UPDATE life_posts
SET body = $1,
category_id = $2,
game_version_id = $3,
game_version_id = $2,
ai_moderation_status = 'reviewing',
ai_moderation_language_code = NULL,
ai_moderation_content_hash = NULL,
ai_moderation_checked_at = NULL,
ai_moderation_retry_count = 0,
ai_moderation_updated_at = now(),
updated_by_user_id = $4,
updated_by_user_id = $3,
updated_at = now()
WHERE id = $5
AND ($6 = true OR created_by_user_id = $4)
WHERE id = $4
AND ($5 = true OR created_by_user_id = $3)
AND deleted_at IS NULL
RETURNING id
`,
[cleanPayload.body, cleanPayload.categoryId, cleanPayload.gameVersionId, userId, id, allowAny]
[cleanPayload.body, cleanPayload.gameVersionId, userId, id, allowAny]
);
const resultId = result.rows[0]?.id ?? null;
if (resultId === null) {
return null;
}
await replaceLifePostCategoryLink(client, resultId, cleanPayload.categoryId);
return resultId;
return result.rows[0]?.id ?? null;
});
if (updatedId) {
@@ -4916,57 +4746,6 @@ export async function deleteLifePostReaction(postId: number, userId: number, loc
return getLifePostById(postId, userId, locale);
}
export async function setLifePostRating(
postId: number,
payload: Record<string, unknown>,
userId: number,
locale = defaultLocale
) {
const rating = cleanLifeRating(payload.rating);
const result = await queryOne<{ postId: number }>(
`
INSERT INTO life_post_ratings (post_id, user_id, rating)
SELECT $1, $2, $3
FROM life_posts lp
JOIN life_tags lt ON lt.id = lp.category_id
WHERE lp.id = $1
AND lp.deleted_at IS NULL
AND lp.ai_moderation_status = 'approved'
AND lt.is_rateable = true
ON CONFLICT (post_id, user_id)
DO UPDATE SET rating = EXCLUDED.rating, updated_at = now()
RETURNING post_id AS "postId"
`,
[postId, userId, rating]
);
return result ? getLifePostById(result.postId, userId, locale) : null;
}
export async function deleteLifePostRating(postId: number, userId: number, locale = defaultLocale) {
const result = await queryOne<{ postId: number }>(
`
DELETE FROM life_post_ratings
WHERE post_id = $1
AND user_id = $2
AND EXISTS (
SELECT 1
FROM life_posts lp
JOIN life_tags lt ON lt.id = lp.category_id
WHERE lp.id = $1
AND lp.deleted_at IS NULL
AND lp.ai_moderation_status = 'approved'
AND lt.is_rateable = true
)
RETURNING post_id AS "postId"
`,
[postId, userId]
);
return result ? getLifePostById(postId, userId, locale) : null;
}
export async function createLifeComment(postId: number, payload: Record<string, unknown>, userId: number) {
const cleanPayload = cleanLifeCommentPayload(payload);
@@ -5609,18 +5388,10 @@ export async function createConfig(type: ConfigType, payload: Record<string, unk
const translations = cleanTranslations(payload.translations, ['name']);
const hasItemDrop = definition.hasItemDrop ? Boolean(payload.hasItemDrop) : false;
const hasTrading = definition.hasTrading ? Boolean(payload.hasTrading) : false;
const isDefault = definition.hasDefault ? Boolean(payload.isDefault) : false;
const isRateable = definition.hasRateable ? Boolean(payload.isRateable) : false;
const changeLog = definition.hasChangeLog ? cleanOptionalText(payload.changeLog) : '';
const id = await withTransaction(async (client) => {
const sortOrder = await nextSortOrder(client, definition.table);
if (definition.hasDefault && isDefault) {
await client.query(
`UPDATE ${definition.table} SET is_default = false, updated_by_user_id = $1, updated_at = now() WHERE is_default = true`,
[userId]
);
}
const columns = ['name'];
const values: unknown[] = [name];
if (definition.hasItemDrop) {
@@ -5631,14 +5402,6 @@ export async function createConfig(type: ConfigType, payload: Record<string, unk
columns.push('has_trading');
values.push(hasTrading);
}
if (definition.hasDefault) {
columns.push('is_default');
values.push(isDefault);
}
if (definition.hasRateable) {
columns.push('is_rateable');
values.push(isRateable);
}
if (definition.hasChangeLog) {
columns.push('change_log');
values.push(changeLog);
@@ -5690,19 +5453,10 @@ export async function updateConfig(
const translations = cleanTranslations(payload.translations, ['name']);
const hasItemDrop = definition.hasItemDrop ? Boolean(payload.hasItemDrop) : false;
const hasTrading = definition.hasTrading ? Boolean(payload.hasTrading) : false;
const isDefault = definition.hasDefault ? Boolean(payload.isDefault) : false;
const isRateable = definition.hasRateable ? Boolean(payload.isRateable) : false;
const changeLog = definition.hasChangeLog ? cleanOptionalText(payload.changeLog) : '';
const before = await getConfigById(type, id, defaultLocale);
const updated = await withTransaction(async (client) => {
if (definition.hasDefault && isDefault) {
await client.query(
`UPDATE ${definition.table} SET is_default = false, updated_by_user_id = $2, updated_at = now() WHERE id <> $1 AND is_default = true`,
[id, userId]
);
}
const assignments = ['name = $1'];
const values: unknown[] = [name];
if (definition.hasItemDrop) {
@@ -5713,14 +5467,6 @@ export async function updateConfig(
values.push(hasTrading);
assignments.push(`has_trading = $${values.length}`);
}
if (definition.hasDefault) {
values.push(isDefault);
assignments.push(`is_default = $${values.length}`);
}
if (definition.hasRateable) {
values.push(isRateable);
assignments.push(`is_rateable = $${values.length}`);
}
if (definition.hasChangeLog) {
values.push(changeLog);
assignments.push(`change_log = $${values.length}`);
@@ -5768,7 +5514,7 @@ export async function updateConfig(
await replaceEntityTranslations(client, definition.entityType, id, translations, ['name']);
const changes = before
? configEditChanges(definition, before as ConfigChangeSource, { name, translations, hasItemDrop, hasTrading, isDefault, isRateable, changeLog })
? configEditChanges(definition, before as ConfigChangeSource, { name, translations, hasItemDrop, hasTrading, changeLog })
: [];
await recordEditLog(client, type, id, 'update', userId, changes);
return true;