import pkg from '@jlinc/core'; const { JlincEvent, JlincAudit } = pkg; import { getPool } from "../../../db/index.js"; import { entity } from "./entity.js"; import { putQueue } from "../../../common/queue.js"; import { cacheAgreementMarkdown } from "../../../util/cacheAgreement.js"; async function getEvent(client, userId, id, includeData, meta) { const dataSql = includeData ? ` 'data', ( SELECT ed.data FROM event_data ed WHERE ed.event_id = e.id ), ` : ``; let fields = [userId]; let count = fields.length + 1; let whereAnd = `` if (id) { whereAnd += ` AND e.event_id_uuid = $${count++}`; fields.push(id); } if (meta) { let whereInVals = `` for await (const [key, value] of Object.entries(meta)) { if (whereInVals != ``) whereInVals = ` AND ` whereInVals += `(em.key = $${count++} AND em.value = $${count++})`; fields.push(key); fields.push(value); } whereAnd += ` AND e.id IN ( SELECT em.event_id FROM event_meta em WHERE em.user_id = $1 AND ${whereInVals} ) ` } const sql = ` SELECT JSON_BUILD_OBJECT( 'version', e.version, 'eventId', e.event_id_uuid, 'type', ( SELECT et.value FROM event_type et WHERE et.id = e.event_type_id ), 'senderId', e.sender_id, 'recipientId', e.recipient_id, 'created', e.created, 'agreementId', ( SELECT a.agreement_id_uuid FROM agreement a WHERE a.id = e.agreement_id AND a.user_id = $1 ), ${dataSql} 'created', e.created ) AS record FROM event e WHERE e.user_id = $1 ${whereAnd}; ` const res = await client.query(sql, fields); if (res.rows.length > 0 && res.rows[0].record) { const ret = res.rows[0].record; if (ret.data) { try { ret.data = JSON.parse(ret.data); } catch(e) { // ignore } } return ret; } return null; } async function getSignatures(client, userId, id) { const sql = ` SELECT JSON_AGG( JSON_BUILD_OBJECT( 'version', s.version, 'id', s.signer_id, 'signedOn', s.signed_on, 'type', s.type, 'jws', s.jws ) ) AS records FROM signature s INNER JOIN event e ON s.event_id = e.id WHERE e.event_id_uuid = $1 AND e.user_id = $2; ` const res = await client.query(sql, [ id, userId, ]); if (res.rows.length > 0 && res.rows[0].records) { return res.rows[0].records; } return null; } async function get(input, userId) { let response = { success: false, error: 'Unknown error', }; const client = await getPool(); try { const existingEvent = await getEvent(client, userId, input.eventId, true, input.meta) if (!existingEvent) { response.error = 'event not found' } else { const data = { event: existingEvent } if (input.includeSignatures) { data.signatures = await getSignatures(client, userId, input.eventId) } response = { message: `retrieved: ${existingEvent.eventId}`, data, } } } catch(e) { console.error(e) } finally { await client.release(); } return response; } async function save(client, userId, event, meta) { await client.query(`BEGIN`); const res = await client.query(` INSERT INTO event ( user_id, version, event_id_uuid, event_type_id, agreement_id, sender_id, recipient_id, created, created_as_ts ) VALUES ( $1, $2, $3, ( SELECT id FROM event_type WHERE value = $4 ), ( SELECT id FROM agreement WHERE agreement_id_uuid = $5 ), $6, $7, $8, $9 ) RETURNING id; `, [ userId, event.version, event.eventId, event.type, event.agreementId, event.senderId, event.recipientId, event.created, new Date(event.created).toISOString(), ]); await client.query(` INSERT INTO event_data ( user_id, event_id, data ) VALUES ( $1, $2, $3 ); `, [ userId, res.rows[0].id, event.data, ]); if (meta) { for await (const [key, value] of Object.entries(meta)) { await client.query(` INSERT INTO event_meta ( user_id, event_id, key, value ) VALUES ( $1, $2, $3, $4 ); `, [ userId, res.rows[0].id, key, value, ]); } } await client.query('COMMIT'); } async function saveSignatures(client, userId, eventId, signatures) { await client.query(`BEGIN`); for (const signature of signatures) { await client.query(` INSERT INTO signature ( user_id, version, signer_id, signed_on, type, jws, role_id, agreement_id, event_id, signed_on_as_ts ) VALUES ( $1, $2, $3, $4, $5, $6, ( SELECT id FROM role WHERE value = $7 AND user_id = $1 ), $8, ( SELECT id FROM event WHERE event_id_uuid = $9 AND user_id = $1 ), $10 ) ON CONFLICT DO NOTHING; `, [ userId, signature.version, signature.id, signature.signedOn, signature.type, signature.jws, signature.role, null, eventId, new Date(signature.signedOn).toISOString(), ]); } await client.query('COMMIT'); } async function create(input, userId, _client, _sender) { let response = { success: false, error: 'Unknown error', }; const client = _client || await getPool(); try { input.senderId = _sender ? _sender.didDoc.id : (await entity.getEntity(client, userId, input.senderShortName)).didDoc.id input.recipientId = (await entity.getEntity(client, userId, input.recipientShortName)).didDoc.id delete input.senderShortName delete input.recipientShortName const event = await JlincEvent.create(input); await save(client, userId, event, input.meta); // Pull down any referenced agreement content now (request refs are URIs, // permission refs are { reference, permitted }), so a get never has to. for (const ref of (event.references || [])) { const uri = typeof ref === "string" ? ref : ref?.reference; await cacheAgreementMarkdown({ userId, agreementUuid: event.agreementId, uri }, client); } response = { message: `created and saved: ${event.eventId}`, data: event, } } catch(e) { console.error(e) } finally { if (!_client) await client.release(); } return response; } async function process(input, userId, _client, _event, _sender, meta) { let response = { success: false, error: 'Unknown error', }; const client = _client || await getPool(); try { const existingEvent = _event || await getEvent(client, userId, input.eventId, true) if (!existingEvent) { throw new Error('event does not exist') } const inputEntity = _sender || input.shortName ? await entity.getEntity(client, userId, input.shortName) : null; const didDoc = inputEntity ? inputEntity.didDoc : input.didDoc; const signingKey = inputEntity ? inputEntity.controlPrivateKeyB64U : input.signingKey; const signingPublicKey = inputEntity ? inputEntity.didDoc.verificationMethod[0].key : input.signingPublicKey; const signingInput = { event: existingEvent, didDoc, signingKey, signingPublicKey, } const eventData = await JlincEvent.sign(signingInput); await saveSignatures(client, userId, existingEvent.eventId, eventData.signatures); const audit = await JlincAudit.create(eventData); const auditInput = { audit, didDoc, signingKey, signingPublicKey, } const auditData = await JlincAudit.sign(auditInput); response = { message: `signed and saved: ${eventData?.event?.eventId}`, data: { auditData, }, } if (input.archive) { if (meta) { response.data.auditData.meta = meta; } await putQueue( client, 'audit', `${input.archive.url}/api/v1/audit/put`, { 'Authorization': `Bearer ${input.archive.key}`, }, response.data.auditData, ) } } catch(e) { console.error(e) } finally { if (!_client) await client.release(); } return response; } async function produce(input, userId) { let response = { success: false, error: 'Unknown error', }; const client = await getPool(); try { const shortName = input.senderShortName; const sender = (await entity.getEntity(client, userId, input.senderShortName)) const created = (await create(input, userId, client, sender)).data; const processed = (await process( { eventId: created.eventId, shortName, archive: input.archive, }, userId, client, created, sender, input.meta, )).data; response = { message: `created and processed: ${created.eventId}`, data: { created, processed, }, } } catch(e) { console.error(e) } finally { await client.release(); } return response; } export const event = { getEvent, getSignatures, get, create, process, produce, }