Add canvas workflow and harden data import

This commit is contained in:
FengLee
2026-05-09 23:54:18 +08:00
parent 1a0607fe8d
commit 24be9c550b
15 changed files with 3257 additions and 4 deletions

View File

@@ -75,6 +75,7 @@ type ImportContext = {
apiKeyIdMap: Map<string, string>;
apiKeyOwnerIdMap: Map<string, string>;
columnCache: Map<string, Set<string>>;
defaultableColumnCache: Map<string, Set<string>>;
};
export async function POST(request: NextRequest) {
@@ -136,11 +137,15 @@ async function importRows(
let skipped = 0;
const errors: string[] = [];
const existingColumns = await getExistingColumns(client, table, context);
const defaultableColumns = await getDefaultableColumns(client, table, context);
const effectiveAllowedColumns = allowedColumns.filter(col => existingColumns.has(col));
for (const rawRow of rows) {
const row = await normalizeImportRow(table, rawRow as Record<string, unknown>, context);
const cols = Object.keys(row).filter(col => effectiveAllowedColumns.includes(col));
const cols = Object.keys(row).filter(col => (
effectiveAllowedColumns.includes(col)
&& !(row[col] == null && defaultableColumns.has(col))
));
if (!cols.includes('id') || cols.length === 0) {
skipped++;
errors.push(`${table}: 缺少 id 或没有允许导入的字段`);
@@ -235,7 +240,15 @@ async function buildImportContext(
apiKeyIdMap.set(oldId, isUuid(oldId) ? oldId : crypto.randomUUID());
}
const ownerId = findImportedWorkUserId(row);
const ownerByEmail = findUserIdByEmail(row, { userIdMap, workIdMap, emailUserIdMap, apiKeyIdMap, apiKeyOwnerIdMap, columnCache: new Map() });
const ownerByEmail = findUserIdByEmail(row, {
userIdMap,
workIdMap,
emailUserIdMap,
apiKeyIdMap,
apiKeyOwnerIdMap,
columnCache: new Map(),
defaultableColumnCache: new Map(),
});
const mappedOwnerId = ownerId
? (userIdMap.get(ownerId) || ownerId)
: ownerByEmail;
@@ -266,7 +279,15 @@ async function buildImportContext(
}
}
return { userIdMap, workIdMap, emailUserIdMap, apiKeyIdMap, apiKeyOwnerIdMap, columnCache: new Map() };
return {
userIdMap,
workIdMap,
emailUserIdMap,
apiKeyIdMap,
apiKeyOwnerIdMap,
columnCache: new Map(),
defaultableColumnCache: new Map(),
};
}
async function normalizeImportRow(table: string, row: Record<string, unknown>, context: ImportContext): Promise<Record<string, unknown>> {
@@ -331,6 +352,12 @@ async function normalizeImportRow(table: string, row: Record<string, unknown>, c
}
if (table === 'user_api_keys') {
if (typeof next.note !== 'string' || next.note.trim() === '') {
next.note = '导入的 API Key';
}
if (typeof next.type !== 'string' || next.type.trim() === '') {
next.type = 'image';
}
const rawEncrypted = typeof next.api_key_encrypted === 'string' ? next.api_key_encrypted.trim() : '';
const rawApiKey = typeof next.apiKey === 'string' ? next.apiKey.trim() : '';
const secret = rawApiKey || rawEncrypted;
@@ -519,6 +546,29 @@ async function getExistingColumns(
return columns;
}
async function getDefaultableColumns(
client: Awaited<ReturnType<typeof getDbClient>>,
table: string,
context: ImportContext,
): Promise<Set<string>> {
const cached = context.defaultableColumnCache.get(table);
if (cached) return cached;
const [schemaName, tableName] = table.includes('.') ? table.split('.', 2) : ['public', table];
const result = await client.query(
`SELECT column_name
FROM information_schema.columns
WHERE table_schema = $1
AND table_name = $2
AND is_nullable = 'NO'
AND column_default IS NOT NULL`,
[schemaName, tableName],
);
const columns = new Set((result.rows || []).map((row: Record<string, unknown>) => String(row.column_name)));
context.defaultableColumnCache.set(table, columns);
return columns;
}
function seedUuidMap(map: Map<string, string>, value: unknown): void {
if (typeof value === 'string' && value && !isUuid(value) && !map.has(value)) {
map.set(value, crypto.randomUUID());

View File

@@ -0,0 +1,60 @@
import { NextRequest, NextResponse } from 'next/server';
import { deleteCanvasProject, getCanvasProject, updateCanvasProject } from '@/lib/canvas-store';
import { getAuthenticatedUserId } from '@/lib/session-auth';
import { normalizeCanvasState } from '@/lib/canvas-store';
type RouteContext = {
params: Promise<{ id: string }>;
};
export async function GET(request: NextRequest, context: RouteContext) {
try {
const userId = await getAuthenticatedUserId(request);
if (!userId) return NextResponse.json({ error: '请先登录' }, { status: 401 });
const { id } = await context.params;
const project = await getCanvasProject(userId, id);
if (!project) return NextResponse.json({ error: '画布不存在' }, { status: 404 });
return NextResponse.json({ project });
} catch (error) {
console.error('[canvas/projects/:id] GET error:', error);
return NextResponse.json({ error: '读取画布项目失败' }, { status: 500 });
}
}
export async function PUT(request: NextRequest, context: RouteContext) {
try {
const userId = await getAuthenticatedUserId(request);
if (!userId) return NextResponse.json({ error: '请先登录' }, { status: 401 });
const { id } = await context.params;
const body = await request.json().catch(() => ({}));
const project = await updateCanvasProject(userId, id, {
title: typeof body.title === 'string' ? body.title : undefined,
state: body.state ? normalizeCanvasState(body.state) : undefined,
});
if (!project) return NextResponse.json({ error: '画布不存在' }, { status: 404 });
return NextResponse.json({ project });
} catch (error) {
console.error('[canvas/projects/:id] PUT error:', error);
return NextResponse.json({ error: '保存画布项目失败' }, { status: 500 });
}
}
export async function DELETE(request: NextRequest, context: RouteContext) {
try {
const userId = await getAuthenticatedUserId(request);
if (!userId) return NextResponse.json({ error: '请先登录' }, { status: 401 });
const { id } = await context.params;
const deleted = await deleteCanvasProject(userId, id);
if (!deleted) return NextResponse.json({ error: '画布不存在' }, { status: 404 });
return NextResponse.json({ ok: true });
} catch (error) {
console.error('[canvas/projects/:id] DELETE error:', error);
return NextResponse.json({ error: '删除画布项目失败' }, { status: 500 });
}
}

View File

@@ -0,0 +1,31 @@
import { NextRequest, NextResponse } from 'next/server';
import { createCanvasProject, listCanvasProjects } from '@/lib/canvas-store';
import { getAuthenticatedUserId } from '@/lib/session-auth';
export async function GET(request: NextRequest) {
try {
const userId = await getAuthenticatedUserId(request);
if (!userId) return NextResponse.json({ error: '请先登录' }, { status: 401 });
const projects = await listCanvasProjects(userId);
return NextResponse.json({ projects });
} catch (error) {
console.error('[canvas/projects] GET error:', error);
return NextResponse.json({ error: '读取画布项目失败' }, { status: 500 });
}
}
export async function POST(request: NextRequest) {
try {
const userId = await getAuthenticatedUserId(request);
if (!userId) return NextResponse.json({ error: '请先登录' }, { status: 401 });
const body = await request.json().catch(() => ({}));
const title = typeof body.title === 'string' ? body.title : '未命名画布';
const project = await createCanvasProject(userId, title);
return NextResponse.json({ project }, { status: 201 });
} catch (error) {
console.error('[canvas/projects] POST error:', error);
return NextResponse.json({ error: '创建画布项目失败' }, { status: 500 });
}
}