Files
OnePieceDle/scripts/import-json.ts
whidix d75c74ac3c Refactor character affiliations to singular form
- Updated character data structure to replace 'affiliations' and 'frAffiliations' with 'affiliation' and 'frAffiliation'.
- Modified related functions and components to accommodate the new structure.
- Adjusted database schema and server-side logic to reflect the changes in character affiliation handling.
- Ensured all references in the UI components and data import/export scripts are updated accordingly.
2026-04-14 21:56:26 +02:00

425 lines
12 KiB
TypeScript
Raw Permalink Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { createClient } from '@libsql/client';
import { drizzle } from 'drizzle-orm/libsql';
import { sql, eq, inArray } from 'drizzle-orm';
import fs from 'fs';
import { arc, character, devilFruit, characterScrapeValidation, type DevilFruitType } from '../src/lib/server/db/schema';
type Status = 'Alive' | 'Dead' | 'Unknown';
type ArcRecord = {
id: string;
name: string;
frName?: string | null;
startChapter: number;
endChapter?: number | null;
url?: string | null;
};
type DevilFruitRecord = {
id: string;
name: string;
type?: DevilFruitType | string | null;
url?: string | null;
};
type CharacterRecord = {
id: string;
name: string;
frName?: string | null;
gender?: string | null;
age?: number | null;
affiliation?: string | null;
frAffiliation?: string | null;
devilFruitId?: string | null;
hakiObservation?: boolean;
hakiArmament?: boolean;
hakiConqueror?: boolean;
bounty?: number | null;
height?: number | null;
origin?: string | null;
frOrigin?: string | null;
firstAppearance?: number;
pictureUrl?: string | null;
epithets?: string[] | string | null;
frEpithets?: string[] | string | null;
status?: Status | null;
arcId?: string | null;
url?: string | null;
frUrl?: string | null;
};
const DATABASE_URL = process.env.DATABASE_URL || 'file:local.db';
const client = createClient({ url: DATABASE_URL });
const db = drizzle(client);
function readJsonFile<T>(path: string): T[] | null {
if (!fs.existsSync(path)) {
return null;
}
const content = fs.readFileSync(path, 'utf-8');
return JSON.parse(content) as T[];
}
function toNullable<T>(value: T | undefined | null | ''): T | null {
return value === undefined || value === null || value === '' ? null : value;
}
function toJsonArray(value: string[] | string | null | undefined): string[] | null {
if (Array.isArray(value)) {
return value.length > 0 ? value : null;
}
if (typeof value === 'string' && value.trim() !== '') {
if (value.trim().startsWith('[')) {
try {
const parsed = JSON.parse(value);
return Array.isArray(parsed) ? parsed : [value];
} catch {
return [value];
}
}
const splitValues = value
.split(',')
.map((item) => item.trim())
.filter(Boolean);
return splitValues.length > 0 ? splitValues : null;
}
return null;
}
function toDevilFruitType(value: DevilFruitType | string | null | undefined): DevilFruitType | null {
if (!value) return null;
if (value === 'Paramecia' || value === 'Zoan' || value === 'Logia' || value === 'Smile' || value === 'Unknown') {
return value;
}
return 'Unknown';
}
function toNumber(value: string | number | null | undefined): number | null {
if (value === null || value === undefined || value === '') return null;
const num = typeof value === 'string' ? parseFloat(value) : value;
return isNaN(num) ? null : num;
}
function getErrorMessage(error: unknown): string {
return error instanceof Error ? error.message : String(error);
}
function logSqlOnError(statement: { sql: string; params: unknown[] } | null): void {
if (!statement) return;
console.error(` SQL: ${statement.sql}`);
console.error(` Params: ${JSON.stringify(statement.params)}`);
}
function transformCharacterData(item: CharacterRecord) {
return {
id: item.id,
name: item.name,
frName: toNullable(item.frName),
gender: toNullable(item.gender),
age: toNullable(item.age),
affiliation: toNullable(item.affiliation),
frAffiliation: toNullable(item.frAffiliation),
devilFruitId: toNullable(item.devilFruitId),
hakiObservation: !!item.hakiObservation,
hakiArmament: !!item.hakiArmament,
hakiConqueror: !!item.hakiConqueror,
bounty: item.bounty ?? 0,
height: toNumber(item.height as string | number | null),
origin: toNullable(item.origin),
frOrigin: toNullable(item.frOrigin),
firstAppearance: item.firstAppearance ?? 0,
pictureUrl: toNullable(item.pictureUrl),
epithets: toJsonArray(item.epithets),
frEpithets: toJsonArray(item.frEpithets),
status: toNullable(item.status),
arcId: toNullable(item.arcId),
url: toNullable(item.url),
frUrl: toNullable(item.frUrl),
isDeleted: false
};
}
async function isCharacterTableEmpty(): Promise<boolean> {
const result = await db.select({ count: sql<number>`COUNT(*)` }).from(character);
return result[0]?.count === 0;
}
async function importFromJson(): Promise<void> {
let totalSuccess = 0;
let totalErrors = 0;
try {
const arcs = readJsonFile<ArcRecord>('./scraped-data/arcs.json');
if (arcs) {
console.log('\n=== Importing Arcs ===\n');
console.log(`Found ${arcs.length} arcs\n`);
let successCount = 0;
let errorCount = 0;
for (let i = 0; i < arcs.length; i++) {
const item = arcs[i];
let lastSql: { sql: string; params: unknown[] } | null = null;
try {
const query = db
.insert(arc)
.values({
id: item.id,
name: item.name,
frName: toNullable(item.frName),
startChapter: item.startChapter,
endChapter: toNullable(item.endChapter),
url: toNullable(item.url)
})
.onConflictDoUpdate({
target: arc.id,
set: {
name: item.name,
frName: toNullable(item.frName),
startChapter: item.startChapter,
endChapter: toNullable(item.endChapter),
url: toNullable(item.url)
}
});
lastSql = query.toSQL();
await query;
successCount++;
process.stdout.write(`\rExecuted: ${successCount}/${arcs.length}`);
} catch (error) {
errorCount++;
console.error(`\n✗ Error at arc ${i + 1}:`);
console.error(` ID: ${item.id ?? 'N/A'}`);
console.error(` Message: ${getErrorMessage(error)}`);
logSqlOnError(lastSql);
}
}
console.log(`\n\n✓ Arcs imported!`);
console.log(` Success: ${successCount}`);
console.log(` Errors: ${errorCount}`);
totalSuccess += successCount;
totalErrors += errorCount;
} else {
console.log('\n⚠ No arcs.json found, skipping...\n');
}
const fruits = readJsonFile<DevilFruitRecord>('./scraped-data/devil-fruits.json');
if (fruits) {
console.log('\n=== Importing Devil Fruits ===\n');
console.log(`Found ${fruits.length} devil fruits\n`);
let successCount = 0;
let errorCount = 0;
for (let i = 0; i < fruits.length; i++) {
const item = fruits[i];
let lastSql: { sql: string; params: unknown[] } | null = null;
try {
const query = db
.insert(devilFruit)
.values({
id: item.id,
name: item.name,
type: toDevilFruitType(item.type),
url: toNullable(item.url)
})
.onConflictDoUpdate({
target: devilFruit.id,
set: {
name: item.name,
type: toDevilFruitType(item.type),
url: toNullable(item.url)
}
});
lastSql = query.toSQL();
await query;
successCount++;
process.stdout.write(`\rExecuted: ${successCount}/${fruits.length}`);
} catch (error) {
errorCount++;
console.error(`\n✗ Error at devil fruit ${i + 1}:`);
console.error(` ID: ${item.id ?? 'N/A'}`);
console.error(` Message: ${getErrorMessage(error)}`);
logSqlOnError(lastSql);
}
}
console.log(`\n\n✓ Devil Fruits imported!`);
console.log(` Success: ${successCount}`);
console.log(` Errors: ${errorCount}`);
totalSuccess += successCount;
totalErrors += errorCount;
} else {
console.log('\n⚠ No devil-fruits.json found, skipping...\n');
}
const characters = readJsonFile<CharacterRecord>('./scraped-data/characters.json');
if (characters) {
console.log('\n=== Importing Characters ===\n');
console.log(`Found ${characters.length} characters\n`);
const isEmpty = await isCharacterTableEmpty();
let successCount = 0;
let errorCount = 0;
if (isEmpty) {
// Populate empty character table
console.log('Characters table is empty, populating...\n');
for (let i = 0; i < characters.length; i++) {
const item = characters[i];
let lastSql: { sql: string; params: unknown[] } | null = null;
try {
const data = transformCharacterData(item);
const query = db
.insert(character)
.values(data)
.onConflictDoUpdate({
target: character.id,
set: data
});
lastSql = query.toSQL();
await query;
successCount++;
process.stdout.write(`\rExecuted: ${successCount}/${characters.length}`);
} catch (error) {
errorCount++;
console.error(`\n✗ Error at character ${i + 1}:`);
console.error(` ID: ${item.id ?? 'N/A'}`);
console.error(` Message: ${getErrorMessage(error)}`);
logSqlOnError(lastSql);
}
}
} else {
// Update scrapeValidation table
console.log('Characters table not empty, updating scrapeValidation table for changes...\n');
const scrapedCharacterIds: string[] = [];
for (let i = 0; i < characters.length; i++) {
const item = characters[i];
let lastSql: { sql: string; params: unknown[] } | null = null;
try {
const selectQuery = db
.select()
.from(character)
.where(eq(character.id, item.id));
lastSql = selectQuery.toSQL();
scrapedCharacterIds.push(item.id);
const jsonData = transformCharacterData(item);
const upsertQuery = db
.insert(characterScrapeValidation)
.values(jsonData)
.onConflictDoUpdate({
target: characterScrapeValidation.id,
set: jsonData
});
lastSql = upsertQuery.toSQL();
await upsertQuery;
successCount++;
process.stdout.write(`\rProcessed: ${successCount}/${characters.length}`);
} catch (error) {
errorCount++;
console.error(`\n✗ Error at character ${i + 1}:`);
console.error(` ID: ${item.id ?? 'N/A'}`);
console.error(` Message: ${getErrorMessage(error)}`);
logSqlOnError(lastSql);
}
}
// Fetch all characters from the character table and mark those absent from the
// scrape as deleted in scrape validation.
const allCharacters = await db.select({ id: character.id }).from(character);
const scrapedSet = new Set(scrapedCharacterIds);
const idsToMarkDeleted = allCharacters
.map((c) => c.id)
.filter((id) => !scrapedSet.has(id));
if (idsToMarkDeleted.length > 0) {
console.log(`\n⚠ Marking ${idsToMarkDeleted.length} character(s) as deleted in scrape validation...`);
const deletedCharacterRows = await db
.select()
.from(character)
.where(inArray(character.id, idsToMarkDeleted));
for (const row of deletedCharacterRows) {
await db
.insert(characterScrapeValidation)
.values({
id: row.id,
name: row.name,
frName: row.frName,
gender: row.gender,
age: row.age,
affiliation: row.affiliation,
frAffiliation: row.frAffiliation,
devilFruitId: row.devilFruitId,
hakiObservation: row.hakiObservation,
hakiArmament: row.hakiArmament,
hakiConqueror: row.hakiConqueror,
bounty: row.bounty,
height: row.height,
origin: row.origin,
frOrigin: row.frOrigin,
firstAppearance: row.firstAppearance,
pictureUrl: row.pictureUrl,
epithets: row.epithets,
frEpithets: row.frEpithets,
status: row.status,
arcId: row.arcId,
url: row.url,
frUrl: row.frUrl,
isDeleted: true
})
.onConflictDoUpdate({
target: characterScrapeValidation.id,
set: { isDeleted: true }
});
}
}
}
console.log(`\n\n✓ Characters imported!`);
console.log(` Success: ${successCount}`);
console.log(` Errors: ${errorCount}`);
totalSuccess += successCount;
totalErrors += errorCount;
} else {
console.log('\n⚠ No characters.json found, skipping...\n');
}
console.log(`\n=== Total Import Summary ===`);
console.log(` Total Success: ${totalSuccess}`);
console.log(` Total Errors: ${totalErrors}\n`);
} catch (error) {
console.error('✗ Import failed:', getErrorMessage(error));
process.exit(1);
} finally {
client.close();
}
}
importFromJson().catch((error) => {
console.error(getErrorMessage(error));
process.exit(1);
});