const AWS = require('aws-sdk'); const dynamodb = new AWS.DynamoDB.DocumentClient(); const { v4: uuidv4 } = require('uuid'); const TABLE_NAME = process.env.TABLE_NAME; // CORS headers const headers = { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': process.env.CORS_ORIGIN || '*', 'Access-Control-Allow-Headers': 'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token', 'Access-Control-Allow-Methods': 'GET,POST,PUT,DELETE,OPTIONS', 'Access-Control-Allow-Credentials': 'true' }; // Helper function to create response const createResponse = (statusCode, body) => ({ statusCode, headers, body: JSON.stringify(body) }); // Helper to transform new structure to legacy format for backwards compatibility const transformToLegacy = (item) => { if (item._legacy) { // New structure - extract legacy fields return { id: item.id, brand: item.brand, tip: item._legacy.tip || item.type || item.material?.base, finish: item._legacy.finish || item.material?.modifier || 'Basic', boja: item._legacy.boja || item.color?.name, refill: item._legacy.refill || (item.condition?.isRefill ? 'Da' : ''), vakum: item._legacy.vakum || (item.condition?.storageCondition === 'vacuum' ? 'vakuum' : ''), otvoreno: item._legacy.otvoreno || (item.condition?.storageCondition === 'opened' ? 'otvorena' : ''), kolicina: item._legacy.kolicina || String(item.inventory?.total || ''), cena: item._legacy.cena || String(item.pricing?.purchasePrice || ''), status: item._legacy.status, createdAt: item.createdAt, updatedAt: item.updatedAt }; } // Already in legacy format return item; }; // GET all filaments or filter by query params const getFilaments = async (event) => { try { const queryParams = event.queryStringParameters || {}; let params = { TableName: TABLE_NAME }; // Support both old and new query parameters const brand = queryParams.brand; const materialBase = queryParams.material || queryParams.tip; const storageCondition = queryParams.storageCondition || queryParams.status; // Filter by brand if (brand) { params = { ...params, IndexName: 'brand-index', KeyConditionExpression: 'brand = :brand', ExpressionAttributeValues: { ':brand': brand } }; } // Filter by material (supports old 'tip' param) else if (materialBase) { // Try new index first, fall back to old try { params = { ...params, IndexName: 'material-base-index', KeyConditionExpression: '#mb = :mb', ExpressionAttributeNames: { '#mb': 'material.base' }, ExpressionAttributeValues: { ':mb': materialBase } }; } catch (e) { // Fall back to old index params = { ...params, IndexName: 'tip-index', KeyConditionExpression: 'tip = :tip', ExpressionAttributeValues: { ':tip': materialBase } }; } } // Filter by storage condition else if (storageCondition) { params = { ...params, FilterExpression: '#sc = :sc', ExpressionAttributeNames: { '#sc': 'condition.storageCondition' }, ExpressionAttributeValues: { ':sc': storageCondition } }; } // Execute query or scan let result; if (params.IndexName || params.KeyConditionExpression) { result = await dynamodb.query(params).promise(); } else { result = await dynamodb.scan(params).promise(); } // Check if client supports new format const acceptsNewFormat = event.headers?.['X-Accept-Format'] === 'v2'; if (acceptsNewFormat) { // Return new format return createResponse(200, result.Items); } else { // Transform to legacy format for backwards compatibility const legacyItems = result.Items.map(transformToLegacy); return createResponse(200, legacyItems); } } catch (error) { console.error('Error getting filaments:', error); return createResponse(500, { error: 'Failed to fetch filaments' }); } }; // GET single filament by ID const getFilament = async (event) => { try { const { id } = event.pathParameters; const params = { TableName: TABLE_NAME, Key: { id } }; const result = await dynamodb.get(params).promise(); if (!result.Item) { return createResponse(404, { error: 'Filament not found' }); } return createResponse(200, result.Item); } catch (error) { console.error('Error getting filament:', error); return createResponse(500, { error: 'Failed to fetch filament' }); } }; // Helper to generate SKU const generateSKU = (brand, material, color) => { const brandCode = brand.substring(0, 3).toUpperCase(); const materialCode = material.substring(0, 3).toUpperCase(); const colorCode = color.substring(0, 3).toUpperCase(); const random = Math.random().toString(36).substring(2, 5).toUpperCase(); return `${brandCode}-${materialCode}-${colorCode}-${random}`; }; // POST - Create new filament const createFilament = async (event) => { try { const body = JSON.parse(event.body); const timestamp = new Date().toISOString(); const acceptsNewFormat = event.headers?.['X-Accept-Format'] === 'v2'; let item; if (acceptsNewFormat || body.material) { // New format item = { id: uuidv4(), sku: body.sku || generateSKU(body.brand, body.material.base, body.color.name), ...body, createdAt: timestamp, updatedAt: timestamp }; } else { // Legacy format - convert to new structure const material = { base: body.tip || 'PLA', modifier: body.finish !== 'Basic' ? body.finish : null }; const storageCondition = body.vakum?.toLowerCase().includes('vakuum') ? 'vacuum' : body.otvoreno?.toLowerCase().includes('otvorena') ? 'opened' : 'sealed'; item = { id: uuidv4(), sku: generateSKU(body.brand, material.base, body.boja), brand: body.brand, type: body.tip || 'PLA', material: material, color: { name: body.boja, hex: null }, weight: { value: 1000, unit: 'g' }, diameter: 1.75, inventory: { total: parseInt(body.kolicina) || 1, available: storageCondition === 'opened' ? 0 : 1, inUse: 0, locations: { vacuum: storageCondition === 'vacuum' ? 1 : 0, opened: storageCondition === 'opened' ? 1 : 0, printer: 0 } }, pricing: { purchasePrice: body.cena ? parseFloat(body.cena) : null, currency: 'RSD' }, condition: { isRefill: body.refill === 'Da', storageCondition: storageCondition }, tags: [], _legacy: { tip: body.tip, finish: body.finish, boja: body.boja, refill: body.refill, vakum: body.vakum, otvoreno: body.otvoreno, kolicina: body.kolicina, cena: body.cena, status: body.status || 'new' }, createdAt: timestamp, updatedAt: timestamp }; } const params = { TableName: TABLE_NAME, Item: item }; await dynamodb.put(params).promise(); // Return in appropriate format const response = acceptsNewFormat ? item : transformToLegacy(item); return createResponse(201, response); } catch (error) { console.error('Error creating filament:', error); return createResponse(500, { error: 'Failed to create filament' }); } }; // PUT - Update filament const updateFilament = async (event) => { try { const { id } = event.pathParameters; const body = JSON.parse(event.body); const timestamp = new Date().toISOString(); // Determine status based on vakum and otvoreno fields let status = 'new'; if (body.otvoreno && body.otvoreno.toLowerCase().includes('otvorena')) { status = 'opened'; } else if (body.refill && body.refill.toLowerCase() === 'da') { status = 'refill'; } const params = { TableName: TABLE_NAME, Key: { id }, UpdateExpression: `SET brand = :brand, tip = :tip, finish = :finish, boja = :boja, refill = :refill, vakum = :vakum, otvoreno = :otvoreno, kolicina = :kolicina, cena = :cena, #status = :status, updatedAt = :updatedAt`, ExpressionAttributeNames: { '#status': 'status' }, ExpressionAttributeValues: { ':brand': body.brand, ':tip': body.tip, ':finish': body.finish, ':boja': body.boja, ':refill': body.refill, ':vakum': body.vakum, ':otvoreno': body.otvoreno, ':kolicina': body.kolicina, ':cena': body.cena, ':status': status, ':updatedAt': timestamp }, ReturnValues: 'ALL_NEW' }; const result = await dynamodb.update(params).promise(); return createResponse(200, result.Attributes); } catch (error) { console.error('Error updating filament:', error); return createResponse(500, { error: 'Failed to update filament' }); } }; // DELETE filament const deleteFilament = async (event) => { try { const { id } = event.pathParameters; const params = { TableName: TABLE_NAME, Key: { id } }; await dynamodb.delete(params).promise(); return createResponse(200, { message: 'Filament deleted successfully' }); } catch (error) { console.error('Error deleting filament:', error); return createResponse(500, { error: 'Failed to delete filament' }); } }; // Main handler exports.handler = async (event) => { const { httpMethod, resource } = event; // Handle CORS preflight if (httpMethod === 'OPTIONS') { return createResponse(200, {}); } // Route requests if (resource === '/filaments' && httpMethod === 'GET') { return getFilaments(event); } else if (resource === '/filaments' && httpMethod === 'POST') { return createFilament(event); } else if (resource === '/filaments/{id}' && httpMethod === 'GET') { return getFilament(event); } else if (resource === '/filaments/{id}' && httpMethod === 'PUT') { return updateFilament(event); } else if (resource === '/filaments/{id}' && httpMethod === 'DELETE') { return deleteFilament(event); } return createResponse(404, { error: 'Not found' }); };