import { DDO } from '../ddo/DDO' import DID from '../ocean/DID' import { EditableMetadata } from '../ddo/interfaces/EditableMetadata' import { Logger, isDdo } from '../utils' import { WebServiceConnector } from '../ocean/utils/WebServiceConnector' import { Response } from 'node-fetch' import { Metadata, ValidateMetadata } from '../ddo/interfaces' const apiPath = '/api/v1/aquarius/assets/ddo' export interface QueryResult { results: DDO[] page: number totalPages: number totalResults: number } export interface SearchQuery { offset?: number page?: number query: { match?: { [property: string]: | string | number | boolean | Record } // eslint-disable-next-line camelcase query_string?: { [property: string]: string | number | string[] | number[] | boolean } // eslint-disable-next-line camelcase simple_query_string?: { [property: string]: string | number | string[] | number[] | boolean } } sort?: { [jsonPath: string]: number } } /** * Provides an interface with Metadata Cache. * Metadata Cache provides an off-chain database cache for on-chain metadata about data assets. */ export class MetadataCache { public fetch: WebServiceConnector private logger: Logger private metadataCacheUri: string private get url() { return this.metadataCacheUri } /** * Instantiate Metadata Cache (independently of Ocean) for off-chain interaction. * @param {String} metadataCacheUri * @param {Logger} logger */ constructor(metadataCacheUri: string, logger: Logger) { this.fetch = new WebServiceConnector(logger) this.logger = logger this.metadataCacheUri = metadataCacheUri } public async getVersionInfo(): Promise { return (await this.fetch.get(this.url)).json() } public async getAccessUrl(accessToken: any, payload: any): Promise { const accessUrl: string = await this.fetch .post(`${accessToken.service_endpoint}/${accessToken.resource_id}`, payload) .then((response: Response) => { if (response.ok) { return response.text() } this.logger.error('Failed: ', response.status, response.statusText) return null }) .then((consumptionUrl: string): string => { this.logger.error('Success accessing consume endpoint: ', consumptionUrl) return consumptionUrl }) .catch((error) => { this.logger.error('Error fetching the data asset consumption url: ', error) return null }) return accessUrl } /** * Search over the DDOs using a query. * @param {SearchQuery} query Query to filter the DDOs. * @return {Promise} */ public async queryMetadata(query: SearchQuery): Promise { const result: QueryResult = await this.fetch .post(`${this.url}${apiPath}/query`, JSON.stringify(query)) .then((response: Response) => { if (response.ok) { return response.json() } this.logger.error('queryMetadata failed:', response.status, response.statusText) return this.transformResult() }) .then((results) => { return this.transformResult(results) }) .catch((error) => { this.logger.error('Error fetching querying metadata: ', error) return this.transformResult() }) return result } /** * Stores a DDO in Metadata Store. * @param {DDO} ddo DDO to be stored. * @return {Promise} Final DDO. */ public async storeDDO(ddo: DDO): Promise { const fullUrl = `${this.url}${apiPath}` const result: DDO = await this.fetch .post(fullUrl, DDO.serialize(ddo)) .then((response: Response) => { if (response.ok) { return response.json() } this.logger.error('storeDDO failed:', response.status, response.statusText, ddo) return null as DDO }) .then((response: DDO) => { return new DDO(response) as DDO }) .catch((error) => { this.logger.error('Error fetching querying metadata: ', error) return null as DDO }) return result } /** * Encrypts a DDO * @param {any} ddo bytes to be encrypted. * @return {Promise} Hex encoded encrypted DDO. */ public async encryptDDO(ddo: any): Promise { const fullUrl = `${this.url}/api/v1/aquarius/assets/ddo/encryptashex ` const result = await this.fetch .postWithOctet(fullUrl, ddo) .then((response: Response) => { if (response.ok) { return response.text() } this.logger.error('encryptDDO failed:', response.status, response.statusText, ddo) return null }) .catch((error) => { this.logger.error('Error encryptDDO: ', error) return null }) return result } /** * Validate Metadata * @param {Metadata} metadata metadata to be validated. If it's a Metadata, it will be validated agains the local schema. Else, it's validated agains the remote schema * @return {Promise} Result. */ public async validateMetadata(metadata: Metadata | DDO): Promise { const status: ValidateMetadata = { valid: false } const path = isDdo(metadata) ? '/validate-remote' : '/validate' try { const response = await this.fetch.post( `${this.url}${apiPath}${path}`, JSON.stringify(metadata) ) if (response.ok) { const errors = await response.json() if (errors === true) status.valid = true else status.errors = errors } else { this.logger.error( 'validate Metadata failed:', response.status, response.statusText ) } } catch (error) { this.logger.error('Error validating metadata: ', error) } return status } /** * Retrieves a DDO by DID. * @param {DID | string} did DID of the asset. * @return {Promise} DDO of the asset. */ public async retrieveDDO( did: DID | string, metadataServiceEndpoint?: string ): Promise { did = did && DID.parse(did) const fullUrl = metadataServiceEndpoint || `${this.url}${apiPath}/${did.getDid()}` const result = await this.fetch .get(fullUrl) .then((response: Response) => { if (response.ok) { return response.json() } this.logger.log('retrieveDDO failed:', response.status, response.statusText, did) return null as DDO }) .then((response: DDO) => { return new DDO(response) as DDO }) .catch((error) => { this.logger.error('Error fetching querying metadata: ', error) return null as DDO }) return result } public async retrieveDDOByUrl(metadataServiceEndpoint?: string): Promise { return this.retrieveDDO(undefined, metadataServiceEndpoint) } /** * Transfer ownership of a DDO * @param {DID | string} did DID of the asset to update. * @param {String} newOwner New owner of the DDO * @param {String} updated Updated field of the DDO * @param {String} signature Signature using updated field to verify that the consumer has rights * @return {Promise} Result. */ public async transferOwnership( did: DID | string, newOwner: string, updated: string, signature: string ): Promise { did = did && DID.parse(did) const fullUrl = `${this.url}${apiPath}/owner/update/${did.getDid()}` const result = await this.fetch .put( fullUrl, JSON.stringify({ signature: signature, updated: updated, newOwner: newOwner }) ) .then((response: Response) => { if (response.ok) { return response.text } this.logger.log('transferownership failed:', response.status, response.statusText) return null }) .catch((error) => { this.logger.error('Error transfering ownership metadata: ', error) return null }) return result } public async getOwnerAssets(owner: string): Promise { const q = { page: 1, offset: 100, query: { query_string: { query: `publicKey.owner:${owner}` } }, sort: { created: 1 } } as SearchQuery const result = await this.queryMetadata(q) return result } /** * Retire a DDO (Delete) * @param {DID | string} did DID of the asset to update. * @param {String} updated Updated field of the DDO * @param {String} signature Signature using updated field to verify that the consumer has rights * @return {Promise} Result. */ public async retire( did: DID | string, updated: string, signature: string ): Promise { did = did && DID.parse(did) const fullUrl = `${this.url}${apiPath}/${did.getDid()}` const result = await this.fetch .delete( fullUrl, JSON.stringify({ signature: signature, updated: updated }) ) .then((response: Response) => { if (response.ok) { return response.text } this.logger.log('retire failed:', response.status, response.statusText) return null }) .catch((error) => { this.logger.error('Error transfering ownership metadata: ', error) return null }) return result } public getServiceEndpoint(did: DID): string { return `${this.url}/api/v1/aquarius/assets/ddo/did:op:${did.getId()}` } public getURI(): string { return `${this.url}` } private transformResult( { results, page, total_pages: totalPages, total_results: totalResults }: any = { result: [], page: 0, total_pages: 0, total_results: 0 } ): QueryResult { return { results: (results || []).map((ddo: DDO) => new DDO(ddo as DDO)), page, totalPages, totalResults } } }