diff --git a/backend/src/database/SqliteAdapter.ts b/backend/src/database/SqliteAdapter.ts index 042efec..e85efb8 100644 --- a/backend/src/database/SqliteAdapter.ts +++ b/backend/src/database/SqliteAdapter.ts @@ -400,7 +400,7 @@ export class SqliteAdapter implements DatabaseInterface { } createGlobalFolder(data: Partial): DBGlobalBlockFolder { - const id = data.id ?? `gfolder-${Date.now()}`; + const id = data.id ?? `gfolder-${randomUUID().slice(0, 8)}`; this.db.prepare( 'INSERT INTO global_block_folders (id, name, parent_id) VALUES (?, ?, ?)', ).run(id, data.name ?? 'Neuer Ordner', data.parent_id ?? null); @@ -438,7 +438,7 @@ export class SqliteAdapter implements DatabaseInterface { } createGlobalBlock(data: Partial): DBGlobalBlock { - const id = data.id ?? `gblock-${Date.now()}`; + const id = data.id ?? `gblock-${randomUUID().slice(0, 8)}`; this.db.prepare( 'INSERT INTO global_blocks (id, folder_id, name, block_data, svg_data) VALUES (?, ?, ?, ?, ?)', ).run(id, data.folder_id ?? null, data.name ?? 'Global Block', data.block_data ?? '{}', data.svg_data ?? null); diff --git a/backend/src/routes/globalBlocks.ts b/backend/src/routes/globalBlocks.ts index 31ebbba..0cebe5c 100644 --- a/backend/src/routes/globalBlocks.ts +++ b/backend/src/routes/globalBlocks.ts @@ -164,4 +164,66 @@ export function registerGlobalBlockRoutes(fastify: FastifyInstance, db: Database if (!ok) return reply.code(404).send({ error: 'Global block not found' }); return reply.code(204).send(); }); + + // ─── Task F1: Library-Paket Export/Import (.wcadlib) ────── + + // Export: komplette Library als JSON-Paket (Ordner + Blöcke) + fastify.get('/api/global-blocks/export', async (request, reply) => { + if (!requireAuth(request, reply, authService)) return; + const folders = db.listGlobalFolders(); + const blocks = db.listGlobalBlocks(); + return { + format: 'wcadlib', + version: 1, + exported_at: new Date().toISOString(), + folders: folders.map((f) => ({ id: f.id, name: f.name, parent_id: f.parent_id })), + blocks: blocks.map((b) => ({ + name: b.name, + folder_id: b.folder_id, + block_data: b.block_data, + svg_data: b.svg_data, + })), + }; + }); + + // Import: .wcadlib-Paket einspielen (IDs neu, Ordner-Hierarchie über Mapping erhalten) + fastify.post('/api/global-blocks/import', async (request, reply) => { + if (!requireAuth(request, reply, authService)) return; + const pkg = request.body as { + format?: string; + folders?: Array<{ id: string; name: string; parent_id: string | null }>; + blocks?: Array<{ name: string; folder_id: string | null; block_data: string; svg_data: string | null }>; + }; + if (!pkg || pkg.format !== 'wcadlib' || !Array.isArray(pkg.blocks)) { + return reply.code(400).send({ error: 'Invalid wcadlib package (format != wcadlib or blocks missing)' }); + } + + // Ordner mit ID-Mapping importieren: + const folderIdMap = new Map(); + const sortedFolders = [...(pkg.folders ?? [])].sort((a, b) => { + if (a.parent_id === b.parent_id) return 0; + if (a.parent_id === null) return -1; + if (b.parent_id === null) return 1; + return 0; + }); + for (const f of sortedFolders) { + const newParent = f.parent_id ? folderIdMap.get(f.parent_id) ?? null : null; + const created = db.createGlobalFolder({ name: f.name, parent_id: newParent }); + folderIdMap.set(f.id, created.id); + } + + // Blöcke importieren: + let imported = 0; + for (const b of pkg.blocks) { + const newFolderId = b.folder_id ? folderIdMap.get(b.folder_id) ?? null : null; + db.createGlobalBlock({ + name: b.name, + folder_id: newFolderId, + block_data: b.block_data, + svg_data: b.svg_data, + }); + imported++; + } + return reply.code(201).send({ imported_blocks: imported, imported_folders: folderIdMap.size }); + }); } diff --git a/backend/tests/globalBlocks.test.ts b/backend/tests/globalBlocks.test.ts index c0c719e..cafad0b 100644 --- a/backend/tests/globalBlocks.test.ts +++ b/backend/tests/globalBlocks.test.ts @@ -424,3 +424,104 @@ describe('Global Blocks API', () => { }); }); }); + +// ─── Task F1: Library-Paket Export/Import (.wcadlib) ────── + +describe('Global Blocks API – F1 export/import', () => { + let app: FastifyInstance; + let db: SqliteAdapter; + let authToken: string; + + beforeAll(async () => { + db = new SqliteAdapter(':memory:'); + await db.init(); + app = await createServer({ db, port: 0 }); + await app.ready(); + const regRes = await app.inject({ + method: 'POST', + url: '/api/auth/register', + payload: { email: 'f1-export@example.com', password: 'Password123!', name: 'F1 Test', role: 'admin' }, + }); + authToken = JSON.parse(regRes.body).session.token; + }); + + afterAll(async () => { + await app.close(); + db.close(); + }); + + it('GET /api/global-blocks/export liefert wcadlib-Paket mit folders+blocks', async () => { + // Setup: 1 Ordner + 1 Block + const folder = await app.inject({ + method: 'POST', + url: '/api/global-folders', + headers: { authorization: `Bearer ${authToken}` }, + payload: { name: 'F1-Ordner' }, + }); + const folderId = JSON.parse(folder.body).id; + await app.inject({ + method: 'POST', + url: '/api/global-blocks', + headers: { authorization: `Bearer ${authToken}` }, + payload: { name: 'F1-Block', folder_id: folderId, block_data: '{"type":"rect"}' }, + }); + + const res = await app.inject({ + method: 'GET', + url: '/api/global-blocks/export', + headers: { authorization: `Bearer ${authToken}` }, + }); + expect(res.statusCode).toBe(200); + const pkg = JSON.parse(res.body); + expect(pkg.format).toBe('wcadlib'); + expect(Array.isArray(pkg.folders)).toBe(true); + expect(Array.isArray(pkg.blocks)).toBe(true); + expect(pkg.folders.length).toBeGreaterThanOrEqual(1); + expect(pkg.blocks.length).toBeGreaterThanOrEqual(1); + }); + + it('POST /api/global-blocks/import spielt Paket mit Ordner-Mapping ein', async () => { + const pkg = { + format: 'wcadlib', + version: 1, + folders: [ + { id: 'src-folder-1', name: 'Import-Ordner', parent_id: null }, + ], + blocks: [ + { name: 'Import-Block', folder_id: 'src-folder-1', block_data: '{"type":"circle"}', svg_data: null }, + { name: 'Import-Block-Root', folder_id: null, block_data: '{}', svg_data: null }, + ], + }; + const res = await app.inject({ + method: 'POST', + url: '/api/global-blocks/import', + headers: { authorization: `Bearer ${authToken}` }, + payload: pkg, + }); + expect(res.statusCode, res.body).toBe(201); + const body = JSON.parse(res.body); + expect(body.imported_blocks).toBe(2); + expect(body.imported_folders).toBe(1); + + // Block im importierten Ordner auffindbar: + const blocks = await app.inject({ + method: 'GET', + url: '/api/global-blocks', + headers: { authorization: `Bearer ${authToken}` }, + }); + const list = JSON.parse(blocks.body) as Array<{ name: string; folder_id: string | null }>; + const importedBlock = list.find((b) => b.name === 'Import-Block'); + expect(importedBlock).toBeDefined(); + expect(importedBlock!.folder_id).not.toBeNull(); // Mapping erhalten + }); + + it('Import mit falschem format liefert 400', async () => { + const res = await app.inject({ + method: 'POST', + url: '/api/global-blocks/import', + headers: { authorization: `Bearer ${authToken}` }, + payload: { format: 'wrong', blocks: [] }, + }); + expect(res.statusCode).toBe(400); + }); +});