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());