UNPKG

9.69 kBJavaScriptView Raw
1/*
2 * Licensed under the Apache License, Version 2.0 (the "License");
3 * you may not use this file except in compliance with the License.
4 * You may obtain a copy of the License at
5 *
6 * http://www.apache.org/licenses/LICENSE-2.0
7 *
8 * Unless required by applicable law or agreed to in writing, software
9 * distributed under the License is distributed on an "AS IS" BASIS,
10 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11 * See the License for the specific language governing permissions and
12 * limitations under the License.
13 */
14'use strict';
15
16function asyncGeneratorStep(gen, resolve, reject, _next, _throw, key, arg) { try { var info = gen[key](arg); var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { Promise.resolve(value).then(_next, _throw); } }
17
18function _asyncToGenerator(fn) { return function () { var self = this, args = arguments; return new Promise(function (resolve, reject) { var gen = fn.apply(self, args); function _next(value) { asyncGeneratorStep(gen, resolve, reject, _next, _throw, "next", value); } function _throw(err) { asyncGeneratorStep(gen, resolve, reject, _next, _throw, "throw", err); } _next(undefined); }); }; }
19
20const NodeCache = require('node-cache');
21
22const Template = require('./template');
23
24const Logger = require('@accordproject/ergo-compiler').Logger;
25
26const rp = require('request-promise-native');
27
28const crypto = require('crypto');
29
30const stringify = require('json-stable-stringify');
31
32const semver = require('semver');
33
34const globalTemplateCache = new NodeCache({
35 stdTTL: 600,
36 useClones: false
37});
38const globalTemplateIndexCache = new NodeCache({
39 stdTTL: 600,
40 useClones: false
41});
42/**
43 * <p>
44 * Loads templates from the Accord Project Template Library
45 * stored at: https://templates.accordproject.org. The template index
46 * and the templates themselves are cached in a global in-memory cache with a TTL
47 * of 600 seconds. Call the clearCache method to clear the cache.
48 * </p>
49 * @private
50 * @class
51 * @memberof module:cicero-template-library
52 */
53
54class TemplateLibrary {
55 /**
56 * Create the Template Library
57 * @param {string} url - the url to connect to. Defaults to
58 * https://templates.accordproject.org
59 */
60 constructor() {
61 let url = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : null;
62 this.url = url || 'https://templates.accordproject.org';
63 Logger.info('Creating TemplateLibrary for ' + this.url);
64 }
65 /**
66 * Clears the caches
67 */
68
69
70 clearCache() {
71 return _asyncToGenerator(function* () {
72 globalTemplateCache.flushAll();
73 globalTemplateIndexCache.flushAll();
74 })();
75 }
76 /**
77 * Returns a template index that only contains the latest version
78 * of each template
79 *
80 * @param {object} templateIndex - the template index
81 * @returns {object} a new template index that only contains the latest version of each template
82 */
83
84
85 static filterTemplateIndexLatestVersion(templateIndex) {
86 const result = {};
87 const nameToVersion = {}; // build a map of the latest version of each template
88
89 for (let template of Object.keys(templateIndex)) {
90 const atIndex = template.indexOf('@');
91 const name = template.substring(0, atIndex);
92 const version = template.substring(atIndex + 1);
93 const existingVersion = nameToVersion[name];
94
95 if (!existingVersion || semver.lt(existingVersion, version)) {
96 nameToVersion[name] = version;
97 }
98 } // now build the result
99
100
101 for (let name in nameToVersion) {
102 const id = "".concat(name, "@").concat(nameToVersion[name]);
103 result[id] = templateIndex[id];
104 }
105
106 return result;
107 }
108 /**
109 * Returns a template index that only contains the latest version
110 * of each template
111 *
112 * @param {object} templateIndex - the template index
113 * @param {string} ciceroVersion - the cicero version in semver format
114 * @returns {object} a new template index that only contains the templates that are semver compatible
115 * with the cicero version specified
116 */
117
118
119 static filterTemplateIndexCiceroVersion(templateIndex, ciceroVersion) {
120 const result = {}; // build a map of the templates that are compatible with the cicero version
121
122 for (let key of Object.keys(templateIndex)) {
123 const template = templateIndex[key];
124
125 if (semver.satisfies(ciceroVersion, template.ciceroVersion, {
126 includePrerelease: true
127 })) {
128 result[key] = template;
129 }
130 }
131
132 return result;
133 }
134 /**
135 * Gets the metadata for all the templates in the template library
136 * @param {object} [options] - the (optional) options
137 * @param {object} [options.latestVersion] - only return the latest version of each template
138 * @param {object} [options.ciceroVersion] - semver filter on the cicero engine version. E.g. pass 0.4.6 to
139 * only return templates that are compatible with Cicero version 0.4.6
140 * @return {Promise} promise to a template index
141 */
142
143
144 getTemplateIndex(options) {
145 var _this = this;
146
147 return _asyncToGenerator(function* () {
148 const cacheKey = _this.getTemplateIndexCacheKey(options);
149
150 const result = globalTemplateIndexCache.get(cacheKey);
151
152 if (result) {
153 Logger.info('Returning template index from cache');
154 return Promise.resolve(result);
155 }
156
157 const httpOptions = {
158 uri: "".concat(_this.url, "/template-library.json"),
159 headers: {
160 'User-Agent': 'clause'
161 },
162 json: true // Automatically parses the JSON string in the response
163
164 };
165 Logger.info('Loading template library from', httpOptions.uri);
166 return rp(httpOptions).then(templateIndex => {
167 if (options && options.latestVersion) {
168 templateIndex = TemplateLibrary.filterTemplateIndexLatestVersion(templateIndex);
169 }
170
171 if (options && options.ciceroVersion) {
172 templateIndex = TemplateLibrary.filterTemplateIndexCiceroVersion(templateIndex, options.ciceroVersion);
173 }
174
175 globalTemplateIndexCache.set(cacheKey, templateIndex);
176 return templateIndex;
177 }).catch(err => {
178 Logger.error('Failed to load template index', err);
179 throw err;
180 });
181 })();
182 }
183 /**
184 * Returns true if the template library can handle the URI.
185 * @param {string} templateUri - the template URI
186 * @return {boolean} true if the template library can process these URIs
187 */
188
189
190 static acceptsURI(templateUri) {
191 return templateUri.startsWith('ap://');
192 }
193 /**
194 * Parse a template URI into constituent parts
195 * @param {string} templateUri - the URI of the template. E.g.
196 * ap://helloworld@0.0.3#1cafebabe
197 * @return {object} result of parsing
198 * @throws {Error} if the URI is invalid
199 */
200
201
202 static parseURI(templateUri) {
203 if (!templateUri.startsWith('ap://')) {
204 throw new Error("Unsupported protocol: ".concat(templateUri));
205 }
206
207 const atIndex = templateUri.indexOf('@');
208 const hashIndex = templateUri.indexOf('#');
209
210 if (atIndex < 0 || hashIndex < 0) {
211 throw new Error("Invalid template specifier. Must contain @ and #: ".concat(templateUri));
212 }
213
214 return {
215 protocol: 'ap',
216 templateName: templateUri.substring(5, atIndex),
217 templateVersion: templateUri.substring(atIndex + 1, hashIndex),
218 templateHash: templateUri.substring(hashIndex + 1)
219 };
220 }
221 /**
222 * Gets a template instance from a URI
223 * @param {string} templateUri - the URI of the template. E.g.
224 * ap://helloworld@0.0.3#cafebabe
225 * @return {Promise} promise to a Template instance
226 * @throws {Error} if the templateUri is invalid
227 */
228
229
230 getTemplate(templateUri) {
231 var _this2 = this;
232
233 return _asyncToGenerator(function* () {
234 const cacheKey = _this2.getTemplateCacheKey(templateUri);
235
236 const result = globalTemplateCache.get(cacheKey);
237
238 if (result) {
239 Logger.info('Returning template from cache', templateUri);
240 return result;
241 }
242
243 const templateUriInfo = TemplateLibrary.parseURI(templateUri);
244 const templateIndex = yield _this2.getTemplateIndex();
245 const templateMetadata = templateIndex["".concat(templateUriInfo.templateName, "@").concat(templateUriInfo.templateVersion)];
246
247 if (!templateMetadata) {
248 throw new Error("Failed to find template ".concat(templateUri));
249 } // fetch the template
250
251
252 const template = yield Template.fromUrl(templateMetadata.url); // check the hash matches
253
254 const templateHash = template.getHash();
255
256 if (templateHash !== templateUriInfo.templateHash) {
257 Logger.warn("Requested template ".concat(templateUri, " but the hash of the template is ").concat(templateHash));
258 }
259
260 globalTemplateCache.set(cacheKey, template);
261 return template;
262 })();
263 }
264 /**
265 * Returns the cache key used to cache the template index.
266 * @param {object} [options] - the (optional) options
267 * @returns {string} the cache key or null if the index should not be cached
268 */
269
270
271 getTemplateIndexCacheKey(options) {
272 let prefix = '';
273
274 if (options) {
275 const hasher = crypto.createHash('sha256');
276 hasher.update(stringify(options));
277 prefix = "".concat(hasher.digest('hex'), "-");
278 }
279
280 return "".concat(this.url, "/").concat(prefix, "template-library.json");
281 }
282 /**
283 * Returns the cache key used to cache access to a template.
284 * @param {string} templateUri the URI for the template
285 * @returns {string} the cache key or null if the template should not be cached
286 */
287
288
289 getTemplateCacheKey(templateUri) {
290 return "".concat(this.url, "/").concat(templateUri);
291 }
292
293}
294
295module.exports = TemplateLibrary;
\No newline at end of file