feat : update
This commit is contained in:
@@ -110,6 +110,7 @@ and external services. Never enable it in a deployed environment.
|
||||
| `bun run db:generate` | Generate a Drizzle migration from schema changes |
|
||||
| `bun run db:migrate` | Apply committed Drizzle migrations |
|
||||
| `bun run db:seed` | Upsert the built-in templates |
|
||||
| `bun run import:guides` | Preview the one-time Google Sheets guide migration (`-- --apply` to write) |
|
||||
| `bun run worker:discord` | Listen for Lunaris update announcements |
|
||||
| `bun run worker:outbox` | Process cache and event outbox records |
|
||||
|
||||
|
||||
@@ -9,12 +9,12 @@ export default async function RegisterPage() {
|
||||
await connection();
|
||||
if (await getAdminSession()) redirect("/admin");
|
||||
if (await !getAdminSession()) redirect("/");
|
||||
return ( null
|
||||
// <div className="min-h-svh">
|
||||
// <SiteHeader />
|
||||
// <main className="grid min-h-[calc(100svh-4rem)] place-items-center px-4 py-12">
|
||||
// <RegisterCard />
|
||||
// </main>
|
||||
// </div>
|
||||
return (
|
||||
<div className="min-h-svh">
|
||||
<SiteHeader />
|
||||
<main className="grid min-h-[calc(100svh-4rem)] place-items-center px-4 py-12">
|
||||
<RegisterCard />
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
"discord.js": "^14.27.0",
|
||||
"drizzle-orm": "^0.45.2",
|
||||
"embla-carousel-react": "^8.6.0",
|
||||
"fflate": "^0.8.2",
|
||||
"ioredis": "^6.0.0",
|
||||
"lucide-react": "^1.35.0",
|
||||
"modern-screenshot": "^4.7.0",
|
||||
@@ -953,6 +954,8 @@
|
||||
|
||||
"fdir": ["[email protected]", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
|
||||
|
||||
"fflate": ["[email protected]", "", {}, "sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA=="],
|
||||
|
||||
"figures": ["[email protected]", "", { "dependencies": { "is-unicode-supported": "^2.0.0" } }, "sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg=="],
|
||||
|
||||
"file-entry-cache": ["[email protected]", "", { "dependencies": { "flat-cache": "^4.0.0" } }, "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ=="],
|
||||
|
||||
@@ -148,6 +148,13 @@
|
||||
"when": 1788411911516,
|
||||
"tag": "0020_aberrant_dark_beast",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 21,
|
||||
"version": "7",
|
||||
"when": 1788420000000,
|
||||
"tag": "0021_optional_team_rotation",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import {
|
||||
buildGuideImportDraft,
|
||||
charactersFromCombo,
|
||||
parseCsv,
|
||||
type GuideImportSource,
|
||||
type ImportCatalog,
|
||||
} from "@/lib/guides/google-sheet-import";
|
||||
|
||||
afterEach(() => vi.unstubAllGlobals());
|
||||
|
||||
describe("Google Sheet guide import", () => {
|
||||
it("parses quoted commas, escaped quotes, and multiline cells", () => {
|
||||
expect(parseCsv('"name","note"\n"A, B","line 1\nline ""2"""\n')).toEqual([
|
||||
["name", "note"],
|
||||
["A, B", 'line 1\nline "2"'],
|
||||
]);
|
||||
});
|
||||
|
||||
it("builds supported compact fields and reports missing catalog rows", async () => {
|
||||
const source: GuideImportSource = {
|
||||
characterName: "Hero",
|
||||
overview: "Guide Hero",
|
||||
workbookId: "sheet-id",
|
||||
coverId: "cover-id",
|
||||
layout: "compact",
|
||||
infoSheet: "Hero Info",
|
||||
teamSheets: [],
|
||||
comboSheets: [],
|
||||
weaponMetric: "overall-personal",
|
||||
};
|
||||
const catalog: ImportCatalog = {
|
||||
characters: [{ key: "hero", name: "Hero", constellations: [] }],
|
||||
weapons: [{ key: "weapon", name: "Known Weapon" }],
|
||||
artifacts: [{ key: "artifact", name: "Known Set" }],
|
||||
};
|
||||
const rows = Array.from({ length: 24 }, () => Array.from({ length: 21 }, () => ""));
|
||||
rows[0][10] = "Weapons อาวุธที่แนะนำ";
|
||||
rows[1][11] = "Known Weapon R5";
|
||||
rows[1][13] = "110%";
|
||||
rows[1][14] = "105%";
|
||||
rows[3][11] = "Missing Weapon R1";
|
||||
rows[3][13] = "100%";
|
||||
rows[5][10] = "Artifacts อาร์ติแฟกต์ที่แนะนำ (เลทเกม)";
|
||||
rows[6][11] = "4 Known Set";
|
||||
rows[6][13] = "100%";
|
||||
rows[8][5] = "ATK (พลังโจมตี)";
|
||||
rows[8][6] = "2000+";
|
||||
rows[8][7] = "สแตทที่แนะนำ เลทเกม (AR 45+)";
|
||||
vi.stubGlobal("fetch", vi.fn(async () => new Response(rows.map((row) => row.map((cell) => `"${cell}"`).join(",")).join("\n"))));
|
||||
|
||||
const draft = await buildGuideImportDraft(source, catalog);
|
||||
|
||||
expect(draft.weapons).toEqual([expect.objectContaining({ weaponKey: "weapon", refinement: 5, overallPercent: 110, personalPercent: 105 })]);
|
||||
expect(draft.artifacts.groups[0].rows).toEqual([expect.objectContaining({ firstArtifactKey: "artifact", percent: 100 })]);
|
||||
expect(draft.artifacts.targets).toEqual([{ stat: "ATK", value: "2000+" }]);
|
||||
expect(draft.skipped).toContain("weapon not in catalog: Missing Weapon R1");
|
||||
});
|
||||
|
||||
it("does not fetch or populate a keep-existing source", async () => {
|
||||
const fetchMock = vi.fn();
|
||||
vi.stubGlobal("fetch", fetchMock);
|
||||
const draft = await buildGuideImportDraft({
|
||||
characterName: "Yumemizuki Mizuki",
|
||||
overview: "existing",
|
||||
workbookId: "sheet-id",
|
||||
coverId: "cover-id",
|
||||
layout: "full",
|
||||
infoSheet: "Mizuki Info",
|
||||
teamSheets: [],
|
||||
comboSheets: [],
|
||||
skipWrite: true,
|
||||
}, {
|
||||
characters: [{ key: "mizuki", name: "Yumemizuki Mizuki", constellations: [] }],
|
||||
weapons: [],
|
||||
artifacts: [],
|
||||
});
|
||||
expect(fetchMock).not.toHaveBeenCalled();
|
||||
expect(draft.weapons).toEqual([]);
|
||||
});
|
||||
|
||||
it("resolves the unique characters named by a combo", () => {
|
||||
const characters = ["Yae Miko", "Odette", "Alyosha", "Sandrone"].map((name, index) => ({
|
||||
key: String(index),
|
||||
name,
|
||||
constellations: [],
|
||||
}));
|
||||
expect(charactersFromCombo("Yae EEE > Odette EE > Alyosha EQ > Sandrone 3CE Q", {
|
||||
characters,
|
||||
weapons: [],
|
||||
artifacts: [],
|
||||
}).map((item) => item.name)).toEqual(["Yae Miko", "Odette", "Alyosha", "Sandrone"]);
|
||||
});
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
@@ -16,6 +16,7 @@
|
||||
"db:migrate": "bun scripts/migrate.ts",
|
||||
"db:studio": "drizzle-kit studio",
|
||||
"db:seed": "bun scripts/seed-templates.ts",
|
||||
"import:guides": "bun scripts/import-google-guides.ts",
|
||||
"worker:discord": "bun backend/discord.ts",
|
||||
"worker:outbox": "bun scripts/outbox-worker.ts"
|
||||
},
|
||||
|
||||
@@ -0,0 +1,315 @@
|
||||
import { and, eq, inArray, isNull, sql } from "drizzle-orm";
|
||||
|
||||
import { closeDb, getDb } from "@/db/client";
|
||||
import {
|
||||
artifactGroups,
|
||||
artifactProfiles,
|
||||
artifactRecommendations,
|
||||
catalogArtifacts,
|
||||
catalogCharacters,
|
||||
catalogWeapons,
|
||||
constellationConditions,
|
||||
constellationRecommendations,
|
||||
extraSectionRows,
|
||||
extraSections,
|
||||
guides,
|
||||
guideSections,
|
||||
media,
|
||||
outboxEvents,
|
||||
teamCompositions,
|
||||
teamMembers,
|
||||
weaponRecommendations,
|
||||
} from "@/db/schema";
|
||||
import {
|
||||
artifactSectionSchema,
|
||||
constellationSectionSchema,
|
||||
extraSectionSchema,
|
||||
slugFromCharacterName,
|
||||
teamSectionSchema,
|
||||
weaponSectionSchema,
|
||||
} from "@/lib/guides/schemas";
|
||||
import {
|
||||
buildGuideImportDraft,
|
||||
guideImportSources,
|
||||
type GuideImportDraft,
|
||||
type ImportCatalog,
|
||||
} from "@/lib/guides/google-sheet-import";
|
||||
import { getMediaStorage } from "@/lib/media/storage";
|
||||
import { hasValidImageSignature, MAX_MEDIA_BYTES } from "@/lib/media/validation";
|
||||
|
||||
const apply = process.argv.includes("--apply");
|
||||
|
||||
interface CoverAsset {
|
||||
bytes: Uint8Array;
|
||||
extension: "gif" | "jpeg" | "png" | "webp";
|
||||
mimeType: "image/gif" | "image/jpeg" | "image/png" | "image/webp";
|
||||
objectKey: string;
|
||||
}
|
||||
|
||||
function coverType(bytes: Uint8Array): Pick<CoverAsset, "extension" | "mimeType"> | null {
|
||||
const types = [
|
||||
["png", "image/png"],
|
||||
["jpeg", "image/jpeg"],
|
||||
["webp", "image/webp"],
|
||||
["gif", "image/gif"],
|
||||
] as const;
|
||||
for (const [extension, mimeType] of types) {
|
||||
if (hasValidImageSignature(bytes.subarray(0, 16), mimeType)) return { extension, mimeType };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
async function downloadCover(draft: GuideImportDraft): Promise<CoverAsset> {
|
||||
const response = await fetch(`https://buzz.sudloh.com/cdn/${draft.source.coverId}`, {
|
||||
signal: AbortSignal.timeout(30_000),
|
||||
});
|
||||
if (!response.ok) throw new Error(`${draft.source.characterName}: cover returned ${response.status}`);
|
||||
const bytes = new Uint8Array(await response.arrayBuffer());
|
||||
if (!bytes.length || bytes.length > MAX_MEDIA_BYTES) throw new Error(`${draft.source.characterName}: invalid cover size ${bytes.length}`);
|
||||
const type = coverType(bytes);
|
||||
if (!type) throw new Error(`${draft.source.characterName}: unsupported cover image`);
|
||||
return {
|
||||
...type,
|
||||
bytes,
|
||||
objectKey: `imports/buzz-guide/${draft.characterKey}/cover.${type.extension}`,
|
||||
};
|
||||
}
|
||||
|
||||
function validateDraft(draft: GuideImportDraft): void {
|
||||
weaponSectionSchema.parse({ expectedVersion: 1, note: null, rows: draft.weapons });
|
||||
artifactSectionSchema.parse({ expectedVersion: 1, ...draft.artifacts });
|
||||
constellationSectionSchema.parse({ expectedVersion: 1, note: null, groups: draft.constellationGroups });
|
||||
teamSectionSchema.parse({ expectedVersion: 1, teams: draft.teams });
|
||||
extraSectionSchema.parse({
|
||||
expectedVersion: 1,
|
||||
sections: draft.extras.map((section) => ({
|
||||
title: section.title,
|
||||
rows: section.rows.map((row) => ({ ...row, mediaId: null, ratio: "16:9" as const })),
|
||||
})),
|
||||
});
|
||||
}
|
||||
|
||||
function reportFor(draft: GuideImportDraft) {
|
||||
return {
|
||||
character: draft.source.characterName,
|
||||
action: draft.source.skipWrite ? "keep-existing" : draft.source.characterName === "Columbina" ? "restore-or-update" : "create-or-update",
|
||||
weapons: draft.weapons.length,
|
||||
artifacts: draft.artifacts.groups.reduce((total, group) => total + group.rows.length, 0),
|
||||
constellations: draft.constellationGroups.reduce((total, group) => total + group.rows.length, 0),
|
||||
teams: draft.teams.length,
|
||||
extras: draft.extras.reduce((total, section) => total + section.rows.length, 0),
|
||||
skipped: [...new Set(draft.skipped)],
|
||||
};
|
||||
}
|
||||
|
||||
async function loadCatalog(): Promise<ImportCatalog> {
|
||||
const db = getDb();
|
||||
const [characters, weapons, artifacts] = await Promise.all([
|
||||
db.select({ key: catalogCharacters.key, name: catalogCharacters.name, constellations: catalogCharacters.constellations }).from(catalogCharacters),
|
||||
db.select({ key: catalogWeapons.key, name: catalogWeapons.name }).from(catalogWeapons),
|
||||
db.select({ key: catalogArtifacts.key, name: catalogArtifacts.name }).from(catalogArtifacts),
|
||||
]);
|
||||
return { characters, weapons, artifacts };
|
||||
}
|
||||
|
||||
async function uniqueSlug(tx: Parameters<Parameters<ReturnType<typeof getDb>["transaction"]>[0]>[0], name: string, key: string): Promise<string> {
|
||||
const base = slugFromCharacterName(name, key);
|
||||
const [collision] = await tx.select({ id: guides.id }).from(guides).where(eq(guides.slug, base)).limit(1);
|
||||
return collision ? `${base}-${key.toLowerCase()}` : base;
|
||||
}
|
||||
|
||||
async function replaceGuide(
|
||||
tx: Parameters<Parameters<ReturnType<typeof getDb>["transaction"]>[0]>[0],
|
||||
draft: GuideImportDraft,
|
||||
coverMediaId: string,
|
||||
sortOrder: number,
|
||||
): Promise<void> {
|
||||
const [catalogCharacter] = await tx.select().from(catalogCharacters).where(eq(catalogCharacters.key, draft.characterKey)).limit(1);
|
||||
if (!catalogCharacter) throw new Error(`${draft.source.characterName}: character disappeared from catalog`);
|
||||
const [existing] = await tx.select().from(guides).where(eq(guides.characterKey, draft.characterKey)).limit(1);
|
||||
let guideId: string;
|
||||
let version: number;
|
||||
if (existing) {
|
||||
guideId = existing.id;
|
||||
version = existing.version + 1;
|
||||
await tx.update(guides).set({
|
||||
overview: draft.source.overview,
|
||||
coverMediaId,
|
||||
sortOrder,
|
||||
trashedAt: null,
|
||||
version,
|
||||
}).where(eq(guides.id, guideId));
|
||||
if (existing.coverMediaId !== coverMediaId) {
|
||||
await tx.update(media).set({ currentReferenceCount: sql`greatest(${media.currentReferenceCount} - 1, 0)` }).where(eq(media.id, existing.coverMediaId));
|
||||
await tx.update(media).set({ currentReferenceCount: sql`${media.currentReferenceCount} + 1` }).where(eq(media.id, coverMediaId));
|
||||
}
|
||||
} else {
|
||||
const slug = await uniqueSlug(tx, catalogCharacter.name, catalogCharacter.key);
|
||||
const [created] = await tx.insert(guides).values({
|
||||
characterKey: catalogCharacter.key,
|
||||
slug,
|
||||
name: catalogCharacter.name,
|
||||
overview: draft.source.overview,
|
||||
coverMediaId,
|
||||
sortOrder,
|
||||
}).returning();
|
||||
guideId = created.id;
|
||||
version = created.version;
|
||||
await tx.update(media).set({ currentReferenceCount: sql`${media.currentReferenceCount} + 1` }).where(eq(media.id, coverMediaId));
|
||||
}
|
||||
|
||||
await Promise.all([
|
||||
tx.delete(weaponRecommendations).where(eq(weaponRecommendations.guideId, guideId)),
|
||||
tx.delete(artifactGroups).where(eq(artifactGroups.guideId, guideId)),
|
||||
tx.delete(artifactProfiles).where(eq(artifactProfiles.guideId, guideId)),
|
||||
tx.delete(constellationConditions).where(eq(constellationConditions.guideId, guideId)),
|
||||
tx.delete(teamCompositions).where(eq(teamCompositions.guideId, guideId)),
|
||||
tx.delete(guideSections).where(eq(guideSections.guideId, guideId)),
|
||||
]);
|
||||
|
||||
const defaultSections = [
|
||||
{ kind: "weapon", slug: "weapon", title: "Weapons", sortOrder: 0 },
|
||||
{ kind: "artifact", slug: "artifact", title: "Artifacts & Stats", sortOrder: 1 },
|
||||
{ kind: "constellations", slug: "constellations", title: "Constellations", sortOrder: 2 },
|
||||
{ kind: "team", slug: "team", title: "Team Calc", sortOrder: 3 },
|
||||
];
|
||||
await tx.insert(guideSections).values(defaultSections.map((section) => ({ ...section, guideId })));
|
||||
|
||||
if (draft.weapons.length) {
|
||||
await tx.insert(weaponRecommendations).values(draft.weapons.map((row, rowIndex) => ({
|
||||
...row,
|
||||
guideId,
|
||||
sortOrder: rowIndex,
|
||||
overallPercent: row.overallPercent?.toString() ?? null,
|
||||
personalPercent: row.personalPercent?.toString() ?? null,
|
||||
})));
|
||||
}
|
||||
for (const [groupIndex, group] of draft.artifacts.groups.entries()) {
|
||||
const [created] = await tx.insert(artifactGroups).values({ guideId, name: group.name, sortOrder: groupIndex }).returning();
|
||||
if (group.rows.length) {
|
||||
await tx.insert(artifactRecommendations).values(group.rows.map((row, rowIndex) => ({
|
||||
...row,
|
||||
groupId: created.id,
|
||||
sortOrder: rowIndex,
|
||||
percent: row.percent.toString(),
|
||||
})));
|
||||
}
|
||||
}
|
||||
await tx.insert(artifactProfiles).values({
|
||||
guideId,
|
||||
sands: draft.artifacts.sands,
|
||||
goblet: draft.artifacts.goblet,
|
||||
circlet: draft.artifacts.circlet,
|
||||
substats: draft.artifacts.substats,
|
||||
targets: draft.artifacts.targets,
|
||||
talents: draft.artifacts.talents,
|
||||
note: draft.artifacts.note,
|
||||
});
|
||||
for (const [groupIndex, group] of draft.constellationGroups.entries()) {
|
||||
const [created] = await tx.insert(constellationConditions).values({ guideId, note: group.note, sortOrder: groupIndex }).returning();
|
||||
if (group.rows.length) {
|
||||
await tx.insert(constellationRecommendations).values(group.rows.map((row) => ({
|
||||
...row,
|
||||
conditionId: created.id,
|
||||
cumulativePercent: row.cumulativePercent.toString(),
|
||||
})));
|
||||
}
|
||||
}
|
||||
for (const [teamIndex, team] of draft.teams.entries()) {
|
||||
const [created] = await tx.insert(teamCompositions).values({
|
||||
guideId,
|
||||
title: team.title,
|
||||
rotationSeconds: team.rotationSeconds?.toString() ?? null,
|
||||
combo: team.combo,
|
||||
sortOrder: teamIndex,
|
||||
}).returning();
|
||||
await tx.insert(teamMembers).values(team.members.map((member, position) => ({
|
||||
...member,
|
||||
teamId: created.id,
|
||||
position,
|
||||
damage: member.damage.toString(),
|
||||
})));
|
||||
}
|
||||
for (const [extraIndex, section] of draft.extras.entries()) {
|
||||
const [createdSection] = await tx.insert(guideSections).values({
|
||||
guideId,
|
||||
kind: "extra",
|
||||
slug: `extra-${extraIndex + 1}`,
|
||||
title: section.title,
|
||||
sortOrder: extraIndex + 4,
|
||||
}).returning();
|
||||
await tx.insert(extraSections).values({ sectionId: createdSection.id });
|
||||
await tx.insert(extraSectionRows).values(section.rows.map((row, rowIndex) => ({
|
||||
sectionId: createdSection.id,
|
||||
heading: row.heading,
|
||||
body: row.body,
|
||||
percent: row.percent?.toString() ?? null,
|
||||
ratio: "16:9",
|
||||
sortOrder: rowIndex,
|
||||
})));
|
||||
}
|
||||
|
||||
await tx.insert(outboxEvents).values([
|
||||
{ topic: "directory", aggregateId: guideId, eventType: "directory.updated", payload: { id: guideId, version } },
|
||||
{ topic: `page:${guideId}`, aggregateId: guideId, eventType: "page.updated", payload: { id: guideId, version } },
|
||||
{ topic: "admin", aggregateId: guideId, eventType: "admin.updated", payload: { id: guideId, version } },
|
||||
]);
|
||||
}
|
||||
|
||||
async function applyDrafts(drafts: GuideImportDraft[]): Promise<void> {
|
||||
const writable = drafts.filter((draft) => !draft.source.skipWrite);
|
||||
const covers = new Map<string, CoverAsset>();
|
||||
for (const draft of writable) covers.set(draft.characterKey, await downloadCover(draft));
|
||||
const storage = await getMediaStorage();
|
||||
for (const cover of covers.values()) {
|
||||
await storage.write(cover.objectKey, cover.bytes, { type: cover.mimeType, acl: "public-read" });
|
||||
}
|
||||
|
||||
const db = getDb();
|
||||
const [{ maximum }] = await db.select({ maximum: sql<number>`coalesce(max(${guides.sortOrder}), 0)` }).from(guides).where(isNull(guides.trashedAt));
|
||||
await db.transaction(async (tx) => {
|
||||
for (const [index, draft] of writable.entries()) {
|
||||
const cover = covers.get(draft.characterKey)!;
|
||||
let [coverRecord] = await tx.select().from(media).where(eq(media.objectKey, cover.objectKey)).limit(1);
|
||||
if (!coverRecord) {
|
||||
[coverRecord] = await tx.insert(media).values({
|
||||
objectKey: cover.objectKey,
|
||||
fileName: `cover.${cover.extension}`,
|
||||
mimeType: cover.mimeType,
|
||||
byteSize: cover.bytes.length,
|
||||
status: "ready",
|
||||
}).returning();
|
||||
} else {
|
||||
[coverRecord] = await tx.update(media).set({
|
||||
mimeType: cover.mimeType,
|
||||
byteSize: cover.bytes.length,
|
||||
status: "ready",
|
||||
}).where(eq(media.id, coverRecord.id)).returning();
|
||||
}
|
||||
await replaceGuide(tx, draft, coverRecord.id, Number(maximum) + index + 1);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function main(): Promise<void> {
|
||||
const catalog = await loadCatalog();
|
||||
const drafts: GuideImportDraft[] = [];
|
||||
for (const source of guideImportSources) {
|
||||
const draft = await buildGuideImportDraft(source, catalog);
|
||||
validateDraft(draft);
|
||||
drafts.push(draft);
|
||||
}
|
||||
console.log(JSON.stringify({ mode: apply ? "apply" : "dry-run", guides: drafts.map(reportFor) }, null, 2));
|
||||
if (!apply) return;
|
||||
await applyDrafts(drafts);
|
||||
const sourceKeys = drafts.map((draft) => draft.characterKey);
|
||||
const active = await getDb().select({ characterKey: guides.characterKey }).from(guides).where(and(inArray(guides.characterKey, sourceKeys), isNull(guides.trashedAt)));
|
||||
if (active.length !== sourceKeys.length) throw new Error(`Expected ${sourceKeys.length} active source guides, found ${active.length}`);
|
||||
console.log(`Imported ${drafts.filter((draft) => !draft.source.skipWrite).length} guides; ${active.length} source guides are active.`);
|
||||
}
|
||||
|
||||
try {
|
||||
await main();
|
||||
} finally {
|
||||
await closeDb();
|
||||
}
|
||||
Reference in New Issue
Block a user