Files
Filamenteka/lambda/filaments/index.js
DaX 18110ab159 Major restructure: Remove Confluence, add V2 data structure, organize for dev/prod
- Import real data from PDF (35 Bambu Lab filaments)
- Remove all Confluence integration and dependencies
- Implement new V2 data structure with proper inventory tracking
- Add backwards compatibility for existing data
- Create enhanced UI components (ColorSwatch, InventoryBadge, MaterialBadge)
- Add advanced filtering with quick filters and multi-criteria search
- Organize codebase for dev/prod environments
- Update Lambda functions to support both V1/V2 formats
- Add inventory summary dashboard
- Clean up project structure and documentation

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-06-20 01:12:50 +02:00

362 lines
11 KiB
JavaScript

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' });
};