feat: add community publishing and news syndication
Validate and deploy production / validate (push) Successful in 23s
Validate and deploy staging / validate (push) Successful in 50s
Validate and deploy production / deploy (push) Failing after 40s
Validate and deploy staging / deploy (push) Failing after 7s

This commit is contained in:
jb
2026-08-01 21:42:09 -04:00
parent b47e2588e9
commit dd84dff055
288 changed files with 83254 additions and 38351 deletions
+470 -173
View File
@@ -1,198 +1,495 @@
import { auditRecord, recordIsacAudit } from '$lib/server/audit';
import { requireDataView, requirePermission } from '$lib/server/auth/authorization';
import {
requireDataView,
requirePermission,
} from '$lib/server/auth/authorization';
import { getSessionUser } from '$lib/server/auth/session';
import { getDatabase } from '$lib/server/database';
import { getPendingEntities } from '$lib/server/data-moderation';
import { getPagination, getRegistrySearch, getRegistrySort } from '$lib/server/pagination';
import {
getPagination,
getRegistrySearch,
getRegistrySort,
} from '$lib/server/pagination';
import {
applyGameDataRender,
ManagedImageValidationError,
managedImageUrl,
processManagedImage,
readGameDataRenderMutation,
} from '$lib/server/managed-images';
import { fail } from '@sveltejs/kit';
import { randomUUID } from 'node:crypto';
import type { Actions, PageServerLoad } from './$types';
const qualities = ['Standard', 'Specialized', 'Superior', 'High-end'] as const;
type QualityValue = {
quality: string;
primaryLevel1Min: number;
primaryLevel1Max: number;
primaryLevel20Min: number;
primaryLevel20Max: number;
quality: string;
primaryLevel1Min: number;
primaryLevel1Max: number;
primaryLevel20Min: number;
primaryLevel20Max: number;
};
type SecondaryRange = {
statId: string;
values: Record<string, { min: number; max: number }>;
};
type SecondaryRange = { statId: string; values: Record<string, { min: number; max: number }> };
function parseJson<T>(value: FormDataEntryValue | null, fallback: T): T {
try { return JSON.parse(String(value ?? '')) as T; } catch { return fallback; }
try {
return JSON.parse(String(value ?? '')) as T;
} catch {
return fallback;
}
}
async function snapshot(id: string) {
const database = await getDatabase();
return {
record: await auditRecord('gear_mods', id),
qualityValues: await (await database.prepare('SELECT * FROM gear_mod_quality_values WHERE gear_mod_id=? ORDER BY quality')).all(id),
secondaryOptions: await (await database.prepare('SELECT stat_attribute_id FROM gear_mod_secondary_options WHERE gear_mod_id=? ORDER BY stat_attribute_id')).all(id),
secondaryQualityValues: await (await database.prepare('SELECT * FROM gear_mod_secondary_quality_values WHERE gear_mod_id=? ORDER BY stat_attribute_id,quality')).all(id)
};
const database = await getDatabase();
return {
record: await auditRecord('gear_mods', id),
qualityValues: await (
await database.prepare(
'SELECT * FROM gear_mod_quality_values WHERE gear_mod_id=? ORDER BY quality',
)
).all(id),
secondaryOptions: await (
await database.prepare(
'SELECT stat_attribute_id FROM gear_mod_secondary_options WHERE gear_mod_id=? ORDER BY stat_attribute_id',
)
).all(id),
secondaryQualityValues: await (
await database.prepare(
'SELECT * FROM gear_mod_secondary_quality_values WHERE gear_mod_id=? ORDER BY stat_attribute_id,quality',
)
).all(id),
render:
(await (
await database.prepare(
"SELECT * FROM managed_images WHERE source_entity_type='gear_mod_render' AND source_entity_id=?",
)
).get(id)) ?? null,
};
}
export const load: PageServerLoad = async ({ cookies, url }) => {
const user = await requireDataView(await getSessionUser(cookies));
const database = await getDatabase();
const requestedFilter = url.searchParams.get('filter') ?? '';
const filter = ['active', 'pending', 'archived'].includes(requestedFilter) ? requestedFilter : '';
const { search, pattern } = getRegistrySearch(url);
const sort = getRegistrySort(url, {
name: { asc: 'name COLLATE NOCASE ASC,id ASC', desc: 'name COLLATE NOCASE DESC,id DESC' },
secondaries: { asc: '(SELECT COUNT(*) FROM gear_mod_secondary_options option WHERE option.gear_mod_id=gear_mods.id) ASC,name COLLATE NOCASE ASC', desc: '(SELECT COUNT(*) FROM gear_mod_secondary_options option WHERE option.gear_mod_id=gear_mods.id) DESC,name COLLATE NOCASE ASC' },
state: { asc: 'active ASC,name COLLATE NOCASE ASC', desc: 'active DESC,name COLLATE NOCASE ASC', defaultDirection: 'desc' }
}, 'name');
const conditions = [
...(filter === 'active' ? ['active=1'] : filter === 'archived' ? ['active=0'] : filter === 'pending' ? ['1=0'] : []),
...(search ? ["name LIKE ? ESCAPE '\\' COLLATE NOCASE"] : [])
];
const where = conditions.length ? `WHERE ${conditions.join(' AND ')}` : '';
const args = search ? [pattern] : [];
const [summary, filtered, pendingStats, pendingMods] = await Promise.all([
(await database.prepare(`SELECT COUNT(*) AS total,
const user = await requireDataView(await getSessionUser(cookies));
const database = await getDatabase();
const requestedFilter = url.searchParams.get('filter') ?? '';
const filter = ['active', 'pending', 'archived'].includes(requestedFilter)
? requestedFilter
: '';
const requestedItemId = url.searchParams.get('item')?.trim() ?? '';
const { search, pattern } = getRegistrySearch(url);
const sort = getRegistrySort(
url,
{
name: {
asc: 'name COLLATE NOCASE ASC,id ASC',
desc: 'name COLLATE NOCASE DESC,id DESC',
},
secondaries: {
asc: '(SELECT COUNT(*) FROM gear_mod_secondary_options option WHERE option.gear_mod_id=gear_mods.id) ASC,name COLLATE NOCASE ASC',
desc: '(SELECT COUNT(*) FROM gear_mod_secondary_options option WHERE option.gear_mod_id=gear_mods.id) DESC,name COLLATE NOCASE ASC',
},
state: {
asc: 'active ASC,name COLLATE NOCASE ASC',
desc: 'active DESC,name COLLATE NOCASE ASC',
defaultDirection: 'desc',
},
},
'name',
);
const conditions = [
...(requestedItemId
? ['id=?']
: filter === 'active'
? ['active=1']
: filter === 'archived'
? ['active=0']
: filter === 'pending'
? ['1=0']
: []),
...(!requestedItemId && search
? ["name LIKE ? ESCAPE '\\' COLLATE NOCASE"]
: []),
];
const where = conditions.length ? `WHERE ${conditions.join(' AND ')}` : '';
const args = [
...(requestedItemId ? [requestedItemId] : []),
...(!requestedItemId && search ? [pattern] : []),
];
const [summary, filtered, pendingStats, pendingMods] = await Promise.all([
(
await database.prepare(`SELECT COUNT(*) AS total,
SUM(CASE WHEN active=1 THEN 1 ELSE 0 END) AS active,
SUM(CASE WHEN active=0 THEN 1 ELSE 0 END) AS archived FROM gear_mods`)).get(),
(await database.prepare(`SELECT COUNT(*) AS total FROM gear_mods ${where}`)).get(...args),
getPendingEntities(user.id, [{ module: 'attributes', action: 'save', kind: 'attribute', fields: ['unit'] }]),
getPendingEntities(user.id, [{ module: 'gearmods', action: 'save', kind: 'gearmod', fields: ['qualityValues', 'secondaryRanges'] }])
]);
const pendingRows = filter === 'archived' || filter === 'active' ? [] : pendingMods.filter((item) =>
!search || item.name.toLocaleLowerCase().includes(search.toLocaleLowerCase())
);
const pagination = getPagination(url, Number(filtered?.total ?? 0) + pendingRows.length);
const pendingOnPage = pagination.page === 1 ? pendingRows.slice(0, pagination.perPage) : [];
const databaseOffset = Math.max(0, pagination.offset - pendingRows.length);
const databaseLimit = Math.max(0, pagination.perPage - pendingOnPage.length);
const [mods, qualityRows, secondaryOptions, secondaryValues, stats] = await Promise.all([
databaseLimit ? (await database.prepare(`SELECT * FROM gear_mods ${where}
ORDER BY ${sort.orderBy} LIMIT ? OFFSET ?`)).all(...args, databaseLimit, databaseOffset) : [],
(await database.prepare('SELECT * FROM gear_mod_quality_values')).all(),
(await database.prepare('SELECT * FROM gear_mod_secondary_options')).all(),
(await database.prepare('SELECT * FROM gear_mod_secondary_quality_values')).all(),
(await database.prepare('SELECT id,name,unit FROM stat_attributes WHERE active=1 ORDER BY name COLLATE NOCASE')).all()
]);
const parsedPending = pendingOnPage.map((item) => ({
...item,
qualityValues: parseJson(String(item.qualityValues), []),
secondaryRanges: parseJson(String(item.secondaryRanges), [])
}));
return {
mods: [...parsedPending, ...mods.map((mod) => ({
...mod,
qualityValues: qualityRows.filter((row) => row.gear_mod_id === mod.id).map((row) => ({
quality: row.quality,
primaryLevel1Min: Number(row.primary_level_1_min),
primaryLevel1Max: Number(row.primary_level_1_max),
primaryLevel20Min: Number(row.primary_level_20_min),
primaryLevel20Max: Number(row.primary_level_20_max)
})),
secondaryRanges: secondaryOptions.filter((option) => option.gear_mod_id === mod.id).map((option) => ({
statId: String(option.stat_attribute_id),
values: Object.fromEntries(secondaryValues
.filter((row) => row.gear_mod_id === mod.id && row.stat_attribute_id === option.stat_attribute_id)
.map((row) => [String(row.quality), { min: Number(row.min_value), max: Number(row.max_value) }]))
}))
}))],
stats: [...pendingStats, ...stats],
qualities,
summary: {
...summary,
total: Number(summary?.total ?? 0) + pendingMods.length,
pending: pendingMods.length
},
filter,
pagination,
sort,
search
};
SUM(CASE WHEN active=0 THEN 1 ELSE 0 END) AS archived FROM gear_mods`)
).get(),
(
await database.prepare(`SELECT COUNT(*) AS total FROM gear_mods ${where}`)
).get(...args),
getPendingEntities(user.id, [
{
module: 'attributes',
action: 'save',
kind: 'attribute',
fields: ['unit'],
},
]),
getPendingEntities(user.id, [
{
module: 'gearmods',
action: 'save',
kind: 'gearmod',
fields: ['qualityValues', 'secondaryRanges'],
},
]),
]);
const pendingRows = requestedItemId
? pendingMods.filter((item) => item.id === requestedItemId)
: filter === 'archived' || filter === 'active'
? []
: pendingMods.filter(
(item) =>
!search ||
item.name.toLocaleLowerCase().includes(search.toLocaleLowerCase()),
);
const pagination = getPagination(
url,
Number(filtered?.total ?? 0) + pendingRows.length,
);
const pendingOnPage =
pagination.page === 1 ? pendingRows.slice(0, pagination.perPage) : [];
const databaseOffset = Math.max(0, pagination.offset - pendingRows.length);
const databaseLimit = Math.max(0, pagination.perPage - pendingOnPage.length);
const [mods, qualityRows, secondaryOptions, secondaryValues, stats, renders] =
await Promise.all([
databaseLimit
? (
await database.prepare(`SELECT * FROM gear_mods ${where}
ORDER BY ${sort.orderBy} LIMIT ? OFFSET ?`)
).all(...args, databaseLimit, databaseOffset)
: [],
(await database.prepare('SELECT * FROM gear_mod_quality_values')).all(),
(
await database.prepare('SELECT * FROM gear_mod_secondary_options')
).all(),
(
await database.prepare(
'SELECT * FROM gear_mod_secondary_quality_values',
)
).all(),
(
await database.prepare(
'SELECT id,name,unit FROM stat_attributes WHERE active=1 ORDER BY name COLLATE NOCASE',
)
).all(),
(
await database.prepare(
"SELECT source_entity_id,id,updated_at FROM managed_images WHERE source_entity_type='gear_mod_render'",
)
).all(),
]);
const parsedPending = pendingOnPage.map((item) => ({
...item,
qualityValues: parseJson(String(item.qualityValues), []),
secondaryRanges: parseJson(String(item.secondaryRanges), []),
}));
return {
mods: [
...parsedPending,
...mods.map((mod) => ({
...mod,
qualityValues: qualityRows
.filter((row) => row.gear_mod_id === mod.id)
.map((row) => ({
quality: row.quality,
primaryLevel1Min: Number(row.primary_level_1_min),
primaryLevel1Max: Number(row.primary_level_1_max),
primaryLevel20Min: Number(row.primary_level_20_min),
primaryLevel20Max: Number(row.primary_level_20_max),
})),
secondaryRanges: secondaryOptions
.filter((option) => option.gear_mod_id === mod.id)
.map((option) => ({
statId: String(option.stat_attribute_id),
values: Object.fromEntries(
secondaryValues
.filter(
(row) =>
row.gear_mod_id === mod.id &&
row.stat_attribute_id === option.stat_attribute_id,
)
.map((row) => [
String(row.quality),
{ min: Number(row.min_value), max: Number(row.max_value) },
]),
),
})),
renderUrl: (() => {
const render = renders.find(
(item) => item.source_entity_id === mod.id,
);
return render
? managedImageUrl(String(render.id), render.updated_at)
: '';
})(),
})),
],
stats: [...pendingStats, ...stats],
qualities,
summary: {
...summary,
total: Number(summary?.total ?? 0) + pendingMods.length,
pending: pendingMods.length,
},
filter,
requestedItemId,
pagination,
sort,
search,
};
};
export const actions: Actions = {
save: async ({ request, cookies }) => {
const operator = await requirePermission(await getSessionUser(cookies), 'data.manage');
const form = await request.formData();
const id = String(form.get('id') ?? '');
const name = String(form.get('name') ?? '').normalize('NFKC').trim();
const qualityValues = parseJson<QualityValue[]>(form.get('qualityValues'), []);
const secondaryRanges = parseJson<SecondaryRange[]>(form.get('secondaryRanges'), []);
if (name.length < 2 || name.length > 100) return fail(400, { gearModError: 'Gear Mod names must be 2–100 characters.' });
if (qualityValues.length !== qualities.length || new Set(qualityValues.map((row) => row.quality)).size !== qualities.length ||
qualityValues.some((row) => !qualities.includes(row.quality as typeof qualities[number]) ||
[row.primaryLevel1Min,row.primaryLevel1Max,row.primaryLevel20Min,row.primaryLevel20Max].some((value) => !Number.isFinite(Number(value))) ||
Number(row.primaryLevel1Max) < Number(row.primaryLevel1Min) ||
Number(row.primaryLevel20Max) < Number(row.primaryLevel20Min) ||
Number(row.primaryLevel20Min) < Number(row.primaryLevel1Min) ||
Number(row.primaryLevel20Max) < Number(row.primaryLevel1Max))) {
return fail(400, { gearModError: 'Configure valid primary Level 1 and Level 20 ranges for all four qualities.' });
}
if (secondaryRanges.length < 2 || secondaryRanges.length > 50 ||
new Set(secondaryRanges.map((range) => range.statId)).size !== secondaryRanges.length ||
secondaryRanges.some((range) => !range.statId || qualities.some((quality) => {
const values = range.values?.[quality];
return !values || !Number.isFinite(Number(values.min)) || !Number.isFinite(Number(values.max)) ||
Number(values.max) < Number(values.min);
}))) {
return fail(400, { gearModError: 'Configure at least two unique possible secondary attributes with valid ranges for every quality.' });
}
const database = await getDatabase();
if (await (await database.prepare('SELECT id FROM gear_mods WHERE name=? COLLATE NOCASE AND id<>?')).get(name, id || 'new')) {
return fail(409, { gearModError: 'That Gear Mod name is already registered.' });
}
const statIds = secondaryRanges.map((range) => range.statId);
const validStats = await (await database.prepare(`SELECT COUNT(*) AS count FROM stat_attributes
WHERE active=1 AND id IN (${statIds.map(() => '?').join(',')})`)).get(...statIds);
if (Number(validStats?.count) !== statIds.length) return fail(400, { gearModError: 'One or more selected attributes are unavailable.' });
const modId = id || String(form.get('_proposedEntityId') ?? '') || randomUUID();
const before = id ? await snapshot(id) : null;
if (id && !before?.record) return fail(404, { gearModError: 'Gear Mod not found.' });
const now = new Date().toISOString();
await database.exec('BEGIN');
try {
if (id) await (await database.prepare('UPDATE gear_mods SET name=?,active=1,updated_at=? WHERE id=?')).run(name, now, id);
else await (await database.prepare('INSERT INTO gear_mods(id,name,created_at,updated_at) VALUES(?,?,?,?)')).run(modId, name, now, now);
await (await database.prepare('DELETE FROM gear_mod_quality_values WHERE gear_mod_id=?')).run(modId);
for (const row of qualityValues) await (await database.prepare(`INSERT INTO gear_mod_quality_values
save: async ({ request, cookies }) => {
const operator = await requirePermission(
await getSessionUser(cookies),
'data.manage',
);
const form = await request.formData();
const id = String(form.get('id') ?? '');
const renderMutation = await readGameDataRenderMutation(form);
try {
if (renderMutation.file)
renderMutation.processed = await processManagedImage(
renderMutation.file,
false,
false,
true,
);
} catch (error) {
if (error instanceof ManagedImageValidationError)
return fail(400, { gearModError: error.message });
throw error;
}
const name = String(form.get('name') ?? '')
.normalize('NFKC')
.trim();
const qualityValues = parseJson<QualityValue[]>(
form.get('qualityValues'),
[],
);
const secondaryRanges = parseJson<SecondaryRange[]>(
form.get('secondaryRanges'),
[],
);
if (name.length < 2 || name.length > 100)
return fail(400, {
gearModError: 'Gear Mod names must be 2–100 characters.',
});
if (
qualityValues.length !== qualities.length ||
new Set(qualityValues.map((row) => row.quality)).size !==
qualities.length ||
qualityValues.some(
(row) =>
!qualities.includes(row.quality as (typeof qualities)[number]) ||
[
row.primaryLevel1Min,
row.primaryLevel1Max,
row.primaryLevel20Min,
row.primaryLevel20Max,
].some((value) => !Number.isFinite(Number(value))) ||
Number(row.primaryLevel1Max) < Number(row.primaryLevel1Min) ||
Number(row.primaryLevel20Max) < Number(row.primaryLevel20Min) ||
Number(row.primaryLevel20Min) < Number(row.primaryLevel1Min) ||
Number(row.primaryLevel20Max) < Number(row.primaryLevel1Max),
)
) {
return fail(400, {
gearModError:
'Configure valid primary Level 1 and Level 20 ranges for all four qualities.',
});
}
if (
secondaryRanges.length < 2 ||
secondaryRanges.length > 50 ||
new Set(secondaryRanges.map((range) => range.statId)).size !==
secondaryRanges.length ||
secondaryRanges.some(
(range) =>
!range.statId ||
qualities.some((quality) => {
const values = range.values?.[quality];
return (
!values ||
!Number.isFinite(Number(values.min)) ||
!Number.isFinite(Number(values.max)) ||
Number(values.max) < Number(values.min)
);
}),
)
) {
return fail(400, {
gearModError:
'Configure at least two unique possible secondary attributes with valid ranges for every quality.',
});
}
const database = await getDatabase();
if (
await (
await database.prepare(
'SELECT id FROM gear_mods WHERE name=? COLLATE NOCASE AND id<>?',
)
).get(name, id || 'new')
) {
return fail(409, {
gearModError: 'That Gear Mod name is already registered.',
});
}
const statIds = secondaryRanges.map((range) => range.statId);
const validStats = await (
await database.prepare(`SELECT COUNT(*) AS count FROM stat_attributes
WHERE active=1 AND id IN (${statIds.map(() => '?').join(',')})`)
).get(...statIds);
if (Number(validStats?.count) !== statIds.length)
return fail(400, {
gearModError: 'One or more selected attributes are unavailable.',
});
const modId =
id || String(form.get('_proposedEntityId') ?? '') || randomUUID();
const before = id ? await snapshot(id) : null;
if (id && !before?.record)
return fail(404, { gearModError: 'Gear Mod not found.' });
const now = new Date().toISOString();
await database.exec('BEGIN');
try {
if (id)
await (
await database.prepare(
'UPDATE gear_mods SET name=?,active=1,updated_at=? WHERE id=?',
)
).run(name, now, id);
else
await (
await database.prepare(
'INSERT INTO gear_mods(id,name,created_at,updated_at) VALUES(?,?,?,?)',
)
).run(modId, name, now, now);
await (
await database.prepare(
'DELETE FROM gear_mod_quality_values WHERE gear_mod_id=?',
)
).run(modId);
for (const row of qualityValues)
await (
await database.prepare(`INSERT INTO gear_mod_quality_values
(gear_mod_id,quality,primary_level_1_min,primary_level_1_max,primary_level_20_min,primary_level_20_max)
VALUES(?,?,?,?,?,?)`)).run(modId,row.quality,row.primaryLevel1Min,row.primaryLevel1Max,row.primaryLevel20Min,row.primaryLevel20Max);
await (await database.prepare('DELETE FROM gear_mod_secondary_options WHERE gear_mod_id=?')).run(modId);
for (const range of secondaryRanges) {
await (await database.prepare('INSERT INTO gear_mod_secondary_options(gear_mod_id,stat_attribute_id) VALUES(?,?)')).run(modId, range.statId);
for (const quality of qualities) await (await database.prepare(`INSERT INTO gear_mod_secondary_quality_values
(gear_mod_id,stat_attribute_id,quality,min_value,max_value) VALUES(?,?,?,?,?)`))
.run(modId, range.statId, quality, range.values[quality].min, range.values[quality].max);
}
await database.exec('COMMIT');
} catch (error) {
await database.exec('ROLLBACK');
throw error;
}
await recordIsacAudit({
actorId: operator.id,
category: 'game_data',
action: id ? 'gear_mod.updated' : 'gear_mod.created',
entityId: modId,
before,
after: await snapshot(modId)
});
return { gearModSaved: true, gearModMode: id ? 'edit' : 'add', resultToken: randomUUID() };
},
archive: async ({ request, cookies }) => {
const operator = await requirePermission(await getSessionUser(cookies), 'data.manage');
const id = String((await request.formData()).get('id') ?? '');
const before = await snapshot(id);
if (!before.record) return fail(404, { gearModError: 'Gear Mod not found.' });
await (await (await getDatabase()).prepare('UPDATE gear_mods SET active=0,updated_at=? WHERE id=?')).run(new Date().toISOString(), id);
await recordIsacAudit({ actorId: operator.id, category: 'game_data', action: 'gear_mod.archived', entityId: id, before, after: await snapshot(id) });
return { gearModArchived: true };
},
unarchive: async ({ request, cookies }) => {
const operator = await requirePermission(await getSessionUser(cookies), 'data.manage');
const id = String((await request.formData()).get('id') ?? '');
const before = await snapshot(id);
if (!before.record) return fail(404, { gearModError: 'Gear Mod not found.' });
await (await (await getDatabase()).prepare('UPDATE gear_mods SET active=1,updated_at=? WHERE id=?')).run(new Date().toISOString(), id);
await recordIsacAudit({ actorId: operator.id, category: 'game_data', action: 'gear_mod.unarchived', entityId: id, before, after: await snapshot(id) });
return { gearModUnarchived: true };
}
VALUES(?,?,?,?,?,?)`)
).run(
modId,
row.quality,
row.primaryLevel1Min,
row.primaryLevel1Max,
row.primaryLevel20Min,
row.primaryLevel20Max,
);
await (
await database.prepare(
'DELETE FROM gear_mod_secondary_options WHERE gear_mod_id=?',
)
).run(modId);
for (const range of secondaryRanges) {
await (
await database.prepare(
'INSERT INTO gear_mod_secondary_options(gear_mod_id,stat_attribute_id) VALUES(?,?)',
)
).run(modId, range.statId);
for (const quality of qualities)
await (
await database.prepare(`INSERT INTO gear_mod_secondary_quality_values
(gear_mod_id,stat_attribute_id,quality,min_value,max_value) VALUES(?,?,?,?,?)`)
).run(
modId,
range.statId,
quality,
range.values[quality].min,
range.values[quality].max,
);
}
await database.exec('COMMIT');
} catch (error) {
await database.exec('ROLLBACK');
throw error;
}
await applyGameDataRender(
database,
'gear_mod',
modId,
name,
renderMutation,
operator.id,
);
await recordIsacAudit({
actorId: operator.id,
category: 'game_data',
action: id ? 'gear_mod.updated' : 'gear_mod.created',
entityId: modId,
before,
after: await snapshot(modId),
});
return {
gearModSaved: true,
gearModMode: id ? 'edit' : 'add',
resultToken: randomUUID(),
};
},
archive: async ({ request, cookies }) => {
const operator = await requirePermission(
await getSessionUser(cookies),
'data.manage',
);
const id = String((await request.formData()).get('id') ?? '');
const before = await snapshot(id);
if (!before.record)
return fail(404, { gearModError: 'Gear Mod not found.' });
await (
await (
await getDatabase()
).prepare('UPDATE gear_mods SET active=0,updated_at=? WHERE id=?')
).run(new Date().toISOString(), id);
await recordIsacAudit({
actorId: operator.id,
category: 'game_data',
action: 'gear_mod.archived',
entityId: id,
before,
after: await snapshot(id),
});
return { gearModArchived: true };
},
unarchive: async ({ request, cookies }) => {
const operator = await requirePermission(
await getSessionUser(cookies),
'data.manage',
);
const id = String((await request.formData()).get('id') ?? '');
const before = await snapshot(id);
if (!before.record)
return fail(404, { gearModError: 'Gear Mod not found.' });
await (
await (
await getDatabase()
).prepare('UPDATE gear_mods SET active=1,updated_at=? WHERE id=?')
).run(new Date().toISOString(), id);
await recordIsacAudit({
actorId: operator.id,
category: 'game_data',
action: 'gear_mod.unarchived',
entityId: id,
before,
after: await snapshot(id),
});
return { gearModUnarchived: true };
},
};