Add canvas workflow and harden data import
This commit is contained in:
@@ -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());
|
||||
|
||||
60
src/app/api/canvas/projects/[id]/route.ts
Executable file
60
src/app/api/canvas/projects/[id]/route.ts
Executable 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 });
|
||||
}
|
||||
}
|
||||
31
src/app/api/canvas/projects/route.ts
Executable file
31
src/app/api/canvas/projects/route.ts
Executable 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 });
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user