Skip to content

Commit

Permalink
POST match2/{id}/proposal + GET match2/{id}/proposal+ `GET match2…
Browse files Browse the repository at this point in the history
…/{id}/proposal/{id}` (#34)

* create get local match2

* v bump

* missing test

* create -> propose

* typos

* WIP

* wip

* tests passing

* no token id 400 tests

* payload improvements

* transaction routes

* clean up

* v bump

* return columns

* remove unused query

* check demand status

* reduce identity service calls

* add status checks on chain creation

* review comments

* identity service format
  • Loading branch information
jonmattgray authored Mar 29, 2023
1 parent 9572306 commit f59c3ea
Show file tree
Hide file tree
Showing 17 changed files with 498 additions and 129 deletions.
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@digicatapult/dscp-matchmaker-api",
"version": "0.3.4",
"version": "0.4.0",
"description": "An OpenAPI Matchmaking API service for DSCP",
"main": "src/index.ts",
"scripts": {
Expand Down
34 changes: 18 additions & 16 deletions src/controllers/capacity/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ export class CapacityController extends Controller {
throw new BadRequest('Attachment id not found')
}

const selfAddress = await getMemberBySelf()
const { address: selfAddress, alias: selfAlias } = await getMemberBySelf()

const [capacity] = await this.db.insertDemand({
owner: selfAddress,
Expand All @@ -61,10 +61,9 @@ export class CapacityController extends Controller {
parameters_attachment_id: parametersAttachmentId,
})

const { alias: ownerAlias } = await getMemberByAddress(capacity.owner)
return {
id: capacity.id,
owner: ownerAlias,
owner: selfAlias,
state: capacity.state,
parametersAttachmentId,
}
Expand Down Expand Up @@ -105,6 +104,7 @@ export class CapacityController extends Controller {
public async createCapacityOnChain(@Path() capacityId: UUID): Promise<TransactionResponse> {
const [capacity] = await this.db.getDemandWithAttachment(capacityId, DemandSubtype.capacity)
if (!capacity) throw new NotFound('capacity')
if (capacity.state !== DemandState.created) throw new BadRequest(`Demand must have state: ${DemandState.created}`)

const [transaction] = await this.db.insertTransaction({
token_type: TokenType.DEMAND,
Expand All @@ -113,24 +113,23 @@ export class CapacityController extends Controller {
})

// temp - until there is a blockchain watcher, need to await runProcess to know token IDs
const [tokenId] = await runProcess(demandCreate(capacity, transaction.id))
await observeTokenId(this.db, TokenType.DEMAND, transaction.id, tokenId, true)
return {
id: transaction.id,
submittedAt: new Date(transaction.created_at),
state: transaction.state,
}
const [tokenId] = await runProcess(demandCreate(capacity))
await this.db.updateTransaction(transaction.id, { state: TransactionState.finalised })

// demand-create returns a single token ID
await observeTokenId(TokenType.DEMAND, capacityId, tokenId, true)
return transaction
}

/**
* @summary Get a capacity by ID
* @summary Get a capacity creation transaction by ID
* @param capacityId The capacity's identifier
* @param creationId The capacity's creation ID
*/
@Response<NotFound>(404, 'Items not found.')
@Response<NotFound>(404, 'Item not found.')
@SuccessResponse('200')
@Get('{capacityId}/creation/{creationId}')
public async getCreationID(@Path() capacityId: UUID, creationId: UUID): Promise<TransactionResponse> {
public async getCapacityCreation(@Path() capacityId: UUID, creationId: UUID): Promise<TransactionResponse> {
const [capacity] = await this.db.getDemand(capacityId)
if (!capacity) throw new NotFound('capacity')

Expand All @@ -139,15 +138,18 @@ export class CapacityController extends Controller {
return creation
}

@Response<NotFound>(404, 'Items not found.')
/**
* @summary Get all of a capacity's creation transactions
* @param capacityId The capacity's identifier
*/
@Response<NotFound>(404, 'Item not found.')
@SuccessResponse('200')
@Get('{capacityId}/creation/')
public async getTransactionsFromCapacity(@Path() capacityId: UUID): Promise<TransactionResponse[]> {
const [capacity] = await this.db.getDemand(capacityId)
if (!capacity) throw new NotFound('capacity')

const creations = await this.db.getTransactionsFromCapacityID(capacityId)
return creations
return await this.db.getTransactionsByLocalId(capacityId)
}
}

Expand Down
117 changes: 101 additions & 16 deletions src/controllers/match2/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,12 @@ import { BadRequest, NotFound } from '../../lib/error-handler/index'
import { getMemberByAddress, getMemberBySelf } from '../../lib/services/identity'
import { Match2Request, Match2Response, Match2State } from '../../models/match2'
import { UUID } from '../../models/uuid'
import { DemandSubtype } from '../../models/demand'
import { TransactionResponse, TransactionState } from '../../models/transaction'
import { TokenType } from '../../models/tokenType'
import { observeTokenId } from '../../lib/services/blockchainWatcher'
import { runProcess } from '../../lib/services/dscpApi'
import { match2Propose } from '../../lib/payload'
import { DemandPayload, DemandState, DemandSubtype } from '../../models/demand'

@Route('match2')
@Tags('match2')
Expand All @@ -46,24 +51,12 @@ export class Match2Controller extends Controller {
@Body() { demandA: demandAId, demandB: demandBId }: Match2Request
): Promise<Match2Response> {
const [demandA] = await this.db.getDemand(demandAId)
if (!demandA) {
throw new BadRequest('Demand A not found')
}

if (demandA.subtype !== DemandSubtype.order) {
throw new BadRequest(`DemandA must be ${DemandSubtype.order}`)
}
validatePreLocal(demandA, DemandSubtype.order, 'DemandA')

const [demandB] = await this.db.getDemand(demandBId)
if (!demandB) {
throw new BadRequest('Demand B not found')
}

if (demandB.subtype !== DemandSubtype.capacity) {
throw new BadRequest(`DemandB must be ${DemandSubtype.capacity}`)
}
validatePreLocal(demandB, DemandSubtype.capacity, 'DemandB')

const selfAddress = await getMemberBySelf()
const { address: selfAddress } = await getMemberBySelf()

const [match2] = await this.db.insertMatch2({
optimiser: selfAddress,
Expand Down Expand Up @@ -101,6 +94,76 @@ export class Match2Controller extends Controller {

return responseWithAliases(match2)
}

/**
* An optimiser creates the match2 {match2Id} on-chain. The match2 is now viewable to other members.
* @summary Create a new match2 on-chain
* @param match2Id The match2's identifier
*/
@Post('{match2Id}/proposal')
@Response<NotFound>(404, 'Item not found')
@SuccessResponse('201')
public async createMatch2OnChain(@Path() match2Id: UUID): Promise<TransactionResponse> {
const [match2] = await this.db.getMatch2(match2Id)
if (!match2) throw new NotFound('match2')
if (match2.state !== Match2State.proposed) throw new BadRequest(`Match2 must have state: ${Match2State.proposed}`)

const [demandA] = await this.db.getDemand(match2.demandA)
validatePreOnChain(demandA, DemandSubtype.order, 'DemandA')

const [demandB] = await this.db.getDemand(match2.demandB)
validatePreOnChain(demandB, DemandSubtype.capacity, 'DemandB')

const [transaction] = await this.db.insertTransaction({
token_type: TokenType.MATCH2,
local_id: match2Id,
state: TransactionState.submitted,
})

// temp - until there is a blockchain watcher, need to await runProcess to know token IDs
const tokenIds = await runProcess(match2Propose(match2, demandA, demandB))
await this.db.updateTransaction(transaction.id, { state: TransactionState.finalised })

// match2-propose returns 3 token IDs
await observeTokenId(TokenType.DEMAND, match2.demandA, tokenIds[0], false) // order
await observeTokenId(TokenType.DEMAND, match2.demandB, tokenIds[1], false) // capacity
await observeTokenId(TokenType.MATCH2, match2.id, tokenIds[2], true) // match2

return transaction
}

/**
* @summary Get a match2 proposal transaction by ID
* @param match2Id The match2's identifier
* @param proposalId The match2's proposal ID
*/
@Response<NotFound>(404, 'Item not found.')
@SuccessResponse('200')
@Get('{match2Id}/proposal/{proposalId}')
public async getMatch2Proposal(@Path() match2Id: UUID, proposalId: UUID): Promise<TransactionResponse> {
const [match2] = await this.db.getMatch2(match2Id)
if (!match2) throw new NotFound('match2')

const [proposal] = await this.db.getTransaction(proposalId)
if (!proposal) throw new NotFound('proposal')

return proposal
}

/**
* @summary Get all of a match2's proposal transactions
* @param match2Id The match2's identifier
*/
@Response<NotFound>(404, 'Item not found.')
@SuccessResponse('200')
@Get('{match2Id}/proposal')
public async getMatch2Proposals(@Path() match2Id: UUID): Promise<TransactionResponse[]> {
const [match2] = await this.db.getMatch2(match2Id)
if (!match2) throw new NotFound('match2')

const proposals = await this.db.getTransactionsByLocalId(match2Id)
return proposals
}
}

const responseWithAliases = async (match2: Match2Response): Promise<Match2Response> => {
Expand All @@ -120,3 +183,25 @@ const responseWithAliases = async (match2: Match2Response): Promise<Match2Respon
demandB: match2.demandB,
}
}

const validatePreLocal = (demand: DemandPayload, subtype: DemandSubtype, key: string) => {
if (!demand) {
throw new BadRequest(`${key} not found`)
}

if (demand.subtype !== subtype) {
throw new BadRequest(`${key} must be ${subtype}`)
}

if (demand.state === DemandState.allocated) {
throw new BadRequest(`${key} is already ${DemandState.allocated}`)
}
}

const validatePreOnChain = (demand: DemandPayload, subtype: DemandSubtype, key: string) => {
validatePreLocal(demand, subtype, key)

if (!demand.latestTokenId) {
throw new BadRequest(`${key} must be on chain`)
}
}
2 changes: 1 addition & 1 deletion src/controllers/order/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ export class order extends Controller {
const [attachment] = await this.db.getAttachment(parametersAttachmentId)
if (!attachment) throw new NotFound('attachment')

const selfAddress = await getMemberBySelf()
const { address: selfAddress } = await getMemberBySelf()
const [order] = await this.db.insertDemand({
owner: selfAddress,
subtype: DemandSubtype.order,
Expand Down
17 changes: 13 additions & 4 deletions src/lib/db/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,15 @@ const match2Columns = [
'original_token_id AS originalTokenId',
]

const transactionColumns = [
'id',
'state',
'local_id AS localId',
'token_type AS tokenType',
'created_at AS submittedAt',
'updated_at AS updatedAt',
]

export default class Database {
public client: Knex
private log: Logger
Expand Down Expand Up @@ -78,15 +87,15 @@ export default class Database {
}

insertTransaction = async (transaction: object) => {
return this.db().transaction().insert(transaction).returning('*')
return this.db().transaction().insert(transaction).returning(transactionColumns)
}

getTransaction = async (id: UUID) => {
return this.db().transaction().select('*').where({ id: id })
return this.db().transaction().select(transactionColumns).where({ id })
}

getTransactionsFromCapacityID = async (capacityID: UUID) => {
return this.db().transaction().where({ local_id: capacityID })
getTransactionsByLocalId = async (local_id: UUID) => {
return this.db().transaction().select(transactionColumns).where({ local_id })
}

updateTransaction = async (transactionId: UUID, transaction: object) => {
Expand Down
43 changes: 40 additions & 3 deletions src/lib/payload.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { Match2Response, Match2State } from '../models/match2'
import { DemandPayload, DemandState } from '../models/demand'
import { TokenType } from '../models/tokenType'
import { UUID } from '../models/uuid'

export const demandCreate = (demand: DemandPayload, transactionId: UUID) => ({
export const demandCreate = (demand: DemandPayload) => ({
files: [{ blob: new Blob([demand.binary_blob]), filename: demand.filename }],
process: { id: 'demand-create', version: 1 },
inputs: [],
Expand All @@ -15,7 +15,44 @@ export const demandCreate = (demand: DemandPayload, transactionId: UUID) => ({
state: { type: 'LITERAL', value: DemandState.created },
subtype: { type: 'LITERAL', value: demand.subtype },
parameters: { type: 'FILE', value: demand.filename },
transactionId: { type: 'LITERAL', value: transactionId.replace(/[-]/g, '') },
},
},
],
})

export const match2Propose = (match2: Match2Response, demandA: DemandPayload, demandB: DemandPayload) => ({
files: [],
process: { id: 'match2-propose', version: 1 },
inputs: [demandA.latestTokenId, demandB.latestTokenId],
outputs: [
{
roles: { Owner: demandA.owner },
metadata: {
version: { type: 'LITERAL', value: '1' },
type: { type: 'LITERAL', value: TokenType.DEMAND },
state: { type: 'LITERAL', value: DemandState.created },
subtype: { type: 'LITERAL', value: demandA.subtype },
originalId: { type: 'TOKEN_ID', value: demandA.originalTokenId },
},
},
{
roles: { Owner: demandB.owner },
metadata: {
version: { type: 'LITERAL', value: '1' },
type: { type: 'LITERAL', value: TokenType.DEMAND },
state: { type: 'LITERAL', value: DemandState.created },
subtype: { type: 'LITERAL', value: demandB.subtype },
originalId: { type: 'TOKEN_ID', value: demandB.originalTokenId },
},
},
{
roles: { Optimiser: match2.optimiser, MemberA: match2.memberA, MemberB: match2.memberB },
metadata: {
version: { type: 'LITERAL', value: '1' },
type: { type: 'LITERAL', value: TokenType.MATCH2 },
state: { type: 'LITERAL', value: Match2State.proposed },
demandA: { type: 'TOKEN_ID', value: demandA.originalTokenId },
demandB: { type: 'TOKEN_ID', value: demandB.originalTokenId },
},
},
],
Expand Down
16 changes: 3 additions & 13 deletions src/lib/services/blockchainWatcher.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,16 @@
// a temporary placeholder for the blockchain watcher

import { UUID } from '../../models/uuid'
import { TransactionState } from '../../models/transaction'
import { TokenType } from '../../models/tokenType'
import Database from '../db'

const db = new Database()

const typeTableMap = {
[TokenType.DEMAND]: 'demand',
[TokenType.MATCH2]: 'match2',
}

export const observeTokenId = async (
db: Database,
tokenType: TokenType,
transactionId: UUID,
tokenId: number,
isNewEntity: boolean
) => {
const [{ localId }] = await db.updateTransaction(transactionId, {
state: TransactionState.finalised,
token_id: tokenId,
})

export const observeTokenId = async (tokenType: TokenType, localId: UUID, tokenId: number, isNewEntity: boolean) => {
await db.updateLocalWithTokenId(typeTableMap[tokenType], localId, tokenId, isNewEntity)
}
2 changes: 1 addition & 1 deletion src/lib/services/dscpApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,5 +25,5 @@ export const runProcess = async ({ files, ...payload }: { files: RunProcessFile[
return result
}

throw new HttpResponse({ code: 500, message: result }) // pass through dscpApi error
throw new HttpResponse({ code: 500, message: JSON.stringify(result) }) // pass through dscpApi error
}
Loading

0 comments on commit f59c3ea

Please sign in to comment.