refactor(db): finish Drizzle migration for remaining actions and services
Co-authored-by: Cursor <[email protected]>
This commit is contained in:
1 parent
22234fe102
commit
65b2fbee6a
19 files changed
+422
-342
No files matched your search
@@ -1,7 +1,8 @@
|
||||
import { existsSync, promises as fs } from "node:fs";
|
||||
import path from "node:path";
|
||||
import { and, eq, type SQL, sql } from "drizzle-orm";
|
||||
import { CatalogPages, db, ItemsBase } from "@/lib/db";
|
||||
import { logger } from "@/lib/logger";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import {
|
||||
appendFurniEntry,
|
||||
buildFurniEntry,
|
||||
@@ -166,50 +167,39 @@ export function buildItemsBaseEnrichmentUpdate(params: {
|
||||
canstandon: boolean;
|
||||
allowStack: string;
|
||||
interactionType: string;
|
||||
}): { sql: string; values: Array<number | string> } {
|
||||
return {
|
||||
sql: `
|
||||
}): SQL {
|
||||
return sql`
|
||||
UPDATE items_base
|
||||
SET width = ?, length = ?, stack_height = ?,
|
||||
allow_sit = ?,
|
||||
allow_lay = ?,
|
||||
allow_walk = ?,
|
||||
allow_stack = ?,
|
||||
interaction_type = ?
|
||||
WHERE id = ?
|
||||
`,
|
||||
values: [
|
||||
params.dims.x,
|
||||
params.dims.y,
|
||||
params.dims.z,
|
||||
params.cansiton ? "1" : "0",
|
||||
params.canlayon ? "1" : "0",
|
||||
params.canstandon ? "1" : "0",
|
||||
params.allowStack,
|
||||
params.interactionType,
|
||||
params.id,
|
||||
],
|
||||
};
|
||||
SET width = ${params.dims.x}, length = ${params.dims.y}, stack_height = ${params.dims.z},
|
||||
allow_sit = ${params.cansiton ? "1" : "0"},
|
||||
allow_lay = ${params.canlayon ? "1" : "0"},
|
||||
allow_walk = ${params.canstandon ? "1" : "0"},
|
||||
allow_stack = ${params.allowStack},
|
||||
interaction_type = ${params.interactionType}
|
||||
WHERE id = ${params.id}
|
||||
`;
|
||||
}
|
||||
|
||||
/** Get or create the root "Imported Furniture" parent page (no items directly on this page). */
|
||||
export async function getOrCreateImportedParentPage(): Promise<number> {
|
||||
const existing = await prisma.catalogPages.findFirst({
|
||||
where: { captionSave: IMPORTED_PAGE_CAPTION_SAVE },
|
||||
select: { id: true },
|
||||
});
|
||||
const [existing] = await db
|
||||
.select({ id: CatalogPages.id })
|
||||
.from(CatalogPages)
|
||||
.where(eq(CatalogPages.captionSave, IMPORTED_PAGE_CAPTION_SAVE))
|
||||
.limit(1);
|
||||
if (existing) return existing.id;
|
||||
|
||||
// Use raw SQL to avoid ENUM mismatch (page_layout, visible, enabled)
|
||||
// All string columns must be '' (not NULL) or the Java emulator may skip the page
|
||||
await prisma.$executeRaw`
|
||||
await db.execute(sql`
|
||||
INSERT INTO catalog_pages (caption_save, caption, page_layout, parent_id, min_rank, order_num, icon_image, enabled, visible, includes, page_headline, page_teaser, page_special, page_text1, page_text2, page_text_details, page_text_teaser)
|
||||
VALUES (${IMPORTED_PAGE_CAPTION_SAVE}, ${IMPORTED_PAGE_CAPTION}, 'default_3x3', -1, '1', 9999, 263, '1', '1', '', '', '', '', '', '', '', '')
|
||||
`;
|
||||
const created = await prisma.catalogPages.findFirst({
|
||||
where: { captionSave: IMPORTED_PAGE_CAPTION_SAVE },
|
||||
select: { id: true },
|
||||
});
|
||||
`);
|
||||
const [created] = await db
|
||||
.select({ id: CatalogPages.id })
|
||||
.from(CatalogPages)
|
||||
.where(eq(CatalogPages.captionSave, IMPORTED_PAGE_CAPTION_SAVE))
|
||||
.limit(1);
|
||||
if (!created) {
|
||||
throw new Error(
|
||||
'Failed to create "Imported Furniture" parent catalog page',
|
||||
@@ -281,7 +271,7 @@ export function autoPriceFurni(classname: string): {
|
||||
* Allocate the next `catalog_items.id` and run the INSERT through a
|
||||
* process-level async mutex so concurrent imports in the same Node process
|
||||
* cannot race on MAX(id). A DB-level lock would require pinning a single
|
||||
* Prisma connection for the whole operation; the in-process mutex is enough
|
||||
* connection for the whole operation; the in-process mutex is enough
|
||||
* for the single-server deployment and avoids pool-connection gymnastics.
|
||||
*/
|
||||
let catalogIdAllocChain: Promise<unknown> = Promise.resolve();
|
||||
@@ -296,11 +286,9 @@ export async function allocateCatalogItemId<T>(
|
||||
});
|
||||
await prev.catch(() => {});
|
||||
try {
|
||||
const maxIdResult = await prisma.$queryRaw<
|
||||
Array<{ maxId: number | bigint | null }>
|
||||
>`
|
||||
const [maxIdResult] = (await db.execute(sql`
|
||||
SELECT MAX(id) as maxId FROM catalog_items
|
||||
`;
|
||||
`)) as unknown as [Array<{ maxId: number | bigint | null }>, unknown];
|
||||
const nextId = Number(maxIdResult[0]?.maxId ?? 0) + 1;
|
||||
return await insertFn(nextId);
|
||||
} finally {
|
||||
@@ -318,21 +306,33 @@ export async function getOrCreateCategoryPage(
|
||||
const captionSave = `imp_${catKey}`;
|
||||
const catInfo = CATEGORY_PAGE[catKey] || CATEGORY_PAGE.other;
|
||||
|
||||
const existing = await prisma.catalogPages.findFirst({
|
||||
where: { captionSave, parentId },
|
||||
select: { id: true },
|
||||
});
|
||||
const [existing] = await db
|
||||
.select({ id: CatalogPages.id })
|
||||
.from(CatalogPages)
|
||||
.where(
|
||||
and(
|
||||
eq(CatalogPages.captionSave, captionSave),
|
||||
eq(CatalogPages.parentId, parentId),
|
||||
),
|
||||
)
|
||||
.limit(1);
|
||||
if (existing) return existing.id;
|
||||
|
||||
await prisma.$executeRaw`
|
||||
await db.execute(sql`
|
||||
INSERT INTO catalog_pages (caption_save, caption, page_layout, parent_id, min_rank, order_num, icon_image, enabled, visible, includes, page_headline, page_teaser, page_special, page_text1, page_text2, page_text_details, page_text_teaser)
|
||||
VALUES (${captionSave}, ${catInfo.label}, 'default_3x3', ${parentId}, '1', ${catInfo.order}, ${catInfo.icon}, '1', '1', '', '', '', '', '', '', '', '')
|
||||
`;
|
||||
`);
|
||||
|
||||
const created = await prisma.catalogPages.findFirst({
|
||||
where: { captionSave, parentId },
|
||||
select: { id: true },
|
||||
});
|
||||
const [created] = await db
|
||||
.select({ id: CatalogPages.id })
|
||||
.from(CatalogPages)
|
||||
.where(
|
||||
and(
|
||||
eq(CatalogPages.captionSave, captionSave),
|
||||
eq(CatalogPages.parentId, parentId),
|
||||
),
|
||||
)
|
||||
.limit(1);
|
||||
if (!created) {
|
||||
throw new Error(
|
||||
`Failed to create catalog sub-page "${captionSave}" under parent ${parentId}`,
|
||||
@@ -368,10 +368,11 @@ export async function importSingleFurni(params: {
|
||||
const warnings: string[] = [];
|
||||
|
||||
// Check if already exists by classname OR by spriteId (primary key collision).
|
||||
const existsByName = await prisma.itemsBase.findFirst({
|
||||
where: { itemName: classname },
|
||||
select: { id: true },
|
||||
});
|
||||
const [existsByName] = await db
|
||||
.select({ id: ItemsBase.id })
|
||||
.from(ItemsBase)
|
||||
.where(eq(ItemsBase.itemName, classname))
|
||||
.limit(1);
|
||||
if (existsByName) {
|
||||
return {
|
||||
ok: false,
|
||||
@@ -379,10 +380,11 @@ export async function importSingleFurni(params: {
|
||||
error: `Item "${classname}" already exists (id: ${existsByName.id})`,
|
||||
};
|
||||
}
|
||||
const existsById = await prisma.itemsBase.findUnique({
|
||||
where: { id: originalId },
|
||||
select: { itemName: true },
|
||||
});
|
||||
const [existsById] = await db
|
||||
.select({ itemName: ItemsBase.itemName })
|
||||
.from(ItemsBase)
|
||||
.where(eq(ItemsBase.id, originalId))
|
||||
.limit(1);
|
||||
if (existsById) {
|
||||
return {
|
||||
ok: false,
|
||||
@@ -417,10 +419,10 @@ export async function importSingleFurni(params: {
|
||||
// NOTE: live Arcturus `items_base` has no description/revision/rare columns —
|
||||
// those metadata fields are persisted in FurnitureData.json (see entry below),
|
||||
// not in the DB. Inserting them here throws "Unknown column 'description'".
|
||||
await prisma.$executeRaw`
|
||||
await db.execute(sql`
|
||||
INSERT INTO items_base (id, sprite_id, public_name, item_name, type)
|
||||
VALUES (${spriteId}, ${spriteId}, ${bestName}, ${classname}, ${itemType})
|
||||
`;
|
||||
`);
|
||||
} catch (err) {
|
||||
return {
|
||||
ok: false,
|
||||
@@ -547,7 +549,9 @@ export async function importSingleFurni(params: {
|
||||
// ── Rollback: if conversion failed AND no .nitro exists, remove DB record ──
|
||||
if (!existsSync(/*turbopackIgnore: true*/ nitroPath) && !conversionResult) {
|
||||
try {
|
||||
await prisma.$executeRaw`DELETE FROM items_base WHERE id = ${spriteId} AND item_name = ${classname}`;
|
||||
await db.execute(
|
||||
sql`DELETE FROM items_base WHERE id = ${spriteId} AND item_name = ${classname}`,
|
||||
);
|
||||
} catch {
|
||||
/* best effort cleanup */
|
||||
}
|
||||
@@ -635,7 +639,7 @@ export async function importSingleFurni(params: {
|
||||
allowStack,
|
||||
interactionType,
|
||||
});
|
||||
await prisma.$executeRawUnsafe(update.sql, ...update.values);
|
||||
await db.execute(update);
|
||||
} catch (err) {
|
||||
warnings.push(
|
||||
`items_base dimension update failed: ${(err as Error).message}`,
|
||||
@@ -676,11 +680,9 @@ export async function importSingleFurni(params: {
|
||||
const itemIdStr = String(spriteId);
|
||||
|
||||
// Check if this item already exists anywhere in the catalog
|
||||
const existingCatalog = await prisma.$queryRaw<
|
||||
Array<{ id: number; page_id: string }>
|
||||
>`
|
||||
const [existingCatalog] = (await db.execute(sql`
|
||||
SELECT id, page_id FROM catalog_items WHERE item_ids = ${itemIdStr} LIMIT 1
|
||||
`;
|
||||
`)) as unknown as [Array<{ id: number; page_id: string }>, unknown];
|
||||
|
||||
if (existingCatalog.length > 0) {
|
||||
catalogItemId = existingCatalog[0].id;
|
||||
@@ -696,10 +698,10 @@ export async function importSingleFurni(params: {
|
||||
// across concurrent imports via a MySQL named lock to avoid MAX(id)
|
||||
// TOCTOU that would cause duplicate-key failures.
|
||||
catalogItemId = await allocateCatalogItemId(async (nextCatalogId) => {
|
||||
await prisma.$executeRaw`
|
||||
await db.execute(sql`
|
||||
INSERT INTO catalog_items (id, page_id, item_ids, catalog_name, cost_credits, cost_points, points_type, amount, order_number, offer_id, extradata)
|
||||
VALUES (${nextCatalogId}, ${pageIdStr}, ${itemIdStr}, ${classname}, ${price.credits}, ${price.points}, ${price.pointsType}, 1, 1, '-1', '')
|
||||
`;
|
||||
`);
|
||||
return nextCatalogId;
|
||||
});
|
||||
}
|
||||
@@ -734,10 +736,11 @@ export async function deleteImportedItem(classname: string): Promise<{
|
||||
const errors: string[] = [];
|
||||
|
||||
// Look up item in DB
|
||||
const item = await prisma.itemsBase.findFirst({
|
||||
where: { itemName: classname },
|
||||
select: { id: true, type: true },
|
||||
});
|
||||
const [item] = await db
|
||||
.select({ id: ItemsBase.id, type: ItemsBase.type })
|
||||
.from(ItemsBase)
|
||||
.where(eq(ItemsBase.itemName, classname))
|
||||
.limit(1);
|
||||
|
||||
if (!item) {
|
||||
throw new Error(`Item "${classname}" not found in database`);
|
||||
@@ -754,14 +757,18 @@ export async function deleteImportedItem(classname: string): Promise<{
|
||||
// 1. Delete catalog_items
|
||||
try {
|
||||
const itemIdStr = String(spriteId);
|
||||
await prisma.$executeRaw`DELETE FROM catalog_items WHERE item_ids = ${itemIdStr} AND catalog_name = ${classname}`;
|
||||
await db.execute(
|
||||
sql`DELETE FROM catalog_items WHERE item_ids = ${itemIdStr} AND catalog_name = ${classname}`,
|
||||
);
|
||||
} catch (err) {
|
||||
errors.push(`catalog_items: ${(err as Error).message}`);
|
||||
}
|
||||
|
||||
// 2. Delete items_base
|
||||
try {
|
||||
await prisma.$executeRaw`DELETE FROM items_base WHERE id = ${spriteId} AND item_name = ${classname}`;
|
||||
await db.execute(
|
||||
sql`DELETE FROM items_base WHERE id = ${spriteId} AND item_name = ${classname}`,
|
||||
);
|
||||
} catch (err) {
|
||||
errors.push(`items_base: ${(err as Error).message}`);
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user