Skip to content

add: Round api #51

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 11 commits into
base: add/tasking-service
Choose a base branch
from
2 changes: 2 additions & 0 deletions lib/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import Fastify from 'fastify'
import cors from '@fastify/cors'
import pg from '@fastify/postgres'
import { subnetRoutes } from './subnet-routes.js'
import { roundRoutes } from './round-routes.js'

/**
* @param {object} args
Expand Down Expand Up @@ -30,6 +31,7 @@ export const createApp = ({ databaseUrl, dbPoolConfig, logger }) => {
return 'OK'
})

app.register(roundRoutes)
app.register(subnetRoutes)
return app
}
156 changes: 156 additions & 0 deletions lib/round-routes.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
/** @import { RequestWithRoundId } from './typings.js' */
/** @import { FastifyInstance, FastifyReply } from 'fastify' */

const roundResponse = {
200: {
type: 'object',
properties: {
id: {
type: 'number',
format: 'bigint'
},
start_time: {
type: 'string',
format: 'datetime'
},
end_time: {
type: 'string',
format: 'datetime'
},
active: {
type: 'boolean'
},
tasks: {
type: 'array',
items: {
type: 'object',
properties: {
subnet: {
type: 'string',
pattern: '^(walrus|arweave)$'
},
task_definition: {
type: 'object',
additionalProperties: true
}
}
}
}
}
}
}

/**
* Define the round routes
* @param {FastifyInstance} app
*/
export const roundRoutes = (app) => {
app.get(
'/rounds/current',
{
schema: {
response: roundResponse
}
},
/**
* @param {RequestWithRoundId} request
* @param {FastifyReply} reply
*/
async (request, reply) => {
const client = await app.pg.connect()
try {
const { rows } = await client.query(
`SELECT
cr.*,
CASE
WHEN COUNT(cst.*) > 0 THEN json_agg(
json_build_object(
'subnet', cst.subnet,
'task_definition', cst.task_definition
)
)
ELSE '[]'::json
END AS tasks
FROM
checker_rounds cr
LEFT JOIN
checker_subnet_tasks cst ON cr.id = cst.round_id
WHERE
cr.active = true
GROUP BY
cr.id
ORDER BY
cr.id DESC
LIMIT 1`
)

if (!rows || rows.length === 0) {
reply.status(404)
return
}

reply.send(rows[0])
} finally {
client.release()
}
})

app.get(
'/rounds/:roundId',
{
schema: {
params: {
type: 'object',
additionalProperties: false,
properties: {
roundId: {
type: 'number'
}
},
required: ['roundId']
},
response: roundResponse
}
},
/**
* @param {RequestWithRoundId} request
* @param {FastifyReply} reply
*/
async (request, reply) => {
const client = await app.pg.connect()
try {
const roundId = Number(request.params.roundId)
const { rows } = await client.query(
`SELECT
cr.*,
CASE
WHEN COUNT(cst.*) > 0 THEN json_agg(
json_build_object(
'subnet', cst.subnet,
'task_definition', cst.task_definition
)
)
ELSE '[]'::json
END AS tasks
FROM
checker_rounds cr
LEFT JOIN
checker_subnet_tasks cst ON cr.id = cst.round_id
WHERE
cr.id = $1
GROUP BY
cr.id`,
[roundId]
)

if (!rows || rows.length === 0) {
reply.status(404)
return
}

reply.send(rows[0])
} finally {
client.release()
}
})
}
4 changes: 4 additions & 0 deletions lib/typings.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ export type RequestWithSubnet<TBody = {}, UQuery = {}> = FastifyRequest<{
Querystring: UQuery;
}>;

export type RequestWithRoundId = FastifyRequest<{
Params: { roundId: string };
}>;

export interface Logger {
info: typeof console.info;
error: typeof console.error;
Expand Down
99 changes: 97 additions & 2 deletions test/round-and-tasking-service.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@ import assert from 'assert'
import { after, before, beforeEach, describe, it } from 'node:test'
import { createPgPool } from '../lib/pool.js'
import { migrateWithPgClient } from '../lib/migrate.js'
import { DATABASE_URL } from '../lib/config.js'
import { DATABASE_URL, poolConfig } from '../lib/config.js'
import { RoundService } from '../lib/round-service.js'
import { TaskingService } from '../lib/tasking-service.js'
import { withRound } from './test-helpers.js'
import { createApp } from '../lib/app.js'
import { withRound, withSubnetTasks } from './test-helpers.js'

const DEFAULT_CONFIG = {
roundDurationMs: 1000,
Expand All @@ -15,13 +16,31 @@ const DEFAULT_CONFIG = {
describe('round and tasking service', () => {
/** @type {import('pg').Pool} */
let pgPool
/** @type {import('fastify').FastifyInstance} */
let app
/** @type {string} */
let baseUrl

before(async () => {
pgPool = await createPgPool(DATABASE_URL)
await migrateWithPgClient(pgPool)

app = createApp({
databaseUrl: DATABASE_URL,
dbPoolConfig: poolConfig,
logger: {
level:
process.env.DEBUG === '*' || process.env.DEBUG?.includes('test')
? 'debug'
: 'error'
}
})

baseUrl = await app.listen()
})

after(async () => {
await app.close()
await pgPool.end()
})

Expand Down Expand Up @@ -174,4 +193,80 @@ describe('round and tasking service', () => {
})
})
})

describe('round API routes', () => {
describe('GET /rounds/current', () => {
it('should return the current active round with tasks', async () => {
const round = await withRound({
pgPool,
roundDurationMs: 1000, // 1 second duration
active: true
})
await withSubnetTasks(pgPool, round.id, 'walrus', { key: 'value' })

/** @type {any} */
const response = await fetch(`${baseUrl}/rounds/current`)
assert.strictEqual(response.status, 200)
const responseBody = await response.json()
assert.equal(responseBody.id, round.id)
assert.strictEqual(responseBody.active, true)
assert.strictEqual(responseBody.tasks.length, 1)
assert.deepStrictEqual(responseBody.tasks, [{
subnet: 'walrus',
task_definition: { key: 'value' }
}])
})

it('should return 404 if no current active', async () => {
/** @type {any} */
const response = await fetch(`${baseUrl}/rounds/current`)
assert.strictEqual(response.status, 404)

// insert inactive round
await withRound({
pgPool,
roundDurationMs: 1000, // 1 second duration
active: false
})

/** @type {any} */
const secondResponse = await fetch(`${baseUrl}/rounds/current`)
assert.strictEqual(secondResponse.status, 404)
})
})

describe('GET /rounds/:roundId', () => {
it('should return the round with the specified ID and its tasks', async () => {
const round = await withRound({
pgPool,
roundDurationMs: 1000, // 1 second duration
active: false
})
await withSubnetTasks(pgPool, round.id, 'arweave', { key: 'value' })

/** @type {any} */
const response = await fetch(`${baseUrl}/rounds/${round.id}`)
assert.strictEqual(response.status, 200)
const responseBody = await response.json()
assert.equal(responseBody.id, round.id)
assert.strictEqual(responseBody.active, false)
assert.strictEqual(responseBody.tasks.length, 1)
assert.deepStrictEqual(responseBody.tasks, [{
subnet: 'arweave',
task_definition: { key: 'value' }
}])
})

it('should return 400 if roundId is not a number', async () => {
const response = await fetch(`${baseUrl}/rounds/invalid`)
assert.strictEqual(response.status, 400)
})

it('should return 404 if round is not found', async () => {
/** @type {any} */
const response = await fetch(`${baseUrl}/rounds/999999`)
assert.strictEqual(response.status, 404)
})
})
})
})
13 changes: 13 additions & 0 deletions test/test-helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -67,3 +67,16 @@ export const withRound = async ({ pgPool, roundDurationMs, active = false }) =>

return rows[0]
}

/**
*
* @param {import('../lib/typings.js').PgPool} pgPool
* @param {string} roundId
* @param {string} subnet
* @param {object} task
*/
export const withSubnetTasks = async (pgPool, roundId, subnet, task) => {
await pgPool.query(`
INSERT INTO checker_subnet_tasks (round_id, subnet, task_definition)
VALUES ($1, $2, $3)`, [roundId, subnet, task])
}