diff --git a/src/controllers/v1/attachment/index.ts b/src/controllers/v1/attachment/index.ts index e945f2f6..60343837 100644 --- a/src/controllers/v1/attachment/index.ts +++ b/src/controllers/v1/attachment/index.ts @@ -20,12 +20,11 @@ import { Readable } from 'node:stream' import { logger } from '../../../lib/logger' import Database, { AttachmentRow } from '../../../lib/db' -import type { Attachment } from '../../../models' +import type * as Attachment from '../../../models' import { BadRequest, NotFound } from '../../../lib/error-handler' import type { UUID, DATE } from '../../../models/strings' import Ipfs from '../../../lib/ipfs' import env from '../../../env' -import { parseDateParam } from '../../../lib/utils/queryParams' const parseAccept = (acceptHeader: string) => acceptHeader @@ -87,14 +86,14 @@ export class attachment extends Controller { @Get('/') @SuccessResponse(200, 'returns all attachment') - public async get(@Query() updated_since?: DATE): Promise { + public async getAll(@Query() createdAt?: DATE): Promise { this.log.debug('retrieving all attachment') + const where: Record = {} + if (createdAt) where.created_at = createdAt - const attachments: AttachmentRow[] = await this.db.get( - 'attachment', - updated_since ? { created_at: parseDateParam(updated_since) } : {} - ) - return attachments.map(({ ipfs_hash, ...rest }): Attachment => ({ ...rest, createdAt: rest.created_at })) + const attachments = await this.db.get('attachment', where) + + return { message: 'ok', attachments } } @Post('/') @@ -103,7 +102,7 @@ export class attachment extends Controller { public async create( @Request() req: express.Request, @UploadedFile() file?: Express.Multer.File - ): Promise { + ): Promise { this.log.debug(`creating an attachment filename: ${file?.originalname || 'json'}`) if (!req.body && !file) throw new BadRequest('nothing to upload') @@ -112,18 +111,13 @@ export class attachment extends Controller { const fileBlob = new Blob([Buffer.from(file?.buffer || JSON.stringify(req.body))]) const ipfsHash = await this.ipfs.addFile({ blob: fileBlob, filename }) - const [{ id, created_at }] = await this.db.insert('attachment', { + const result: AttachmentRow | string = await this.db.insert('attachment', { filename, ipfs_hash: ipfsHash, size: fileBlob.size, }) - return { - id, - filename, - size: fileBlob.size, - createdAt: created_at, - } + return result } @Get('/{id}') @@ -131,7 +125,7 @@ export class attachment extends Controller { @Produces('application/json') @Produces('application/octet-stream') @SuccessResponse(200) - public async getById(@Request() req: express.Request, @Path() id: UUID): Promise { + public async getById(@Request() req: express.Request, @Path() id: UUID): Promise { this.log.debug(`attempting to retrieve ${id} attachment`) const [attachment] = await this.db.get('attachment', { id }) if (!attachment) throw new NotFound('attachment') diff --git a/src/controllers/v1/certificate/index.ts b/src/controllers/v1/certificate/index.ts new file mode 100644 index 00000000..51afc7e8 --- /dev/null +++ b/src/controllers/v1/certificate/index.ts @@ -0,0 +1,115 @@ +import { + ValidateError, + Controller, + Post, + Get, + Route, + Response, + Body, + SuccessResponse, + Example, + Tags, + Security, + Path, + Query, +} from 'tsoa' +import type { Logger } from 'pino' +import { injectable } from 'tsyringe' + +import { logger } from '../../../lib/logger' +import Database from '../../../lib/db' +import { BadRequest, NotFound } from '../../../lib/error-handler/index' +import Identity from '../../../lib/services/identity' +import * as Certificate from '../../../models/certificate' +import { DATE, UUID } from '../../../models/strings' +// import { TransactionResponse, TransactionType } from '../../../models/transaction' +import ChainNode from '../../../lib/chainNode' +import env from '../../../env' +import { camelToSnake } from '../../../lib/utils/shared' + +@Route('v1/certificate') +@injectable() +@Tags('certificate') +@Security('BearerAuth') +export class CertificateController extends Controller { + log: Logger + db: Database + node: ChainNode + + constructor(private identity: Identity) { + super() + this.log = logger.child({ controller: '/certificate' }) + this.db = new Database() + this.node = new ChainNode({ + host: env.NODE_HOST, + port: env.NODE_PORT, + logger, + userUri: env.USER_URI, + }) + this.identity = identity + } + + /** + * @summary insert a certificate for initialisation + */ + @Example({ + id: '52907745-7672-470e-a803-a2f8feb52944', + capacity: 10, + co2e: 1000, + }) + @Post() + @Response(400, 'Request was invalid') + @Response(422, 'Validation Failed') + @SuccessResponse('201') + public async post(@Body() body: Certificate.Request): Promise { + this.log.info({ identity: this.identity, body }) + + const formatted = Object.keys(body).reduce( + (out, key) => ({ + [camelToSnake(key)]: body[key], + ...out, + }), + {} + ) + + return { + message: 'ok', + result: await this.db.insert('certificate', formatted), + } + } + + /** + * + * @summary Lists all local certificates + * TODO - combine where so with all possible options e.g. + * columns so it's not a default and will be rendered in swagger + */ + + @Get('/') + public async getAll(@Query() createdAt?: DATE): Promise { + const where: Record = {} + if (createdAt) where.created_at = createdAt + + return { + message: 'ok', + certificates: await this.db.get('certificate', where), + } + } + + /** + * @summary returns certificate by id + * @example id "52907745-7672-470e-a803-a2f8feb52944" + */ + @Response(422, 'Validation Failed') + @Response(404, ' not found') + @Response(400, 'ID must be supplied in UUID format') + @Get('{id}') + public async getById(@Path() id: UUID): Promise { + if (!id) throw new BadRequest() + + return { + message: 'ok', + certificate: await this.db.get('certificate', { id }), + } + } +} diff --git a/src/controllers/v1/example/index.ts b/src/controllers/v1/example/index.ts deleted file mode 100644 index 9b2065bd..00000000 --- a/src/controllers/v1/example/index.ts +++ /dev/null @@ -1,86 +0,0 @@ -import { - ValidateError, - Controller, - Post, - Get, - Route, - Response, - Body, - SuccessResponse, - Tags, - Security, - Path, - Query, -} from 'tsoa' -import type { Logger } from 'pino' -import { injectable } from 'tsyringe' - -import { logger } from '../../../lib/logger' -import Database from '../../../lib/db' -import { BadRequest, NotFound } from '../../../lib/error-handler/index' -import Identity from '../../../lib/services/identity' -import * as example from '../../../models/example' -import { DATE, UUID } from '../../../models/strings' -// import { TransactionResponse, TransactionType } from '../../../models/transaction' -import ChainNode from '../../../lib/chainNode' -import env from '../../../env' - -@Route('v1/example') -@injectable() -@Tags('example') -@Security('BearerAuth') -export class Example extends Controller { - log: Logger - db: Database - node: ChainNode - - constructor(private identity: Identity) { - super() - this.log = logger.child({ controller: '/example' }) - this.db = new Database() - this.node = new ChainNode({ - host: env.NODE_HOST, - port: env.NODE_PORT, - logger, - userUri: env.USER_URI, - }) - this.identity = identity - } - - /** - * description - * @summary - */ - @Post() - @Response(400, 'Request was invalid') - @Response(422, 'Validation Failed') - @SuccessResponse('201') - public async proposeMatch2(@Body() { example = '' }: example.Request): Promise { - this.log.info({ identity: this.identity, example }) - - return { message: 'ok' } - } - - /** - * description - * @summary Lists - */ - @Get('/') - public async getAll(@Query() createdAt?: DATE): Promise { - if (createdAt) return { message: 'by createdAt' } - return [{ message: 'all' }] - } - - /** - * @summary - * @param id The example's identifier - */ - @Response(422, 'Validation Failed') - @Response(404, 'Item not found') - @Get('{id}') - public async getMatch2(@Path() id: UUID): Promise { - if (!id) throw new BadRequest() - - return { message: 'ok', id } - } -} diff --git a/src/lib/db/index.ts b/src/lib/db/index.ts index 0cdb5f91..c7bc9c37 100644 --- a/src/lib/db/index.ts +++ b/src/lib/db/index.ts @@ -1,11 +1,11 @@ import knex, { Knex } from 'knex' import { pgConfig } from './knexfile' -import { HEX, UUID } from '../../models/strings' +import { DATE, HEX, UUID } from '../../models/strings' import { TransactionApiType, TransactionState, TransactionType } from '../../models/transaction' import { NotFound } from '../error-handler' -const tablesList = ['attachment', 'example', 'transaction', 'processed_blocks'] as const +const tablesList = ['attachment', 'certificate', 'transaction', 'processed_blocks'] as const type TABLES_TUPLE = typeof tablesList type TABLE = TABLES_TUPLE[number] @@ -21,7 +21,7 @@ export interface AttachmentRow { filename: string | null size: number | null ipfs_hash: string - created_at: Date + created_at: DATE } export interface TransactionRow { @@ -34,12 +34,12 @@ export interface TransactionRow { updated_at: Date } -export interface ExampleRow { +export interface CertificateRow { id: UUID createdAt: Date } -export type Entities = ExampleRow | TransactionRow | AttachmentRow +export type Entities = CertificateRow | TransactionRow | AttachmentRow function restore0x(input: ProcessedBlockTrimmed): ProcessedBlock { return { @@ -73,13 +73,18 @@ export default class Database { } // generics methods - insert = async (model: keyof Models<() => QueryBuilder>, record: Record) => { + insert = async ( + model: keyof Models<() => QueryBuilder>, + record: Record + ): Promise => { const query = this.db()[model] + + // TODO address indexer (create a backlog item) if (model == 'processed_blocks') { return query().insert(trim0x(record as ProcessedBlock)) } - return query().insert(record).returning('*') + return query().insert(record) } delete = async (model: keyof Models<() => QueryBuilder>, where: Record) => { @@ -115,7 +120,9 @@ export default class Database { // TODO some methods could be generic as well, e.g. insert/get for event processor indexer findLocalIdForToken = async (tokenId: number): Promise => { - const result = (await Promise.all([this.db().example().select(['id']).where({ latest_token_id: tokenId })])) as { + const result = (await Promise.all([ + this.db().certificate().select(['id']).where({ latest_token_id: tokenId }), + ])) as { id: UUID }[][] const flatten = result.reduce((acc, set) => [...acc, ...set], []) diff --git a/src/lib/db/migrations/20230310111029_initial.ts b/src/lib/db/migrations/20230310111029_initial.ts index c64486c8..253f14a6 100644 --- a/src/lib/db/migrations/20230310111029_initial.ts +++ b/src/lib/db/migrations/20230310111029_initial.ts @@ -18,18 +18,21 @@ export async function up(knex: Knex): Promise { def.primary(['id']) }) - await knex.schema.createTable('example', (def) => { + await knex.schema.createTable('certificate', (def) => { def.uuid('id').defaultTo(knex.raw('uuid_generate_v4()')) + def.integer('capacity').notNullable().index('capacity_index') + def.integer('co2e').notNullable().index('co2e_index') def.string('owner', 48).notNullable() def - .enum('state', ['created', 'allocated'], { - enumName: 'example_state', + .enum('state', ['initialized', 'issued', 'revoked'], { + enumName: 'certificate_state', useNative: true, }) .notNullable() - def.uuid('parameters_attachment_id').notNullable() - def.integer('latest_token_id') - def.integer('original_token_id') + .defaultTo('initialized') + def.uuid('parameters_attachment_id').nullable().defaultTo(null) + def.integer('latest_token_id').defaultTo(null) + def.integer('original_token_id').defaultTo(null) def.datetime('created_at').notNullable().defaultTo(now()) def.datetime('updated_at').notNullable().defaultTo(now()) def.primary(['id']) @@ -62,20 +65,23 @@ export async function up(knex: Knex): Promise { useNative: true, }) .notNullable() + .defaultTo('submitted') def.datetime('created_at').notNullable().defaultTo(now()) def.datetime('updated_at').notNullable().defaultTo(now()) def.integer('token_id') def.primary(['id']) def.specificType('hash', 'CHAR(64)').notNullable() - def.enu('api_type', ['example_a', 'example_b'], { useNative: true, enumName: 'api_type' }) - def.enu('transaction_type', ['creation', 'proposal', 'accept'], { useNative: true, enumName: 'transaction_type' }) + def.enu('api_type', ['certificate', 'attachment'], { useNative: true, enumName: 'api_type' }).notNullable() + def + .enu('transaction_type', ['initialise', 'issue', 'revoke'], { useNative: true, enumName: 'transaction_type' }) + .defaultTo('initialise') def.unique(['id', 'local_id'], { indexName: 'transaction-id-local-id' }) }) } export async function down(knex: Knex): Promise { await knex.schema.dropTable('attachment') - await knex.schema.dropTable('example') + await knex.schema.dropTable('certificate') await knex.schema.dropTable('transaction') await knex.schema.dropTable('processed_blocks') await knex.raw('DROP EXTENSION "uuid-ossp"') diff --git a/src/lib/indexer/__tests__/changeSet.test.ts b/src/lib/indexer/__tests__/changeSet.test.ts index 16262fae..3d667d47 100644 --- a/src/lib/indexer/__tests__/changeSet.test.ts +++ b/src/lib/indexer/__tests__/changeSet.test.ts @@ -70,7 +70,7 @@ describe('changeSet', function () { expect(result).to.equal(null) }) - it('should return id if token appears in examples', function () { + it('should return id if token appears in certificates', function () { const result = findLocalIdInChangeSet(findIdTestSet, 42) expect(result).to.equal('0x02') }) diff --git a/src/lib/indexer/__tests__/eventProcessor.test.ts b/src/lib/indexer/__tests__/eventProcessor.test.ts index f765665f..713b8e35 100644 --- a/src/lib/indexer/__tests__/eventProcessor.test.ts +++ b/src/lib/indexer/__tests__/eventProcessor.test.ts @@ -5,19 +5,19 @@ import { expect } from 'chai' import { TransactionRow } from '../../db' describe('eventProcessor', function () { - describe('example-create', function () { + describe('process_initiate_cert', function () { it('should error with version != 1', function () { let error: Error | null = null try { - eventProcessors['example-create'](0, null, 'alice', [], []) + eventProcessors['process_initiate_cert'](0, null, 'alice', [], []) } catch (err) { error = err instanceof Error ? err : null } expect(error).instanceOf(Error) }) - it('should return update to example if transaction exists', function () { - const result = eventProcessors['example-create']( + it('should return update to certificate if transaction exists', function () { + const result = eventProcessors['process_initiate_cert']( 1, { localId: '42' } as TransactionRow, 'alice', @@ -26,14 +26,14 @@ describe('eventProcessor', function () { ) expect(result).to.deep.equal({ - examples: new Map([ + certificates: new Map([ ['42', { type: 'update', id: '42', state: 'created', latest_token_id: 1, original_token_id: 1 }], ]), }) }) - it("should return new attachment and example if transaction doesn't exist", function () { - const result = eventProcessors['example-create']( + it("should return new certificate if transaction doesn't exist", function () { + const result = eventProcessors['process_initiate_cert']( 1, null, 'alice', @@ -41,21 +41,26 @@ describe('eventProcessor', function () { [ { id: 1, - roles: new Map([['owner', '123']]), + roles: new Map([['owner', 'emma-hydrogen-producer']]), metadata: new Map([ ['parameters', 'a'], - ['subtype', 'example_a'], + ['co2e', '10'], + ['capacity', '1'], ]), }, ] ) - expect(result.attachments?.size).to.equal(1) - const [[attachmentId, attachment]] = [...(result.attachments || [])] - expect(attachment).to.deep.equal({ + const [[_, certificate]] = [...(result.certificates || [])] + expect(certificate).to.deep.contain({ type: 'insert', - id: attachmentId, - ipfs_hash: 'a', + owner: 'emma-hydrogen-producer', + state: 'initialized', + co2e: '10', + capacity: '1', + latest_token_id: 1, + original_token_id: 1, + parameters_attachment_id: '', }) }) }) diff --git a/src/lib/indexer/__tests__/fixtures/changeSet.ts b/src/lib/indexer/__tests__/fixtures/changeSet.ts index 5f895036..76e8543b 100644 --- a/src/lib/indexer/__tests__/fixtures/changeSet.ts +++ b/src/lib/indexer/__tests__/fixtures/changeSet.ts @@ -1,13 +1,13 @@ -import { AttachmentRecord, ChangeSet, ExampleRecord } from '../../changeSet' +import { AttachmentRecord, CertificateRecord, ChangeSet } from '../../changeSet' export const changeSets2: ChangeSet[] = [ { - examples: new Map([['1', { id: '1', type: 'update', latest_token_id: 1, state: 'created' }]]), + certificates: new Map([['1', { id: '1', type: 'update', latest_token_id: 1, state: 'initialized' }]]), }, { - examples: new Map([ - ['1', { id: '1', type: 'update', latest_token_id: 1, state: 'created' }], - ['3', { id: '3', type: 'update', latest_token_id: 3, state: 'created' }], + certificates: new Map([ + ['1', { id: '1', type: 'update', latest_token_id: 1, state: 'initialized' }], + ['3', { id: '3', type: 'update', latest_token_id: 3, state: 'initialized' }], ]), }, ] @@ -23,13 +23,13 @@ export const findIdTestSet: ChangeSet = { }, ], ]), - examples: new Map([ + certificates: new Map([ [ '0x02', { type: 'update', id: '0x02', - state: 'created', + state: 'initialized', latest_token_id: 42, }, ], diff --git a/src/lib/indexer/__tests__/fixtures/event.ts b/src/lib/indexer/__tests__/fixtures/event.ts index 52746787..71df41fd 100644 --- a/src/lib/indexer/__tests__/fixtures/event.ts +++ b/src/lib/indexer/__tests__/fixtures/event.ts @@ -6,7 +6,7 @@ export const complexEvent: ProcessRanEvent = { inputs: [1, 2, 3], outputs: [4, 5, 6], process: { - id: 'example-create', + id: 'process_initiate_cert', version: 1, }, sender: 'alice', @@ -18,7 +18,7 @@ export const noInputsOutputs: ProcessRanEvent = { inputs: [], outputs: [], process: { - id: 'example-create', + id: 'process_initiate_cert', version: 1, }, sender: 'alice', diff --git a/src/lib/indexer/__tests__/fixtures/eventProcessor.ts b/src/lib/indexer/__tests__/fixtures/eventProcessor.ts index dcefc065..d48325fd 100644 --- a/src/lib/indexer/__tests__/fixtures/eventProcessor.ts +++ b/src/lib/indexer/__tests__/fixtures/eventProcessor.ts @@ -3,5 +3,5 @@ import { EventProcessors } from '../../eventProcessor' import sinon from 'sinon' export const withMockEventProcessors: (result?: ChangeSet) => EventProcessors = (result: ChangeSet = {}) => ({ - 'example-create': sinon.stub().returns(result), + process_initiate_cert: sinon.stub().returns(result), }) diff --git a/src/lib/indexer/__tests__/handleEvent.test.ts b/src/lib/indexer/__tests__/handleEvent.test.ts index fff4cdfb..71937664 100644 --- a/src/lib/indexer/__tests__/handleEvent.test.ts +++ b/src/lib/indexer/__tests__/handleEvent.test.ts @@ -37,7 +37,7 @@ describe('EventHandler', function () { const result = await eventHandler.handleEvent(noInputsOutputs, {}) expect(result).to.deep.equal({}) - const stub = eventProcessors['example-create'] as SinonStub + const stub = eventProcessors['process_initiate_cert'] as SinonStub expect(stub.calledOnce).to.equal(true) expect(stub.firstCall.args).to.deep.equal([1, tx, 'alice', [], []]) }) @@ -59,7 +59,7 @@ describe('EventHandler', function () { const result = await eventHandler.handleEvent(complexEvent, {}) expect(result).to.deep.equal({}) - const stub = eventProcessors['example-create'] as SinonStub + const stub = eventProcessors['process_initiate_cert'] as SinonStub expect(stub.calledOnce).to.equal(true) expect(stub.firstCall.args).to.deep.equal([ 1, @@ -93,7 +93,7 @@ describe('EventHandler', function () { const eventHandler = new EventHandler({ db, logger, node, eventProcessors }) const baseChangeSet: ChangeSet = { - examples: new Map([ + certificates: new Map([ ['7', { type: 'update', id: '7', latest_token_id: 1, state: 'created' }], ['8', { type: 'update', id: '8', latest_token_id: 2, state: 'created' }], ['9', { type: 'update', id: '9', latest_token_id: 3, state: 'created' }], @@ -103,7 +103,7 @@ describe('EventHandler', function () { const result = await eventHandler.handleEvent(complexEvent, baseChangeSet) expect(result).to.deep.equal(baseChangeSet) - const stub = eventProcessors['example-create'] as SinonStub + const stub = eventProcessors['process_initiate_cert'] as SinonStub expect(stub.calledOnce).to.equal(true) expect(stub.firstCall.args).to.deep.equal([ 1, @@ -133,7 +133,7 @@ describe('EventHandler', function () { const eventHandler = new EventHandler({ db, logger, node, eventProcessors }) const baseChangeSet: ChangeSet = { - examples: new Map([ + certificates: new Map([ ['7', { type: 'update', id: '7', latest_token_id: 1, state: 'created' }], ['8', { type: 'update', id: '8', latest_token_id: 2, state: 'created' }], ['9', { type: 'update', id: '9', latest_token_id: 3, state: 'created' }], diff --git a/src/lib/indexer/__tests__/index.test.ts b/src/lib/indexer/__tests__/index.test.ts index d233db70..d4baf983 100644 --- a/src/lib/indexer/__tests__/index.test.ts +++ b/src/lib/indexer/__tests__/index.test.ts @@ -157,11 +157,11 @@ describe('Indexer', function () { expect(handleBlock.secondCall.args[0]).to.equal('3-hash') }) - it('should upsert examples and example entries from changeset', async function () { + it('should upsert certificates and certificate entries from changeset', async function () { const db = withInitialLastProcessedBlock({ hash: '1-hash', parent: '0-hash', height: 1 }) const node = withHappyChainNode() const handleBlock = sinon.stub().resolves({ - examples: new Map([ + certificates: new Map([ ['123', { type: 'update', id: '42' }], ['456', { type: 'update', id: '43' }], ]), @@ -176,19 +176,19 @@ describe('Indexer', function () { await indexer.processNextBlock('2-hash') expect((db.update as sinon.SinonStub).calledTwice).to.equal(true) - expect((db.update as sinon.SinonStub).firstCall.args).to.deep.equal(['example', { id: '42' }, {}]) - expect((db.update as sinon.SinonStub).secondCall.args).to.deep.equal(['example', { id: '43' }, {}]) + expect((db.update as sinon.SinonStub).firstCall.args).to.deep.equal(['certificate', { id: '42' }, {}]) + expect((db.update as sinon.SinonStub).secondCall.args).to.deep.equal(['certificate', { id: '43' }, {}]) expect((db.insert as sinon.SinonStub).calledTwice).to.equal(true) expect((db.insert as sinon.SinonStub).firstCall.args).to.deep.equal(['attachment', { id: '46' }]) expect((db.insert as sinon.SinonStub).secondCall.args).to.deep.equal(['attachment', { id: '47' }]) }) - it('should insert examples and example entries from changeset', async function () { + it('should insert certificates and certificate entries from changeset', async function () { const db = withInitialLastProcessedBlock({ hash: '1-hash', parent: '0-hash', height: 1 }) const node = withHappyChainNode() const handleBlock = sinon.stub().resolves({ - examples: new Map([ + certificates: new Map([ ['123', { type: 'insert', id: '42' }], ['456', { type: 'insert', id: '43' }], ]), @@ -205,8 +205,8 @@ describe('Indexer', function () { expect((db.insert as sinon.SinonStub).getCalls().length).to.equal(4) expect((db.insert as sinon.SinonStub).getCall(0).args).to.deep.equal(['attachment', { id: '46' }]) expect((db.insert as sinon.SinonStub).getCall(1).args).to.deep.equal(['attachment', { id: '47' }]) - expect((db.insert as sinon.SinonStub).getCall(2).args).to.deep.equal(['example', {}]) - expect((db.insert as sinon.SinonStub).getCall(3).args).to.deep.equal(['example', {}]) + expect((db.insert as sinon.SinonStub).getCall(2).args).to.deep.equal(['certificate', {}]) + expect((db.insert as sinon.SinonStub).getCall(3).args).to.deep.equal(['certificate', {}]) }) describe('exception cases', function () { diff --git a/src/lib/indexer/changeSet.ts b/src/lib/indexer/changeSet.ts index 66500aaa..09486004 100644 --- a/src/lib/indexer/changeSet.ts +++ b/src/lib/indexer/changeSet.ts @@ -6,13 +6,14 @@ interface Change { type: ChangeOperation } -export type ExampleRecord = +export type CertificateRecord = | { type: 'insert' id: string owner: string - subtype: string state: string + co2e: string + capacity: string parameters_attachment_id: string latest_token_id: number original_token_id: number @@ -35,7 +36,7 @@ export type AttachmentRecord = { export type ChangeSet = { attachments?: Map - examples?: Map + certificates?: Map } const mergeMaps = (base?: Map, update?: Map) => { @@ -58,19 +59,19 @@ const mergeMaps = (base?: Map, update?: Map { - const examples = mergeMaps(base.examples, update.examples) + const certificates = mergeMaps(base.certificates, update.certificates) const attachments = mergeMaps(base.attachments, update.attachments) const result: ChangeSet = { ...(attachments ? { attachments } : {}), - ...(examples ? { examples } : {}), + ...(certificates ? { certificates } : {}), } return result } export const findLocalIdInChangeSet = (change: ChangeSet, tokenId: number): UUID | null => { - const matchRecordValues = [...(change.examples?.values() || [])] + const matchRecordValues = [...(change.certificates?.values() || [])] // idea to have multiple here const match = [...matchRecordValues].find((el) => el.latest_token_id === tokenId) diff --git a/src/lib/indexer/eventProcessor.ts b/src/lib/indexer/eventProcessor.ts index 76f35c2d..276f8193 100644 --- a/src/lib/indexer/eventProcessor.ts +++ b/src/lib/indexer/eventProcessor.ts @@ -2,9 +2,9 @@ import { v4 as UUIDv4 } from 'uuid' import { UUID } from '../../models/strings' import { TransactionRow } from '../db' -import { AttachmentRecord, ChangeSet, ExampleRecord } from './changeSet' +import { ChangeSet, CertificateRecord } from './changeSet' -const processNames = ['example-create'] as const +const processNames = ['process_initiate_cert'] as const type PROCESSES_TUPLE = typeof processNames type PROCESSES = PROCESSES_TUPLE[number] @@ -30,46 +30,54 @@ const getOrError = (map: Map, key: string): T => { return val } +/* TODO uncomment if we decided to use attachments const attachmentPayload = (map: Map, key: string): AttachmentRecord => ({ type: 'insert', id: UUIDv4(), ipfs_hash: getOrError(map, key), }) +*/ const DefaultEventProcessors: EventProcessors = { - 'example-create': (version, transaction, _sender, _inputs, outputs) => { - if (version !== 1) throw new Error(`Incompatible version ${version} for example-create process`) + process_initiate_cert: (version, transaction, _sender, _inputs, outputs) => { + if (version !== 1) throw new Error(`Incompatible version ${version} for process_initiate_cert process`) - const newExampleId = outputs[0].id - const newExample = outputs[0] + const newCertificateId = outputs[0].id + const newCertificate = outputs[0] if (transaction) { const id = transaction.localId return { - examples: new Map([ + certificates: new Map([ [ id, - { type: 'update', id, state: 'created', latest_token_id: newExampleId, original_token_id: newExampleId }, + { + type: 'update', + id, + state: 'created', + latest_token_id: newCertificateId, + original_token_id: newCertificateId, + }, ], ]), } } - const attachment: AttachmentRecord = attachmentPayload(newExample.metadata, 'parameters') - const example: ExampleRecord = { + // const attachment: AttachmentRecord = attachmentPayload(newCertificate.metadata, 'parameters') + const certificate: CertificateRecord = { type: 'insert', id: UUIDv4(), - owner: getOrError(newExample.roles, 'owner'), - subtype: getOrError(newExample.metadata, 'subtype'), - state: 'created', - parameters_attachment_id: attachment.id, - latest_token_id: newExample.id, - original_token_id: newExample.id, + owner: getOrError(newCertificate.roles, 'owner'), + state: 'initialized', + co2e: getOrError(newCertificate.metadata, 'co2e'), + capacity: getOrError(newCertificate.metadata, 'capacity'), + latest_token_id: newCertificate.id, + original_token_id: newCertificate.id, + parameters_attachment_id: '', } return { - attachments: new Map([[attachment.id, attachment]]), - examples: new Map([[example.id, example]]), + certificates: new Map([[certificate.id, certificate]]), } }, } diff --git a/src/lib/indexer/index.ts b/src/lib/indexer/index.ts index 512f5d57..385b5e68 100644 --- a/src/lib/indexer/index.ts +++ b/src/lib/indexer/index.ts @@ -198,15 +198,16 @@ export default class Indexer { } } - if (changeSet.examples) { - for (const [, examples] of changeSet.examples) { - const { type, id, ...record } = examples + // TODO we can do no if and just pass entity as arg? + if (changeSet.certificates) { + for (const [, certificates] of changeSet.certificates) { + const { type, id, ...record } = certificates switch (type) { case 'insert': - await db.insert('example', record) + await db.insert('certificate', record) break case 'update': - await db.update('example', { id: id }, record) + await db.update('certificate', { id: id }, record) break } } diff --git a/src/lib/payload.ts b/src/lib/payload.ts index f9b2722d..345c9ce0 100644 --- a/src/lib/payload.ts +++ b/src/lib/payload.ts @@ -18,18 +18,35 @@ export interface MetadataFile { export type Metadata = Record -export const processExample = (example: Record): Payload => ({ - process: { id: 'processExample', version: 1 }, +export const processInitiateCert = (certificate: Record): Payload => ({ + process: { id: 'process_initiate_cert', version: 1 }, inputs: [], outputs: [ { - roles: { Owner: example.owner }, + roles: { Owner: certificate.owner }, metadata: { version: { type: 'LITERAL', value: '1' }, type: { type: 'LITERAL', value: 'EXAMPLE' }, state: { type: 'LITERAL', value: 'created' }, - subtype: { type: 'LITERAL', value: example.subtype }, - parameters: { type: 'FILE', value: bs58ToHex(example.ipfs_hash) }, + subtype: { type: 'LITERAL', value: certificate.subtype }, + parameters: { type: 'FILE', value: bs58ToHex(certificate.ipfs_hash) }, + }, + }, + ], +}) + +export const processIssueCert = (certificate: Record): Payload => ({ + process: { id: 'process_issue_cert', version: 1 }, + inputs: [], + outputs: [ + { + roles: { Owner: certificate.owner }, + metadata: { + version: { type: 'LITERAL', value: '1' }, + type: { type: 'LITERAL', value: 'EXAMPLE' }, + state: { type: 'LITERAL', value: 'created' }, + subtype: { type: 'LITERAL', value: certificate.subtype }, + parameters: { type: 'FILE', value: bs58ToHex(certificate.ipfs_hash) }, }, }, ], diff --git a/src/lib/services/blockchainWatcher.ts b/src/lib/services/blockchainWatcher.ts index d695ac4b..e7c88c90 100644 --- a/src/lib/services/blockchainWatcher.ts +++ b/src/lib/services/blockchainWatcher.ts @@ -11,8 +11,8 @@ // switch (tokenType) { // case 'DEMAND': // return 'demand' -// case 'example': -// return 'example' +// case 'certificate': +// return 'certificate' // } // } diff --git a/src/lib/utils/shared.ts b/src/lib/utils/shared.ts new file mode 100644 index 00000000..9ffb59e9 --- /dev/null +++ b/src/lib/utils/shared.ts @@ -0,0 +1,10 @@ +export function camelToSnake(str: string): string { + return str + .replace(/([a-z])([A-Z])/g, '$1_$2') + .trim() + .toLowerCase() +} + +export function snakeToCamel(str: string): string { + return str.replace(/[^a-zA-Z0-9]+(.)/g, (_, char) => char.toUpperCase()) +} diff --git a/src/models/attachment.ts b/src/models/attachment.ts index 49a3649f..a3525740 100644 --- a/src/models/attachment.ts +++ b/src/models/attachment.ts @@ -1,5 +1,11 @@ import { UUID } from './strings' +import { AttachmentRow } from '../lib/db' +import { Readable } from 'node:stream' + +export type Request = Record +export type Response = Record | string | Readable + /** * File or JSON attachment * @example [{ @@ -9,7 +15,7 @@ import { UUID } from './strings' * "createdAt": "2023-03-16T13:18:42.357Z" * }] */ -export interface Attachment { +export interface Payload { /** * uuid generated using knex */ diff --git a/src/models/certificate.ts b/src/models/certificate.ts new file mode 100644 index 00000000..0545a327 --- /dev/null +++ b/src/models/certificate.ts @@ -0,0 +1,23 @@ +import { CertificateRow } from 'src/lib/db' +import { UUID } from './strings' + +export type Request = Record +export type Response = Record +/** + * Certificate Request Body example + * @example { + * "id": "52907745-7672-470e-a803-a2f8feb52944", + * "co2e": 20, + * "capacity": 1, + * } + */ +export interface Payload { + co2e?: number + capacity?: number + /** + * uuid generated using knex + * this is normally what it would be returned + */ + id?: UUID + commits?: Record +} diff --git a/src/models/example.ts b/src/models/example.ts deleted file mode 100644 index 085054e7..00000000 --- a/src/models/example.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { UUID } from './strings' - -export type Request = { [k: string]: string } -export type Response = { [k: string]: string } - -/** - * File or JSON attachment - * @example [{ - * "id": "string", - * "createdAt": "2023-03-16T13:18:42.357Z" - * }] - */ -export interface Example { - /** - * uuid generated using knex - */ - id: UUID - /** - * for json files name will be 'json' - */ - filename: string | 'json' | null - createdAt: Date -} diff --git a/src/models/index.d.ts b/src/models/index.d.ts index cb80a10a..6346faac 100644 --- a/src/models/index.d.ts +++ b/src/models/index.d.ts @@ -1,3 +1,3 @@ export type * from './health' export type * from './attachment' -export type * from './example' +export type * from './certificate' diff --git a/src/models/strings.ts b/src/models/strings.ts index a6c1b531..9057ecbc 100644 --- a/src/models/strings.ts +++ b/src/models/strings.ts @@ -1,7 +1,7 @@ /** * Stringified UUIDv4. * @pattern [0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-4[0-9A-Fa-f]{3}-[89ABab][0-9A-Fa-f]{3}-[0-9A-Fa-f]{12} - * @format uuid + * @example "52907745-7672-470e-a803-a2f8feb52944" */ export type UUID = string @@ -9,6 +9,7 @@ export type UUID = string * Hex string with 0x prefix * @pattern 0x[0-9a-zA-Z]+ * @format hex + * @example "0xFF" */ export type HEX = `0x${string}` @@ -16,6 +17,6 @@ export type HEX = `0x${string}` * ISO 8601 date string * @pattern (\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d\.\d+([+-][0-2]\d:[0-5]\d|Z))|(\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d([+-][0-2]\d:[0-5]\d|Z))|(\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d([+-][0-2]\d:[0-5]\d|Z)) * @format date - * @example 2023-05-04T09:47:32.393Z + * @example "2023-05-04T09:47:32.393Z" */ export type DATE = string diff --git a/src/models/transaction.ts b/src/models/transaction.ts index 9a304066..a776649c 100644 --- a/src/models/transaction.ts +++ b/src/models/transaction.ts @@ -20,7 +20,7 @@ export interface TransactionResponse { /** * The type of the entity involved in the transaction */ -export type TransactionApiType = 'example' | 'example_a' | 'example_b' +export type TransactionApiType = 'certificate' | 'example_a' | 'example_b' /** * Transaction type - matches the endpoint that initiates the transaction