Skip to content

Commit 74b6ba2

Browse files
Merge pull request #54 from Tebrihk/metadata_cache
Asset Metadata Cache
2 parents 59ea005 + 7f92de7 commit 74b6ba2

File tree

7 files changed

+1443
-1
lines changed

7 files changed

+1443
-1
lines changed

.env.example

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,31 @@
1+
# Algorand Configuration
12
# Algorand Network Configuration
23
ALGOD_TOKEN=
34
ALGOD_SERVER=https://testnet-api.algonode.cloud
45
ALGOD_PORT=443
56

7+
# PostgreSQL Database Configuration
8+
DATABASE_URL=postgresql://username:password@localhost:5432/leaseflow_db
9+
# OR use individual variables:
10+
# DB_HOST=localhost
11+
# DB_PORT=5432
12+
# DB_NAME=leaseflow_db
13+
# DB_USER=username
14+
# DB_PASSWORD=password
15+
16+
# Email Configuration (for notifications)
17+
SMTP_HOST=smtp.gmail.com
18+
SMTP_PORT=587
19+
SMTP_SECURE=false
20+
SMTP_USER=your-email@gmail.com
21+
SMTP_PASS=your-app-password
22+
EMAIL_FROM=your-email@gmail.com
23+
24+
# SMS Configuration (Twilio)
25+
TWILIO_ACCOUNT_SID=your-twilio-account-sid
26+
TWILIO_AUTH_TOKEN=your-twilio-auth-token
27+
TWILIO_PHONE_NUMBER=+1234567890
28+
29+
# Owner Configuration (for auto-reclaim)
630
# Owner Account (for executing reclaim transactions)
731
OWNER_MNEMONIC=

index.js

Lines changed: 315 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
const express = require('express');
22
const cors = require('cors');
33
const AvailabilityService = require('./services/availabilityService');
4+
const AssetMetadataService = require('./services/assetMetadataService');
45
const AutoReclaimWorker = require('./services/autoReclaimWorker');
56

67
const app = express();
@@ -9,14 +10,23 @@ const port = 3000;
910
app.use(cors());
1011
app.use(express.json());
1112

13+
// Initialize services
14+
const availabilityService = new AvailabilityService();
15+
const assetMetadataService = new AssetMetadataService();
16+
1217
app.get('/', (req, res) => {
1318
res.json({
1419
project: 'LeaseFlow Protocol',
1520
status: 'Active',
16-
contract_id: 'CAEGD57WVTVQSYWYB23AISBW334QO7WNA5XQ56S45GH6BP3D2AVHKUG4'
21+
contract_id: 'CAEGD57WVTVQSYWYB23AISBW334QO7WNA5XQ56S45GH6BP3D2AVHKUG4',
22+
services: {
23+
availability: 'active',
24+
metadata: 'active'
25+
}
1726
});
1827
});
1928

29+
// Availability endpoints
2030
app.get('/api/asset/:id/availability', async (req, res) => {
2131
try {
2232
const { id } = req.params;
@@ -86,6 +96,310 @@ app.get('/api/assets/availability', async (req, res) => {
8696
}
8797
});
8898

99+
// Asset metadata endpoints
100+
app.get('/api/asset/:id/metadata', async (req, res) => {
101+
try {
102+
const { id } = req.params;
103+
const { refresh } = req.query;
104+
105+
if (!id || isNaN(id)) {
106+
return res.status(400).json({
107+
error: 'Invalid asset ID. Must be a number.',
108+
code: 'INVALID_ASSET_ID'
109+
});
110+
}
111+
112+
const metadata = await assetMetadataService.getAssetMetadata(id, refresh === 'true');
113+
114+
if (!metadata) {
115+
return res.status(404).json({
116+
error: 'Asset metadata not found',
117+
code: 'METADATA_NOT_FOUND'
118+
});
119+
}
120+
121+
res.json({
122+
success: true,
123+
data: metadata
124+
});
125+
126+
} catch (error) {
127+
console.error(`Error fetching metadata for asset ${req.params.id}:`, error);
128+
129+
res.status(500).json({
130+
error: 'Failed to fetch asset metadata',
131+
code: 'METADATA_ERROR',
132+
details: process.env.NODE_ENV === 'development' ? error.message : undefined
133+
});
134+
}
135+
});
136+
137+
app.get('/api/assets/metadata', async (req, res) => {
138+
try {
139+
const { ids, refresh } = req.query;
140+
141+
if (ids) {
142+
const assetIds = ids.split(',').map(id => id.trim()).filter(id => id && !isNaN(id));
143+
144+
if (assetIds.length === 0) {
145+
return res.status(400).json({
146+
error: 'No valid asset IDs provided',
147+
code: 'INVALID_ASSET_IDS'
148+
});
149+
}
150+
151+
const metadata = await assetMetadataService.getMultipleAssetMetadata(assetIds, refresh === 'true');
152+
153+
res.json({
154+
success: true,
155+
data: metadata
156+
});
157+
} else {
158+
const metadata = await assetMetadataService.getAllAssetMetadata();
159+
160+
res.json({
161+
success: true,
162+
data: metadata
163+
});
164+
}
165+
166+
} catch (error) {
167+
console.error('Error fetching assets metadata:', error);
168+
169+
res.status(500).json({
170+
error: 'Failed to fetch assets metadata',
171+
code: 'METADATA_ERROR',
172+
details: process.env.NODE_ENV === 'development' ? error.message : undefined
173+
});
174+
}
175+
});
176+
177+
app.post('/api/asset/:id/metadata', async (req, res) => {
178+
try {
179+
const { id } = req.params;
180+
const metadata = req.body;
181+
182+
if (!id || isNaN(id)) {
183+
return res.status(400).json({
184+
error: 'Invalid asset ID. Must be a number.',
185+
code: 'INVALID_ASSET_ID'
186+
});
187+
}
188+
189+
const result = await assetMetadataService.saveAssetMetadata({
190+
assetId: id,
191+
...metadata
192+
});
193+
194+
res.status(201).json({
195+
success: true,
196+
message: 'Asset metadata saved successfully',
197+
data: result
198+
});
199+
200+
} catch (error) {
201+
console.error(`Error saving metadata for asset ${req.params.id}:`, error);
202+
203+
res.status(500).json({
204+
error: 'Failed to save asset metadata',
205+
code: 'SAVE_ERROR',
206+
details: process.env.NODE_ENV === 'development' ? error.message : undefined
207+
});
208+
}
209+
});
210+
211+
app.put('/api/asset/:id/metadata', async (req, res) => {
212+
try {
213+
const { id } = req.params;
214+
const updates = req.body;
215+
216+
if (!id || isNaN(id)) {
217+
return res.status(400).json({
218+
error: 'Invalid asset ID. Must be a number.',
219+
code: 'INVALID_ASSET_ID'
220+
});
221+
}
222+
223+
const result = await assetMetadataService.updateAssetMetadata(id, updates);
224+
225+
res.json({
226+
success: true,
227+
message: 'Asset metadata updated successfully',
228+
data: result
229+
});
230+
231+
} catch (error) {
232+
console.error(`Error updating metadata for asset ${req.params.id}:`, error);
233+
234+
if (error.message.includes('not found')) {
235+
return res.status(404).json({
236+
error: 'Asset not found',
237+
code: 'ASSET_NOT_FOUND'
238+
});
239+
}
240+
241+
res.status(500).json({
242+
error: 'Failed to update asset metadata',
243+
code: 'UPDATE_ERROR',
244+
details: process.env.NODE_ENV === 'development' ? error.message : undefined
245+
});
246+
}
247+
});
248+
249+
app.delete('/api/asset/:id/metadata', async (req, res) => {
250+
try {
251+
const { id } = req.params;
252+
253+
if (!id || isNaN(id)) {
254+
return res.status(400).json({
255+
error: 'Invalid asset ID. Must be a number.',
256+
code: 'INVALID_ASSET_ID'
257+
});
258+
}
259+
260+
const result = await assetMetadataService.deleteAssetMetadata(id);
261+
262+
if (!result) {
263+
return res.status(404).json({
264+
error: 'Asset metadata not found',
265+
code: 'METADATA_NOT_FOUND'
266+
});
267+
}
268+
269+
res.json({
270+
success: true,
271+
message: 'Asset metadata deleted successfully',
272+
data: result
273+
});
274+
275+
} catch (error) {
276+
console.error(`Error deleting metadata for asset ${req.params.id}:`, error);
277+
278+
res.status(500).json({
279+
error: 'Failed to delete asset metadata',
280+
code: 'DELETE_ERROR',
281+
details: process.env.NODE_ENV === 'development' ? error.message : undefined
282+
});
283+
}
284+
});
285+
286+
app.get('/api/assets/search', async (req, res) => {
287+
try {
288+
const { q } = req.query;
289+
290+
if (!q || q.trim().length === 0) {
291+
return res.status(400).json({
292+
error: 'Search query is required',
293+
code: 'MISSING_QUERY'
294+
});
295+
}
296+
297+
const results = await assetMetadataService.searchAssets(q.trim());
298+
299+
res.json({
300+
success: true,
301+
data: results,
302+
query: q.trim(),
303+
count: results.length
304+
});
305+
306+
} catch (error) {
307+
console.error(`Error searching assets with query "${req.query.q}":`, error);
308+
309+
res.status(500).json({
310+
error: 'Failed to search assets',
311+
code: 'SEARCH_ERROR',
312+
details: process.env.NODE_ENV === 'development' ? error.message : undefined
313+
});
314+
}
315+
});
316+
317+
app.post('/api/asset/:id/refresh', async (req, res) => {
318+
try {
319+
const { id } = req.params;
320+
321+
if (!id || isNaN(id)) {
322+
return res.status(400).json({
323+
error: 'Invalid asset ID. Must be a number.',
324+
code: 'INVALID_ASSET_ID'
325+
});
326+
}
327+
328+
const result = await assetMetadataService.refreshAssetCache(id);
329+
330+
res.json({
331+
success: true,
332+
message: 'Asset cache refreshed successfully',
333+
data: result
334+
});
335+
336+
} catch (error) {
337+
console.error(`Error refreshing cache for asset ${req.params.id}:`, error);
338+
339+
res.status(500).json({
340+
error: 'Failed to refresh asset cache',
341+
code: 'REFRESH_ERROR',
342+
details: process.env.NODE_ENV === 'development' ? error.message : undefined
343+
});
344+
}
345+
});
346+
347+
app.get('/api/metadata/stats', async (req, res) => {
348+
try {
349+
const stats = await assetMetadataService.getCacheStatistics();
350+
351+
res.json({
352+
success: true,
353+
data: stats
354+
});
355+
356+
} catch (error) {
357+
console.error('Error fetching metadata statistics:', error);
358+
359+
res.status(500).json({
360+
error: 'Failed to fetch metadata statistics',
361+
code: 'STATS_ERROR',
362+
details: process.env.NODE_ENV === 'development' ? error.message : undefined
363+
});
364+
}
365+
});
366+
367+
app.get('/api/health', async (req, res) => {
368+
try {
369+
const health = await assetMetadataService.healthCheck();
370+
371+
res.json({
372+
success: true,
373+
data: health
374+
});
375+
376+
} catch (error) {
377+
console.error('Error performing health check:', error);
378+
379+
res.status(500).json({
380+
error: 'Failed to perform health check',
381+
code: 'HEALTH_ERROR',
382+
details: process.env.NODE_ENV === 'development' ? error.message : undefined
383+
});
384+
}
385+
});
386+
387+
if (require.main === module) {
388+
// Initialize and start services
389+
Promise.all([
390+
availabilityService.initialize(),
391+
assetMetadataService.initialize()
392+
]).then(() => {
393+
app.locals.availabilityService = availabilityService;
394+
app.locals.assetMetadataService = assetMetadataService;
395+
396+
app.listen(port, () => {
397+
console.log(`LeaseFlow Backend listening at http://localhost:${port}`);
398+
console.log('Availability Service started');
399+
console.log('Asset Metadata Service started');
400+
});
401+
}).catch(error => {
402+
console.error('Failed to initialize services:', error);
89403
if (require.main === module) {
90404
const availabilityService = new AvailabilityService();
91405

0 commit comments

Comments
 (0)