task(F1): wcadlib export/import endpoints + fix gfolder/gblock id collisions
This commit is contained in:
@@ -400,7 +400,7 @@ export class SqliteAdapter implements DatabaseInterface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
createGlobalFolder(data: Partial<DBGlobalBlockFolder>): DBGlobalBlockFolder {
|
createGlobalFolder(data: Partial<DBGlobalBlockFolder>): DBGlobalBlockFolder {
|
||||||
const id = data.id ?? `gfolder-${Date.now()}`;
|
const id = data.id ?? `gfolder-${randomUUID().slice(0, 8)}`;
|
||||||
this.db.prepare(
|
this.db.prepare(
|
||||||
'INSERT INTO global_block_folders (id, name, parent_id) VALUES (?, ?, ?)',
|
'INSERT INTO global_block_folders (id, name, parent_id) VALUES (?, ?, ?)',
|
||||||
).run(id, data.name ?? 'Neuer Ordner', data.parent_id ?? null);
|
).run(id, data.name ?? 'Neuer Ordner', data.parent_id ?? null);
|
||||||
@@ -438,7 +438,7 @@ export class SqliteAdapter implements DatabaseInterface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
createGlobalBlock(data: Partial<DBGlobalBlock>): DBGlobalBlock {
|
createGlobalBlock(data: Partial<DBGlobalBlock>): DBGlobalBlock {
|
||||||
const id = data.id ?? `gblock-${Date.now()}`;
|
const id = data.id ?? `gblock-${randomUUID().slice(0, 8)}`;
|
||||||
this.db.prepare(
|
this.db.prepare(
|
||||||
'INSERT INTO global_blocks (id, folder_id, name, block_data, svg_data) VALUES (?, ?, ?, ?, ?)',
|
'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);
|
).run(id, data.folder_id ?? null, data.name ?? 'Global Block', data.block_data ?? '{}', data.svg_data ?? null);
|
||||||
|
|||||||
@@ -164,4 +164,66 @@ export function registerGlobalBlockRoutes(fastify: FastifyInstance, db: Database
|
|||||||
if (!ok) return reply.code(404).send({ error: 'Global block not found' });
|
if (!ok) return reply.code(404).send({ error: 'Global block not found' });
|
||||||
return reply.code(204).send();
|
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<string, string>();
|
||||||
|
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 });
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user