github.com/kaleido-io/firefly@v0.0.0-20210622132723-8b4b6aacb971/kat/src/routers/asset-instances.ts (about) 1 // Copyright © 2021 Kaleido, Inc. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 import { Router, Request } from 'express'; 16 import RequestError from '../lib/request-handlers'; 17 import * as assetInstancesHandler from '../handlers/asset-instances'; 18 import { constants, requestKeys, streamToString } from '../lib/utils'; 19 import Busboy from 'busboy'; 20 import * as utils from '../lib/utils'; 21 import { IRequestMultiPartContent } from '../lib/interfaces'; 22 import { config } from '../lib/config'; 23 24 const router = Router(); 25 26 router.get('/:assetDefinitionID', async (req, res, next) => { 27 try { 28 const skip = Number(req.query.skip || 0); 29 const limit = Number(req.query.limit || constants.DEFAULT_PAGINATION_LIMIT); 30 if (isNaN(skip) || isNaN(limit)) { 31 throw new RequestError('Invalid skip / limit', 400); 32 } 33 res.send(await assetInstancesHandler.handleGetAssetInstancesRequest(req.params.assetDefinitionID, {}, {}, skip, limit)); 34 } catch (err) { 35 next(err); 36 } 37 }); 38 39 router.get('/:assetDefinitionID/:assetInstanceID', async (req, res, next) => { 40 try { 41 const content = req.query.content === 'true'; 42 res.send(await assetInstancesHandler.handleGetAssetInstanceRequest(req.params.assetDefinitionID, req.params.assetInstanceID, content)); 43 } catch (err) { 44 next(err); 45 } 46 }); 47 48 router.post('/search/:assetDefinitionID', async (req, res, next) => { 49 try { 50 const skip = Number(req.body.skip || 0); 51 const limit = Number(req.body.limit || constants.DEFAULT_PAGINATION_LIMIT); 52 if (req.body.count !== true && (isNaN(skip) || isNaN(limit))) { 53 throw new RequestError('Invalid skip / limit', 400); 54 } 55 if (!req.body.query) { 56 throw new RequestError('Missing search query', 400); 57 } 58 res.send(req.body.count === true ? 59 await assetInstancesHandler.handleCountAssetInstancesRequest(req.params.assetDefinitionID, req.body.query) : 60 await assetInstancesHandler.handleGetAssetInstancesRequest(req.params.assetDefinitionID, req.body.query, req.body.sort || {}, skip, limit) 61 ); 62 } catch (err) { 63 next(err); 64 } 65 }); 66 67 router.post('/:assetDefinitionID', async (req, res, next) => { 68 try { 69 let assetInstanceID: string; 70 const sync = req.query.sync === 'true'; 71 72 if (req.headers["content-type"]?.startsWith('multipart/form-data')) { 73 let description: Object | undefined; 74 const formData = await extractDataFromMultipartForm(req); 75 if (formData.description !== undefined) { 76 try { 77 description = JSON.parse(await formData.description); 78 } catch (err) { 79 throw new RequestError(`Invalid description. ${err}`, 400); 80 } 81 } 82 if (!formData.author ||!utils.isAuthorValid(formData.author, config.protocol)) { 83 throw new RequestError('Missing or invalid asset instance author', 400); 84 } 85 assetInstanceID = await assetInstancesHandler.handleCreateUnstructuredAssetInstanceRequest(formData.author, req.params.assetDefinitionID, description, formData.contentStream, formData.contentFileName, formData.isContentPrivate, req.body.participants, sync); 86 } else { 87 if (!utils.isAuthorValid(req.body.author, config.protocol)) { 88 throw new RequestError('Missing or invalid asset instance author', 400); 89 } 90 if (!(typeof req.body.content === 'object' && req.body.content !== null)) { 91 throw new RequestError('Missing or invalid asset content', 400); 92 } 93 if(req.body.isContentPrivate !== undefined && typeof req.body.isContentPrivate !== 'boolean') { 94 throw new RequestError('Invalid isContentPrivate', 400); 95 } 96 assetInstanceID = await assetInstancesHandler.handleCreateStructuredAssetInstanceRequest(req.body.author, req.params.assetDefinitionID, req.body.description, req.body.content, req.body.isContentPrivate, req.body.participants, sync); 97 } 98 res.send({ status: sync ? 'success' : 'submitted', assetInstanceID }); 99 } catch (err) { 100 next(err); 101 } 102 }); 103 104 router.put('/:assetDefinitionID/:assetInstanceID', async (req, res, next) => { 105 try { 106 switch (req.body.action) { 107 case 'set-property': 108 if (!req.body.key) { 109 throw new RequestError('Missing asset property key', 400); 110 } 111 if (!req.body.value) { 112 throw new RequestError('Missing asset property value', 400); 113 } 114 if (!utils.isAuthorValid(req.body.author, config.protocol)) { 115 throw new RequestError('Missing or invalid asset property author', 400); 116 } 117 const sync = req.query.sync === 'true'; 118 await assetInstancesHandler.handleSetAssetInstancePropertyRequest(req.params.assetDefinitionID, req.params.assetInstanceID, req.body.author, req.body.key, req.body.value, sync); 119 res.send({ status: sync ? 'success' : 'submitted' }); 120 break; 121 case 'push': 122 if (!req.body.memberAddress) { 123 throw new RequestError('Missing member address', 400); 124 } 125 await assetInstancesHandler.handlePushPrivateAssetInstanceRequest(req.params.assetDefinitionID, req.params.assetInstanceID, req.body.memberAddress); 126 res.send({ status: 'success' }); 127 break; 128 default: 129 throw new RequestError('Missing or invalid action'); 130 } 131 } catch (err) { 132 next(err); 133 } 134 }); 135 136 router.patch('/:assetDefinitionID/:assetInstanceID', async (req, res, next) => { 137 try { 138 if (!utils.isAuthorValid(req.body.requester, config.protocol)) { 139 throw new RequestError(`Missing requester`); 140 } 141 await assetInstancesHandler.handleAssetInstanceTradeRequest(req.params.assetDefinitionID, req.body.requester, req.params.assetInstanceID, req.body.metadata); 142 res.send({ status: 'success' }); 143 } catch (err) { 144 next(err); 145 } 146 }); 147 148 router.post('/:assetDefinitionID/:assetInstanceID/push', async (req, res, next) => { 149 try { 150 if (!req.body.memberAddress) { 151 throw new RequestError('Missing member address', 400); 152 } 153 await assetInstancesHandler.handlePushPrivateAssetInstanceRequest(req.params.assetDefinitionID, req.params.assetInstanceID, req.body.memberAddress); 154 res.send({ status: 'success' }); 155 } catch (err) { 156 next(err); 157 } 158 }); 159 160 const extractDataFromMultipartForm = (req: Request): Promise<IRequestMultiPartContent> => { 161 return new Promise(async (resolve, reject) => { 162 let author: string | undefined; 163 let assetDefinitionID: string | undefined; 164 let description: Promise<string> | undefined; 165 let isContentPrivate: boolean | undefined = undefined; 166 req.pipe(new Busboy({ headers: req.headers }) 167 .on('field', (fieldname, value) => { 168 switch (fieldname) { 169 case requestKeys.ASSET_AUTHOR: author = value; break; 170 case requestKeys.ASSET_DEFINITION_ID: assetDefinitionID = value; break; 171 case requestKeys.ASSET_IS_CONTENT_PRIVATE: isContentPrivate = value === 'true'; break; 172 } 173 }).on('file', (fieldname, readableStream, fileName) => { 174 switch (fieldname) { 175 case requestKeys.ASSET_DESCRIPTION: description = streamToString(readableStream); break; 176 case requestKeys.ASSET_CONTENT: resolve({ author, assetDefinitionID, description, contentStream: readableStream, contentFileName: fileName, isContentPrivate }); break; 177 default: readableStream.resume(); 178 } 179 })).on('finish', () => { 180 reject(new RequestError('Missing content', 400)); 181 }); 182 }); 183 }; 184 185 export default router;