Skip to content

Commit 7f41a1b

Browse files
committed
feat: backend version 1.0
1 parent 3fc6f37 commit 7f41a1b

5 files changed

Lines changed: 131 additions & 77 deletions

File tree

backend/src/app.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import fastifySwagger from '@fastify/swagger'
33
import fastifySwaggerUi from '@fastify/swagger-ui'
44

55
import Db from './db/Db.ts'
6+
import TaskBoardIPFS from './ipfs/TaskBoardIPFS.ts'
67
import EventListener from './listener/Listener.ts'
78
import taskRoutes from './routes/taskRoutes.ts';
89

@@ -30,14 +31,17 @@ async function buildApp() {
3031
const db = new Db()
3132
await db.init()
3233

34+
// Shared IPFS
35+
const ipfs = new TaskBoardIPFS(db)
36+
3337
// Start smart contract event listener
3438
const el = new EventListener(db)
3539
el.startListener().catch(err => {
3640
app.log.error(err)
3741
})
3842

3943
// Register routes
40-
app.register(taskRoutes, { prefix: '/api' })
44+
app.register(taskRoutes, { prefix: '/api', ipfs })
4145

4246
// Error handler (good practice)
4347
app.setErrorHandler((error, request, reply) => {

backend/src/controllers/taskController.ts

Lines changed: 47 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -14,69 +14,62 @@ import { TaskToken } from '../../../objects/TaskToken.ts'
1414

1515
type TokenBody = Static<typeof TokenMetadataSchema>
1616

17-
const IPFS = TaskBoardIPFS.getInstance()
1817

19-
export async function createTaskController(
20-
request: FastifyRequest<{ Body: TokenBody }>,
21-
reply: FastifyReply
22-
) {
23-
try {
24-
// The Task is part of the TokenMetadata
25-
const token = TaskToken.fromJSON(request.body)
26-
const cid = await IPFS.addTask(token)
27-
return reply.code(201).send({ ...token.toJSON(), createdAt: token.task.createdAt, ipfsCid: cid })
28-
} catch (error) {
29-
request.log.error(error)
30-
return reply.code(500).send({
31-
error: 'Failed to create task',
32-
message: 'Could not create task. Please try again later.'
33-
})
18+
export function createTaskController(IPFS: TaskBoardIPFS) {
19+
return async (request: FastifyRequest<{ Body: TokenBody }>, reply: FastifyReply) => {
20+
try {
21+
const token = TaskToken.fromJSON(request.body)
22+
const cid = await IPFS.addTask(token)
23+
return reply.code(201).send({ ...token.toJSON(), createdAt: token.task.createdAt, ipfsCid: cid })
24+
} catch (error) {
25+
request.log.error(error)
26+
return reply.code(500).send({
27+
error: 'Failed to create task',
28+
message: 'Could not create task. Please try again later.'
29+
})
30+
}
3431
}
35-
3632
}
3733

34+
export function getTasksController(IPFS: TaskBoardIPFS) {
35+
return async (request: FastifyRequest<{ Querystring: { limit?: number; offset?: number; difficulty?: string; name?: string } }>, reply: FastifyReply) => {
36+
try {
37+
const { limit = 10, offset = 0, difficulty, name } = request.query
38+
const filters: any = {}
39+
if (difficulty) filters.difficulty = difficulty
40+
if (name) filters.name = name
3841

39-
export async function getTasksController(
40-
request: FastifyRequest<{ Querystring: { limit?: number; offset?: number; difficulty?: string } }>,
41-
reply: FastifyReply
42-
) {
43-
try {
44-
const { limit = 10, offset = 0, difficulty } = request.query
45-
const filters: any = {}
46-
if (difficulty) filters.difficulty = difficulty
42+
const tasks = await IPFS.getTasks(limit, offset, filters)
4743

48-
const tasks = await IPFS.getTasks(limit, offset, filters)
49-
50-
if (tasks.length === 0) {
51-
return reply.code(404).send({
52-
error: 'No tasks found',
53-
message: 'No tasks match the provided criteria.'
54-
})
55-
} else {
56-
reply.code(200).send(tasks.map(t => ({ ...t.toJSON(), createdAt: t.task.createdAt, ipfsCid: t.task.ipfsCid })))
44+
if (tasks.length === 0) {
45+
return reply.code(404).send({
46+
error: 'No tasks found',
47+
message: 'No tasks match the provided criteria.'
48+
})
49+
} else {
50+
reply.code(200).send(tasks.map(t => ({ ...t.toJSON(), createdAt: t.task.createdAt, ipfsCid: t.task.ipfsCid })))
51+
}
52+
} catch (error) {
53+
reply.code(500).send({
54+
error: 'Failed to fetch tasks',
55+
message: 'Could not retrieve tasks. Please try again later.'
56+
})
5757
}
58-
} catch (error) {
59-
reply.code(500).send({
60-
error: 'Failed to fetch tasks',
61-
message: 'Could not retrieve tasks. Please try again later.'
62-
})
6358
}
6459
}
6560

66-
67-
export async function deleteTaskController(
68-
request: FastifyRequest<{ Params: { cid: string } }>,
69-
reply: FastifyReply
70-
) {
71-
try {
72-
const { cid } = request.params
73-
await IPFS.deleteTask(cid)
74-
return reply.code(204).send()
75-
} catch (error) {
76-
request.log.error(error)
77-
return reply.code(500).send({
78-
error: 'Failed to delete task',
79-
message: 'Could not delete task. Please try again later.'
80-
})
61+
export function deleteTaskController(IPFS: TaskBoardIPFS) {
62+
return async (request: FastifyRequest<{ Params: { cid: string } }>, reply: FastifyReply) => {
63+
try {
64+
const { cid } = request.params
65+
await IPFS.deleteTask(cid)
66+
return reply.code(204).send()
67+
} catch (error) {
68+
request.log.error(error)
69+
return reply.code(500).send({
70+
error: 'Failed to delete task',
71+
message: 'Could not delete task. Please try again later.'
72+
})
73+
}
8174
}
8275
}

backend/src/db/Db.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,4 +92,44 @@ export default class Db {
9292
[blockNumber]
9393
);
9494
}
95+
96+
async getTasks(
97+
limit: number,
98+
offset: number,
99+
filters?: { name?: string; difficulty?: string; createdAfter?: number; createdBefore?: number }
100+
) {
101+
const conditions: string[] = []
102+
const values: any[] = []
103+
let idx = 1
104+
105+
if (filters?.name) {
106+
conditions.push(`name ILIKE $${idx++}`)
107+
values.push(`%${filters.name}%`)
108+
}
109+
if (filters?.difficulty) {
110+
conditions.push(`difficulty = $${idx++}`)
111+
values.push(filters.difficulty)
112+
}
113+
if (filters?.createdAfter) {
114+
conditions.push(`created_at >= to_timestamp($${idx++} / 1000.0)`)
115+
values.push(filters.createdAfter)
116+
}
117+
if (filters?.createdBefore) {
118+
conditions.push(`created_at <= to_timestamp($${idx++} / 1000.0)`)
119+
values.push(filters.createdBefore)
120+
}
121+
122+
const where = conditions.length ? `WHERE ${conditions.join(' AND ')}` : ''
123+
const sql = `
124+
SELECT task_id, name, difficulty, created_at, uri
125+
FROM tasks
126+
${where}
127+
ORDER BY created_at DESC
128+
LIMIT $${idx++} OFFSET $${idx++}
129+
`
130+
values.push(limit, offset)
131+
132+
const res = await this.pool.query(sql, values)
133+
return res.rows
134+
}
95135
}

backend/src/ipfs/TaskBoardIPFS.ts

Lines changed: 30 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -8,17 +8,18 @@
88

99
import { IPFSClient, IPFSMode } from './IPFSClient.ts'
1010
import { TaskToken } from '../../../objects/TaskToken.ts'
11+
import Db from '../db/Db.ts'
1112
import dotenv from 'dotenv'
1213

1314
// Load env variables
1415
dotenv.config()
1516

1617
// Singletone pattern
1718
export default class TaskBoardIPFS {
18-
private static instance: TaskBoardIPFS
19+
private db: Db
1920
private ipfs: IPFSClient
2021

21-
private constructor () {
22+
constructor (db: Db) {
2223
this.ipfs = new IPFSClient({
2324
mode: (process.env.IPFS_STORAGE_MODE! as IPFSMode) || IPFSMode.DEVELOPMENT,
2425
localIPFSApiUrl: process.env.IPFS_LOCAL_API_URL!,
@@ -29,13 +30,17 @@ export default class TaskBoardIPFS {
2930
pinataTasksGroupId: process.env.PINATA_TASKS_GROUP_ID!,
3031
pinataImagesGroupId: process.env.PINATA_IMAGES_GROUP_ID!
3132
})
33+
this.db = db
3234
}
3335

34-
static getInstance(): TaskBoardIPFS {
35-
if (!TaskBoardIPFS.instance) {
36-
TaskBoardIPFS.instance = new TaskBoardIPFS()
36+
/// Transform URI to CID
37+
private _extractCid(uri: string): string | undefined {
38+
if (!uri) return undefined
39+
if (uri.startsWith('ipfs://')) {
40+
return uri.replace('ipfs://', '').split('/')[0]
3741
}
38-
return TaskBoardIPFS.instance
42+
if (/^[A-Za-z0-9]+$/.test(uri)) return uri
43+
return undefined
3944
}
4045

4146
/**
@@ -58,18 +63,27 @@ export default class TaskBoardIPFS {
5863
* @param filters optional filters for task metadata
5964
* @returns array of ERC-1155 token metadata with tasks data
6065
*/
61-
public async getTasks(limit: number, offset: number, filters?: Partial<TaskToken>): Promise<TaskToken[]> {
62-
let tasks: TaskToken[] = []
66+
public async getTasks(
67+
limit: number,
68+
offset: number,
69+
filters?: { name?: string; difficulty?: string; createdAfter?: number; createdBefore?: number }
70+
): Promise<TaskToken[]> {
71+
const rows = await this.db.getTasks(limit, offset, filters)
6372

64-
// TODO: fetch taskIDs from DB based on filters, limit and offset
65-
/*
66-
for (const cid in cids) {
67-
const jsonTask = await this.ipfs.downloadJson(cid)
68-
const task = Task.fromJSON(jsonTask)
69-
task.ipfsCid = cid
70-
tasks.push(task)
73+
const tasks: TaskToken[] = []
74+
for (const row of rows) {
75+
const cid = this._extractCid(row.uri)
76+
if (!cid) continue
77+
try {
78+
const jsonTask = await this.ipfs.downloadJson(cid)
79+
const token = TaskToken.fromJSON(jsonTask)
80+
token.task.ipfsCid = cid
81+
token.task.createdAt = new Date(row.created_at).getTime()
82+
tasks.push(token)
83+
} catch (err) {
84+
console.error(`Failed to load task ${row.task_id} from IPFS:`, err)
85+
}
7186
}
72-
*/
7387
return tasks
7488
}
7589

backend/src/routes/taskRoutes.ts

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,24 +9,27 @@
99
import { type FastifyPluginAsync } from 'fastify';
1010
import { TokenMetadataSchema, TokenMetadataResponseSchema } from '../schemas/taskSchema.ts'
1111
import { createTaskController, deleteTaskController, getTasksController } from '../controllers/taskController.ts'
12+
import TaskBoardIPFS from '../ipfs/TaskBoardIPFS.ts'
13+
14+
const taskRoutes: FastifyPluginAsync<{ ipfs: TaskBoardIPFS }> = async (app, opts) => {
15+
const { ipfs } = opts
1216

13-
const taskRoutes: FastifyPluginAsync = async (app) => {
14-
1517
app.get('/tasks', {
1618
schema: {
1719
querystring: {
1820
type: 'object',
1921
properties: {
2022
limit: { type: 'integer', minimum: 1, maximum: 50 },
2123
offset: { type: 'integer', minimum: 0 },
22-
difficulty: { type: 'string' }
24+
difficulty: { type: 'string' },
25+
name: { type: 'string' }
2326
}
2427
},
2528
response: {
2629
200: { type: 'array', items: TokenMetadataSchema }
2730
}
2831
},
29-
handler: getTasksController
32+
handler: getTasksController(ipfs)
3033
})
3134

3235
app.post('/tasks', {
@@ -36,7 +39,7 @@ const taskRoutes: FastifyPluginAsync = async (app) => {
3639
201: TokenMetadataResponseSchema
3740
}
3841
},
39-
handler: createTaskController
42+
handler: createTaskController(ipfs)
4043
})
4144

4245
app.delete('/tasks/:cid', {
@@ -47,7 +50,7 @@ const taskRoutes: FastifyPluginAsync = async (app) => {
4750
properties: { cid: { type: 'string' } }
4851
}
4952
},
50-
handler: deleteTaskController
53+
handler: deleteTaskController(ipfs)
5154
})
5255
}
5356

0 commit comments

Comments
 (0)