772 lines
20 KiB
JavaScript
772 lines
20 KiB
JavaScript
const mysql = require('mysql2/promise')
|
|
|
|
const DB_HOST = process.env.DB_HOST || '127.0.0.1'
|
|
const DB_PORT = process.env.DB_PORT ? Number(process.env.DB_PORT) : 3306
|
|
const DB_USER = process.env.DB_USER || 'root'
|
|
const DB_PASSWORD = process.env.DB_PASSWORD || ''
|
|
const DB_NAME = process.env.DB_NAME || 'tier_cursor'
|
|
const DB_CONNECTION_LIMIT = process.env.DB_CONNECTION_LIMIT ? Number(process.env.DB_CONNECTION_LIMIT) : 10
|
|
const FREEFORM_GAME_ID = 'freeform'
|
|
|
|
let poolPromise = null
|
|
let initPromise = null
|
|
|
|
function now() {
|
|
return Date.now()
|
|
}
|
|
|
|
function parseJson(value, fallback) {
|
|
if (!value) return fallback
|
|
try {
|
|
return JSON.parse(value)
|
|
} catch (e) {
|
|
return fallback
|
|
}
|
|
}
|
|
|
|
function serializeJson(value) {
|
|
return JSON.stringify(value || [])
|
|
}
|
|
|
|
function mapUserRow(row) {
|
|
if (!row) return null
|
|
return {
|
|
id: row.id,
|
|
email: row.email,
|
|
nickname: row.nickname || '',
|
|
isAdmin: !!row.is_admin,
|
|
avatarSrc: row.avatar_src || '',
|
|
createdAt: Number(row.created_at),
|
|
}
|
|
}
|
|
|
|
function mapGameRow(row) {
|
|
if (!row) return null
|
|
return {
|
|
id: row.id,
|
|
name: row.name,
|
|
thumbnailSrc: row.thumbnail_src || '',
|
|
displayRank: row.display_rank == null ? null : Number(row.display_rank),
|
|
createdAt: Number(row.created_at),
|
|
}
|
|
}
|
|
|
|
function mapGameItemRow(row) {
|
|
if (!row) return null
|
|
return {
|
|
id: row.id,
|
|
gameId: row.game_id,
|
|
src: row.src,
|
|
label: row.label,
|
|
createdAt: Number(row.created_at),
|
|
}
|
|
}
|
|
|
|
function mapTierListRow(row) {
|
|
if (!row) return null
|
|
return {
|
|
id: row.id,
|
|
authorId: row.author_id,
|
|
authorName: getUserDisplayName(row),
|
|
authorAccountName: getUserAccountName(row),
|
|
authorAvatarSrc: row.avatar_src || '',
|
|
gameId: row.game_id,
|
|
title: row.title,
|
|
description: row.description || '',
|
|
isPublic: !!row.is_public,
|
|
groups: parseJson(row.groups_json, []),
|
|
pool: parseJson(row.pool_json, []),
|
|
createdAt: Number(row.created_at),
|
|
updatedAt: Number(row.updated_at),
|
|
}
|
|
}
|
|
|
|
function getUserDisplayName(row) {
|
|
if (!row) return ''
|
|
const nickname = (row.nickname || '').trim()
|
|
if (nickname) return nickname
|
|
const email = (row.email || '').trim()
|
|
if (!email) return ''
|
|
return email.split('@')[0] || email
|
|
}
|
|
|
|
function getUserAccountName(row) {
|
|
if (!row) return ''
|
|
const email = (row.email || '').trim()
|
|
if (!email) return ''
|
|
return email.split('@')[0] || email
|
|
}
|
|
|
|
async function createPool() {
|
|
const rootConnection = await mysql.createConnection({
|
|
host: DB_HOST,
|
|
port: DB_PORT,
|
|
user: DB_USER,
|
|
password: DB_PASSWORD,
|
|
multipleStatements: true,
|
|
})
|
|
|
|
await rootConnection.query(
|
|
`CREATE DATABASE IF NOT EXISTS \`${DB_NAME}\` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci`
|
|
)
|
|
await rootConnection.end()
|
|
|
|
return mysql.createPool({
|
|
host: DB_HOST,
|
|
port: DB_PORT,
|
|
user: DB_USER,
|
|
password: DB_PASSWORD,
|
|
database: DB_NAME,
|
|
connectionLimit: DB_CONNECTION_LIMIT,
|
|
charset: 'utf8mb4',
|
|
})
|
|
}
|
|
|
|
async function getPool() {
|
|
if (!poolPromise) {
|
|
poolPromise = createPool()
|
|
}
|
|
return poolPromise
|
|
}
|
|
|
|
async function query(sql, params = []) {
|
|
const pool = await getPool()
|
|
const [rows] = await pool.execute(sql, params)
|
|
return rows
|
|
}
|
|
|
|
async function ensureSchema() {
|
|
if (initPromise) return initPromise
|
|
initPromise = (async () => {
|
|
await query(`
|
|
CREATE TABLE IF NOT EXISTS users (
|
|
id VARCHAR(64) PRIMARY KEY,
|
|
email VARCHAR(255) NOT NULL UNIQUE,
|
|
nickname VARCHAR(80) NOT NULL DEFAULT '',
|
|
password_hash VARCHAR(255) NOT NULL,
|
|
is_admin TINYINT(1) NOT NULL DEFAULT 0,
|
|
avatar_src VARCHAR(255) NOT NULL DEFAULT '',
|
|
created_at BIGINT NOT NULL
|
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
|
|
`)
|
|
|
|
await query(`
|
|
CREATE TABLE IF NOT EXISTS games (
|
|
id VARCHAR(120) PRIMARY KEY,
|
|
name VARCHAR(120) NOT NULL,
|
|
thumbnail_src VARCHAR(255) NOT NULL DEFAULT '',
|
|
display_rank INT NULL DEFAULT NULL,
|
|
created_at BIGINT NOT NULL
|
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
|
|
`)
|
|
|
|
const displayRankColumns = await query("SHOW COLUMNS FROM games LIKE 'display_rank'")
|
|
if (!displayRankColumns.length) {
|
|
await query('ALTER TABLE games ADD COLUMN display_rank INT NULL DEFAULT NULL AFTER thumbnail_src')
|
|
}
|
|
|
|
await query(`
|
|
CREATE TABLE IF NOT EXISTS game_items (
|
|
id VARCHAR(64) PRIMARY KEY,
|
|
game_id VARCHAR(120) NOT NULL,
|
|
src VARCHAR(255) NOT NULL,
|
|
label VARCHAR(120) NOT NULL,
|
|
created_at BIGINT NOT NULL,
|
|
INDEX idx_game_items_game_id (game_id),
|
|
CONSTRAINT fk_game_items_game FOREIGN KEY (game_id) REFERENCES games(id) ON DELETE CASCADE
|
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
|
|
`)
|
|
|
|
await query(`
|
|
CREATE TABLE IF NOT EXISTS custom_items (
|
|
id VARCHAR(64) PRIMARY KEY,
|
|
owner_id VARCHAR(64) NOT NULL,
|
|
src VARCHAR(255) NOT NULL,
|
|
label VARCHAR(120) NOT NULL,
|
|
created_at BIGINT NOT NULL,
|
|
INDEX idx_custom_items_owner_id (owner_id),
|
|
CONSTRAINT fk_custom_items_owner FOREIGN KEY (owner_id) REFERENCES users(id) ON DELETE CASCADE
|
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
|
|
`)
|
|
|
|
await query(`
|
|
CREATE TABLE IF NOT EXISTS tierlists (
|
|
id VARCHAR(64) PRIMARY KEY,
|
|
author_id VARCHAR(64) NOT NULL,
|
|
game_id VARCHAR(120) NOT NULL,
|
|
title VARCHAR(120) NOT NULL,
|
|
description TEXT NOT NULL,
|
|
is_public TINYINT(1) NOT NULL DEFAULT 0,
|
|
groups_json LONGTEXT NOT NULL,
|
|
pool_json LONGTEXT NOT NULL,
|
|
created_at BIGINT NOT NULL,
|
|
updated_at BIGINT NOT NULL,
|
|
INDEX idx_tierlists_author_id (author_id),
|
|
INDEX idx_tierlists_game_id (game_id),
|
|
INDEX idx_tierlists_public_game_updated (is_public, game_id, updated_at),
|
|
CONSTRAINT fk_tierlists_author FOREIGN KEY (author_id) REFERENCES users(id) ON DELETE CASCADE,
|
|
CONSTRAINT fk_tierlists_game FOREIGN KEY (game_id) REFERENCES games(id) ON DELETE CASCADE
|
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
|
|
`)
|
|
|
|
await query(
|
|
`
|
|
INSERT INTO games (id, name, thumbnail_src, created_at)
|
|
VALUES (?, ?, ?, ?)
|
|
ON DUPLICATE KEY UPDATE name = VALUES(name)
|
|
`,
|
|
[FREEFORM_GAME_ID, '직접 티어표 만들기', '', now()]
|
|
)
|
|
|
|
const countRows = await query('SELECT COUNT(*) AS count FROM games')
|
|
if (Number(countRows[0]?.count || 0) <= 1) {
|
|
const createdAt = now()
|
|
await query(
|
|
`
|
|
INSERT INTO games (id, name, thumbnail_src, created_at)
|
|
VALUES
|
|
(?, ?, ?, ?),
|
|
(?, ?, ?, ?)
|
|
`,
|
|
['example-game', '예시 게임', '', createdAt, 'another-game', '다른 예시 게임', '', createdAt]
|
|
)
|
|
|
|
await query(
|
|
`
|
|
INSERT INTO game_items (id, game_id, src, label, created_at)
|
|
VALUES
|
|
(?, ?, ?, ?, ?),
|
|
(?, ?, ?, ?, ?)
|
|
`,
|
|
[
|
|
'img-1',
|
|
'example-game',
|
|
'/uploads/seeds/example1.png',
|
|
'샘플 1',
|
|
createdAt,
|
|
'img-2',
|
|
'example-game',
|
|
'/uploads/seeds/example2.png',
|
|
'샘플 2',
|
|
createdAt,
|
|
]
|
|
)
|
|
}
|
|
})()
|
|
|
|
return initPromise
|
|
}
|
|
|
|
async function ensureData() {
|
|
await ensureSchema()
|
|
}
|
|
|
|
async function countUsers() {
|
|
const rows = await query('SELECT COUNT(*) AS count FROM users')
|
|
return Number(rows[0]?.count || 0)
|
|
}
|
|
|
|
async function findUserByEmail(email) {
|
|
const rows = await query(
|
|
'SELECT id, email, nickname, password_hash, is_admin, avatar_src, created_at FROM users WHERE email = ? LIMIT 1',
|
|
[email]
|
|
)
|
|
const row = rows[0]
|
|
if (!row) return null
|
|
return { ...mapUserRow(row), passwordHash: row.password_hash }
|
|
}
|
|
|
|
async function findUserById(id) {
|
|
const rows = await query(
|
|
'SELECT id, email, nickname, is_admin, avatar_src, created_at FROM users WHERE id = ? LIMIT 1',
|
|
[id]
|
|
)
|
|
return mapUserRow(rows[0])
|
|
}
|
|
|
|
async function createUser({ id, email, nickname, passwordHash, isAdmin }) {
|
|
const createdAt = now()
|
|
await query(
|
|
`
|
|
INSERT INTO users (id, email, nickname, password_hash, is_admin, avatar_src, created_at)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
`,
|
|
[id, email, nickname || '', passwordHash, isAdmin ? 1 : 0, '', createdAt]
|
|
)
|
|
return findUserById(id)
|
|
}
|
|
|
|
async function updateUserProfile({ id, nickname, avatarSrc }) {
|
|
if (typeof avatarSrc === 'string') {
|
|
await query('UPDATE users SET nickname = ?, avatar_src = ? WHERE id = ?', [nickname || '', avatarSrc, id])
|
|
} else {
|
|
await query('UPDATE users SET nickname = ? WHERE id = ?', [nickname || '', id])
|
|
}
|
|
return findUserById(id)
|
|
}
|
|
|
|
async function listUsers() {
|
|
const rows = await query(
|
|
'SELECT id, email, nickname, is_admin, avatar_src, created_at FROM users ORDER BY created_at ASC, email ASC'
|
|
)
|
|
return rows.map(mapUserRow)
|
|
}
|
|
|
|
async function adminUpdateUser({ id, email, nickname, isAdmin }) {
|
|
await query('UPDATE users SET email = ?, nickname = ?, is_admin = ? WHERE id = ?', [
|
|
email,
|
|
nickname || '',
|
|
isAdmin ? 1 : 0,
|
|
id,
|
|
])
|
|
return findUserById(id)
|
|
}
|
|
|
|
async function adminUpdateUserPassword({ id, passwordHash }) {
|
|
await query('UPDATE users SET password_hash = ? WHERE id = ?', [passwordHash, id])
|
|
return findUserById(id)
|
|
}
|
|
|
|
async function adminDeleteUser(id) {
|
|
await query('DELETE FROM users WHERE id = ?', [id])
|
|
}
|
|
|
|
async function listGames() {
|
|
const rows = await query(
|
|
`
|
|
SELECT id, name, thumbnail_src, display_rank, created_at
|
|
FROM games
|
|
WHERE id <> ?
|
|
ORDER BY
|
|
CASE WHEN display_rank IS NULL THEN 1 ELSE 0 END ASC,
|
|
display_rank ASC,
|
|
created_at DESC,
|
|
name ASC
|
|
`,
|
|
[FREEFORM_GAME_ID]
|
|
)
|
|
return rows.map(mapGameRow)
|
|
}
|
|
|
|
async function findGameById(id) {
|
|
const rows = await query('SELECT id, name, thumbnail_src, display_rank, created_at FROM games WHERE id = ? LIMIT 1', [id])
|
|
return mapGameRow(rows[0])
|
|
}
|
|
|
|
async function listGameItems(gameId) {
|
|
const rows = await query(
|
|
'SELECT id, game_id, src, label, created_at FROM game_items WHERE game_id = ? ORDER BY created_at ASC',
|
|
[gameId]
|
|
)
|
|
return rows.map(mapGameItemRow)
|
|
}
|
|
|
|
async function getGameDetail(gameId) {
|
|
const game = await findGameById(gameId)
|
|
if (!game) return null
|
|
const items = await listGameItems(gameId)
|
|
return { game, items }
|
|
}
|
|
|
|
async function createGame({ id, name }) {
|
|
await query('INSERT INTO games (id, name, thumbnail_src, display_rank, created_at) VALUES (?, ?, ?, ?, ?)', [
|
|
id,
|
|
name,
|
|
'',
|
|
null,
|
|
now(),
|
|
])
|
|
return findGameById(id)
|
|
}
|
|
|
|
async function updateGameThumbnail(gameId, thumbnailSrc) {
|
|
await query('UPDATE games SET thumbnail_src = ? WHERE id = ?', [thumbnailSrc, gameId])
|
|
return findGameById(gameId)
|
|
}
|
|
|
|
async function createGameItem({ id, gameId, src, label }) {
|
|
const createdAt = now()
|
|
await query('INSERT INTO game_items (id, game_id, src, label, created_at) VALUES (?, ?, ?, ?, ?)', [
|
|
id,
|
|
gameId,
|
|
src,
|
|
label,
|
|
createdAt,
|
|
])
|
|
const rows = await query('SELECT id, game_id, src, label, created_at FROM game_items WHERE id = ? LIMIT 1', [id])
|
|
return mapGameItemRow(rows[0])
|
|
}
|
|
|
|
async function deleteGameItem(itemId) {
|
|
const gameItemRows = await query('SELECT game_id FROM game_items WHERE id = ? LIMIT 1', [itemId])
|
|
const gameId = gameItemRows[0]?.game_id
|
|
|
|
if (gameId) {
|
|
const tierListRows = await query(
|
|
`
|
|
SELECT id, author_id, game_id, title, description, is_public, groups_json, pool_json, created_at, updated_at
|
|
FROM tierlists
|
|
WHERE game_id = ?
|
|
`,
|
|
[gameId]
|
|
)
|
|
|
|
for (const row of tierListRows) {
|
|
const tierList = mapTierListRow(row)
|
|
const nextGroups = (tierList.groups || []).map((group) => ({
|
|
...group,
|
|
itemIds: (group.itemIds || []).filter((id) => id !== itemId),
|
|
}))
|
|
const nextPool = (tierList.pool || []).filter((item) => item.id !== itemId)
|
|
|
|
await query(
|
|
'UPDATE tierlists SET groups_json = ?, pool_json = ?, updated_at = ? WHERE id = ?',
|
|
[serializeJson(nextGroups), serializeJson(nextPool), now(), tierList.id]
|
|
)
|
|
}
|
|
}
|
|
|
|
await query('DELETE FROM game_items WHERE id = ?', [itemId])
|
|
}
|
|
|
|
async function deleteGame(gameId) {
|
|
await query('DELETE FROM games WHERE id = ?', [gameId])
|
|
}
|
|
|
|
async function updateGameDisplayOrder(gameIds) {
|
|
const normalizedIds = Array.from(new Set((gameIds || []).filter((id) => id && id !== FREEFORM_GAME_ID))).slice(0, 50)
|
|
|
|
await query('UPDATE games SET display_rank = NULL WHERE id <> ?', [FREEFORM_GAME_ID])
|
|
|
|
await Promise.all(
|
|
normalizedIds.map((gameId, index) =>
|
|
query('UPDATE games SET display_rank = ? WHERE id = ? AND id <> ?', [index + 1, gameId, FREEFORM_GAME_ID])
|
|
)
|
|
)
|
|
|
|
return listGames()
|
|
}
|
|
|
|
async function createCustomItem({ id, ownerId, src, label }) {
|
|
const createdAt = now()
|
|
await query('INSERT INTO custom_items (id, owner_id, src, label, created_at) VALUES (?, ?, ?, ?, ?)', [
|
|
id,
|
|
ownerId,
|
|
src,
|
|
label,
|
|
createdAt,
|
|
])
|
|
return { id, ownerId, src, label, origin: 'custom', createdAt }
|
|
}
|
|
|
|
async function getCustomItemUsageMap() {
|
|
const rows = await query('SELECT groups_json, pool_json FROM tierlists')
|
|
const usageMap = new Map()
|
|
|
|
rows.forEach((row) => {
|
|
const groups = parseJson(row.groups_json, [])
|
|
const pool = parseJson(row.pool_json, [])
|
|
|
|
groups.forEach((group) => {
|
|
;(group?.itemIds || []).forEach((itemId) => {
|
|
usageMap.set(itemId, (usageMap.get(itemId) || 0) + 1)
|
|
})
|
|
})
|
|
|
|
pool.forEach((item) => {
|
|
if (item?.id) {
|
|
usageMap.set(item.id, (usageMap.get(item.id) || 0) + 1)
|
|
}
|
|
})
|
|
})
|
|
|
|
return usageMap
|
|
}
|
|
|
|
async function listCustomItems({ queryText = '', page = 1, limit = 50, orphanOnly = false } = {}) {
|
|
const normalizedLimit = Math.min(Math.max(Number(limit) || 50, 1), 200)
|
|
const normalizedPage = Math.max(Number(page) || 1, 1)
|
|
const hasQuery = !!(queryText || '').trim()
|
|
const search = `%${(queryText || '').trim()}%`
|
|
const whereClause = hasQuery ? 'WHERE c.label LIKE ? OR c.src LIKE ? OR u.email LIKE ? OR u.nickname LIKE ?' : ''
|
|
const params = hasQuery ? [search, search, search, search] : []
|
|
|
|
const rows = await query(
|
|
`
|
|
SELECT
|
|
c.id,
|
|
c.owner_id,
|
|
c.src,
|
|
c.label,
|
|
c.created_at,
|
|
u.nickname,
|
|
u.email
|
|
FROM custom_items c
|
|
INNER JOIN users u ON u.id = c.owner_id
|
|
${whereClause}
|
|
ORDER BY c.created_at DESC
|
|
`,
|
|
params
|
|
)
|
|
|
|
const usageMap = await getCustomItemUsageMap()
|
|
const allItems = rows
|
|
.map((row) => ({
|
|
id: row.id,
|
|
ownerId: row.owner_id,
|
|
src: row.src,
|
|
label: row.label,
|
|
createdAt: Number(row.created_at),
|
|
ownerName: row.nickname || row.email,
|
|
ownerEmail: row.email,
|
|
usageCount: usageMap.get(row.id) || 0,
|
|
}))
|
|
.filter((item) => (orphanOnly ? item.usageCount === 0 : true))
|
|
|
|
const total = allItems.length
|
|
const offset = (normalizedPage - 1) * normalizedLimit
|
|
const pagedItems = allItems.slice(offset, offset + normalizedLimit)
|
|
|
|
return {
|
|
items: pagedItems,
|
|
total,
|
|
page: normalizedPage,
|
|
limit: normalizedLimit,
|
|
}
|
|
}
|
|
|
|
async function findUnusedCustomItems({ queryText = '' } = {}) {
|
|
const hasQuery = !!(queryText || '').trim()
|
|
const search = `%${(queryText || '').trim()}%`
|
|
const whereClause = hasQuery ? 'WHERE c.label LIKE ? OR c.src LIKE ? OR u.email LIKE ? OR u.nickname LIKE ?' : ''
|
|
const params = hasQuery ? [search, search, search, search] : []
|
|
|
|
const rows = await query(
|
|
`
|
|
SELECT
|
|
c.id,
|
|
c.owner_id,
|
|
c.src,
|
|
c.label,
|
|
c.created_at,
|
|
u.nickname,
|
|
u.email
|
|
FROM custom_items c
|
|
INNER JOIN users u ON u.id = c.owner_id
|
|
${whereClause}
|
|
ORDER BY c.created_at DESC
|
|
`,
|
|
params
|
|
)
|
|
|
|
const usageMap = await getCustomItemUsageMap()
|
|
return rows
|
|
.map((row) => ({
|
|
id: row.id,
|
|
ownerId: row.owner_id,
|
|
src: row.src,
|
|
label: row.label,
|
|
createdAt: Number(row.created_at),
|
|
ownerName: row.nickname || row.email,
|
|
ownerEmail: row.email,
|
|
usageCount: usageMap.get(row.id) || 0,
|
|
}))
|
|
.filter((item) => item.usageCount === 0)
|
|
}
|
|
|
|
async function listPublicTierLists(gameId) {
|
|
const params = []
|
|
let whereClause = 'WHERE t.is_public = 1'
|
|
if (gameId) {
|
|
whereClause += ' AND t.game_id = ?'
|
|
params.push(gameId)
|
|
}
|
|
|
|
const rows = await query(
|
|
`
|
|
SELECT
|
|
t.id,
|
|
t.game_id,
|
|
t.title,
|
|
t.created_at,
|
|
t.updated_at,
|
|
t.author_id,
|
|
u.nickname,
|
|
u.email,
|
|
u.avatar_src
|
|
FROM tierlists t
|
|
INNER JOIN users u ON u.id = t.author_id
|
|
${whereClause}
|
|
ORDER BY t.updated_at DESC
|
|
LIMIT 50
|
|
`,
|
|
params
|
|
)
|
|
|
|
return rows.map((row) => ({
|
|
id: row.id,
|
|
gameId: row.game_id,
|
|
title: row.title,
|
|
createdAt: Number(row.created_at),
|
|
updatedAt: Number(row.updated_at),
|
|
authorId: row.author_id,
|
|
authorName: getUserDisplayName(row),
|
|
authorAccountName: getUserAccountName(row),
|
|
authorAvatarSrc: row.avatar_src || '',
|
|
}))
|
|
}
|
|
|
|
async function listUserTierLists(userId) {
|
|
const rows = await query(
|
|
`
|
|
SELECT
|
|
t.id,
|
|
t.game_id,
|
|
t.title,
|
|
t.created_at,
|
|
t.updated_at,
|
|
t.is_public,
|
|
u.nickname,
|
|
u.email,
|
|
u.avatar_src
|
|
FROM tierlists t
|
|
INNER JOIN users u ON u.id = t.author_id
|
|
WHERE t.author_id = ?
|
|
ORDER BY updated_at DESC
|
|
`,
|
|
[userId]
|
|
)
|
|
|
|
return rows.map((row) => ({
|
|
id: row.id,
|
|
gameId: row.game_id,
|
|
title: row.title,
|
|
createdAt: Number(row.created_at),
|
|
updatedAt: Number(row.updated_at),
|
|
isPublic: !!row.is_public,
|
|
authorName: getUserDisplayName(row),
|
|
authorAccountName: getUserAccountName(row),
|
|
authorAvatarSrc: row.avatar_src || '',
|
|
}))
|
|
}
|
|
|
|
async function findTierListById(id) {
|
|
const rows = await query(
|
|
`
|
|
SELECT
|
|
t.id,
|
|
t.author_id,
|
|
t.game_id,
|
|
t.title,
|
|
t.description,
|
|
t.is_public,
|
|
t.groups_json,
|
|
t.pool_json,
|
|
t.created_at,
|
|
t.updated_at,
|
|
u.nickname,
|
|
u.email,
|
|
u.avatar_src
|
|
FROM tierlists t
|
|
INNER JOIN users u ON u.id = t.author_id
|
|
WHERE t.id = ?
|
|
LIMIT 1
|
|
`,
|
|
[id]
|
|
)
|
|
return mapTierListRow(rows[0])
|
|
}
|
|
|
|
async function deleteTierList(id) {
|
|
await query('DELETE FROM tierlists WHERE id = ?', [id])
|
|
}
|
|
|
|
async function findCustomItemsByIds(ids) {
|
|
if (!ids.length) return []
|
|
const placeholders = ids.map(() => '?').join(', ')
|
|
const rows = await query(
|
|
`
|
|
SELECT id, owner_id, src, label, created_at
|
|
FROM custom_items
|
|
WHERE id IN (${placeholders})
|
|
`,
|
|
ids
|
|
)
|
|
|
|
return rows.map((row) => ({
|
|
id: row.id,
|
|
ownerId: row.owner_id,
|
|
src: row.src,
|
|
label: row.label,
|
|
createdAt: Number(row.created_at),
|
|
}))
|
|
}
|
|
|
|
async function deleteCustomItems(ids) {
|
|
if (!ids.length) return
|
|
const placeholders = ids.map(() => '?').join(', ')
|
|
await query(`DELETE FROM custom_items WHERE id IN (${placeholders})`, ids)
|
|
}
|
|
|
|
async function saveTierList({ id, authorId, gameId, title, description, isPublic, groups, pool }) {
|
|
const existing = id ? await findTierListById(id) : null
|
|
|
|
if (existing) {
|
|
await query(
|
|
`
|
|
UPDATE tierlists
|
|
SET title = ?, description = ?, is_public = ?, groups_json = ?, pool_json = ?, updated_at = ?
|
|
WHERE id = ?
|
|
`,
|
|
[title, description || '', isPublic ? 1 : 0, serializeJson(groups), serializeJson(pool), now(), existing.id]
|
|
)
|
|
return findTierListById(existing.id)
|
|
}
|
|
|
|
const createdAt = now()
|
|
await query(
|
|
`
|
|
INSERT INTO tierlists (
|
|
id, author_id, game_id, title, description, is_public, groups_json, pool_json, created_at, updated_at
|
|
)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
`,
|
|
[id, authorId, gameId, title, description || '', isPublic ? 1 : 0, serializeJson(groups), serializeJson(pool), createdAt, createdAt]
|
|
)
|
|
return findTierListById(id)
|
|
}
|
|
|
|
module.exports = {
|
|
DB_NAME,
|
|
ensureData,
|
|
countUsers,
|
|
findUserByEmail,
|
|
findUserById,
|
|
createUser,
|
|
updateUserProfile,
|
|
listUsers,
|
|
adminUpdateUser,
|
|
adminUpdateUserPassword,
|
|
adminDeleteUser,
|
|
listGames,
|
|
findGameById,
|
|
listGameItems,
|
|
getGameDetail,
|
|
createGame,
|
|
updateGameThumbnail,
|
|
createGameItem,
|
|
deleteGameItem,
|
|
deleteGame,
|
|
updateGameDisplayOrder,
|
|
createCustomItem,
|
|
listCustomItems,
|
|
findUnusedCustomItems,
|
|
listPublicTierLists,
|
|
listUserTierLists,
|
|
findTierListById,
|
|
deleteTierList,
|
|
findCustomItemsByIds,
|
|
deleteCustomItems,
|
|
saveTierList,
|
|
}
|