import { createPatch, applyPatch } from 'rfc6902'
import OIPRecord from '../oip-record'
import { decodeArtifact as decodeRecord } from '../../../decoders'
export default class EditRecord extends OIPRecord {
constructor (editRecordJSON, originalRecord, patchedRecord) {
super()
this.oipRecordType = 'artifact'
// Define the edit information
this.edit = {
txid: undefined,
timestamp: undefined,
patch: undefined
}
this.meta = {
applied: undefined
}
this.signature = undefined
if (originalRecord) { this.setOriginalRecord(originalRecord) }
if (patchedRecord) { this.setPatchedRecord(patchedRecord) }
if (editRecordJSON) { this.fromJSON(editRecordJSON) }
}
/**
* Signs the PatchedRecord and EditRecord
* @param {Function} signMessage - A function (provided by a wallet) that allows a message to be signed with the approapriate private address
* @return {Object} Returns `{success: true, signature}` if signing was successful
*/
async signSelf (signMessage) {
// If we have a patched record, attempt to sign it
if (this.getPatchedRecord()) {
try {
await this.getPatchedRecord().signSelf(signMessage)
} catch (e) {
return { success: false, error: `Unable to sign Patched Record: ${e}` }
}
// Make sure the Record is valid
let { success, error } = this.getPatchedRecord().isValid()
if (!success) {
return { success: false, error: `Patched Record is not valid: ${error}` }
}
// Now that we know the patched record is valid, create the latest patch version
if (this.getOriginalRecord()) { this.createPatch() }
}
let signature
try {
signature = await OIPRecord.prototype.signSelf.call(this, signMessage)
} catch (e) {
return { success: false, error: `Unable to create EditRecord signature: ${e}` }
}
return { success: true, signature }
}
setPatchedRecord (patchedRecord) {
this.patchedRecord = patchedRecord
if (this.originalRecord) {
// If the Patched Record has the same Timestamp as the Original Record, update it.
if (this.getOriginalRecord().getTimestamp() === this.getPatchedRecord().getTimestamp()) { this.getPatchedRecord().setTimestamp(Date.now()) }
if (this.getOriginalRecord().getSignature() === this.getPatchedRecord().getSignature()) { this.getPatchedRecord().setSignature('') }
this.createPatch()
} else { this.setOriginalRecordTXID(patchedRecord.getOriginalTXID()) }
}
setOriginalRecord (originalRecord) {
this.originalRecord = originalRecord
this.setOriginalRecordTXID(originalRecord.getOriginalTXID())
}
setOriginalRecordTXID (originalTXID) {
this.edit.txid = originalTXID
}
setTimestamp (timestamp) {
this.edit.timestamp = timestamp
}
setPatch (rfc6902Patch) {
this.edit.patch = rfc6902Patch
if (this.originalRecord && !this.patchedRecord) {
let patchedRecord = this.createPatchedRecord(this.originalRecord, this.edit.patch)
this.setPatchedRecord(patchedRecord)
}
}
setTXID (txid) {
this.meta.txid = txid
}
getPatchedRecord () {
return this.patchedRecord
}
getOriginalRecord () {
return this.originalRecord
}
getOriginalRecordTXID () {
return this.edit.txid
}
getTimestamp () {
return this.edit.timestamp
}
getPatch () {
return this.edit.patch
}
getTXID () {
return this.meta.txid
}
/**
* Apply an RFC6902 patch to an OIP Record
* @param {OIPRecord} originalRecord - The Original Record
* @param {RFC6902PatchJSON} rfc6902Patch - The RFC6902 Patch JSON
* @return {OIPRecord} Returns an OIP Record with the Edit Patch applied
*/
createPatchedRecord (originalRecord, rfc6902Patch) {
let clonedJSON = originalRecord.toJSON()
let patchOperations = applyPatch(clonedJSON.artifact, rfc6902Patch)
for (let op of patchOperations) {
if (op !== null) { throw new Error('Patch Application had an Error! ' + JSON.stringify(patchOperations, null, 4)) }
}
let patchedRecord = decodeRecord(clonedJSON)
this.setPatchedRecord(patchedRecord)
return patchedRecord
}
/**
* Create an RFC6902 JSON Patch
* @param {Object} originalJSON
* @param {Object} modifiedJSON
* @return {RFC6902PatchJSON} Returns the RFC6902 Patch JSON
*/
createRFC6902Patch (originalJSON, modifiedJSON) {
this.rfc6902Patch = createPatch(originalJSON, modifiedJSON)
return this.rfc6902Patch
}
/**
* Create the RFC6902 Patch based on the Original Record and the Patched Record
*/
createPatch () {
// Verify that we have the Records we need
if (!this.originalRecord || !this.patchedRecord) { throw new Error('Cannot create Patch without an Original Record and the Patched Record!') }
// Create the rfc6902 JSON patch from the Record JSON
let originalJSON = this.originalRecord.toJSON().artifact
let patchedJSON = this.patchedRecord.toJSON().artifact
this.rfc6902Patch = this.createRFC6902Patch(originalJSON, patchedJSON)
// Set the RFC6902 Patch
this.setPatch(this.rfc6902Patch)
}
createPreimage () {
this.preimage = `${this.getOriginalRecordTXID()}-${this.getTimestamp()}`
return this.preimage
}
/**
* Get the JSON version of the edit
* @return {Object}
*/
toJSON () {
let cloneJSON = JSON.parse(JSON.stringify({
edit: this.edit,
meta: this.meta
}))
return cloneJSON
}
/**
* Load an EditRecord from JSON
* @param {Object} editRecord - The Edit Record JSON
*/
fromJSON (editRecord) {
if (editRecord.edit) {
if (editRecord.edit.txid) { this.setOriginalRecordTXID(editRecord.edit.txid) }
if (editRecord.edit.timestamp) { this.setTimestamp(editRecord.edit.timestamp) }
if (editRecord.edit.patch) { this.setPatch(editRecord.edit.patch) }
}
if (editRecord.signature) {
this.signature = editRecord.signature
}
if (editRecord.meta) {
this.meta = editRecord.meta
}
}
serialize () {
let serializedJSON = {}
serializedJSON[this.oipRecordType] = this.edit
serializedJSON.signature = this.getSignature()
return `json:${JSON.stringify({ oip042: { edit: serializedJSON } })}`
}
/**
* Get the Class Name.
* @return {string} Returns "EditRecord"
*/
getClassName () {
return 'EditRecord'
}
isValid () {
if (!this.edit.txid || this.edit.txid === '') {
return { success: false, error: 'Original Record TXID is a Required Field!' }
}
if (!this.edit.timestamp || this.edit.timestamp === '') {
return { success: false, error: 'Having a timestamp is Required!' }
}
if (!this.edit.patch || this.edit.patch === '') {
return { success: false, error: 'Having an Edit Patch is Required!' }
}
// Check if we only have [{ op: 'replace', path: '/timestamp', value: 123 }]
let onlyReplace = true
let onlyTimestampAndSignature = true
for (let operation of this.edit.patch) {
if (operation.op !== 'replace') { onlyReplace = false }
if (operation.op === 'replace') {
if (operation.path !== '/timestamp' && operation.path !== '/signature') { onlyTimestampAndSignature = false }
}
}
if (JSON.stringify(this.edit.patch) === '{}' || (onlyReplace && onlyTimestampAndSignature)) {
return { success: false, error: 'Empty Patch! You must modify the Record in order to edit it!' }
}
if (!this.signature || this.signature === '') {
return { success: false, error: 'Having a Signature is Required!' }
}
return { success: true }
}
}