UNPKG

12.6 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
15'use strict';
16
17const debug = require('debug')('concerto:Factory');
18const Globalize = require('./globalize');
19
20const ModelUtil = require('./modelutil');
21
22const InstanceGenerator = require('./serializer/instancegenerator');
23const ValueGeneratorFactory = require('./serializer/valuegenerator');
24const ResourceValidator = require('./serializer/resourcevalidator');
25const TypedStack = require('./serializer/typedstack');
26
27const Relationship = require('./model/relationship');
28const Resource = require('./model/resource');
29const ValidatedResource = require('./model/validatedresource');
30
31const TransactionDeclaration = require('./introspect/transactiondeclaration');
32const EventDeclaration = require('./introspect/eventdeclaration');
33
34const uuid = require('uuid');
35
36const dayjs = require('dayjs');
37const utc = require('dayjs/plugin/utc');
38dayjs.extend(utc);
39
40/**
41 * Use the Factory to create instances of Resource: transactions, participants
42 * and assets.
43 *
44 * @class
45 * @memberof module:concerto-core
46 */
47class Factory {
48 /**
49 * Create a new ID for an object.
50 * @returns {string} a new ID
51 */
52 static newId() {
53 return uuid.v4();
54 }
55
56 /**
57 * Create the factory.
58 *
59 * @param {ModelManager} modelManager - The ModelManager to use for this registry
60 */
61 constructor(modelManager) {
62 this.modelManager = modelManager;
63 this._isFactory = true;
64 }
65
66 /**
67 * Create a new Resource with a given namespace, type name and id
68 * @param {String} ns - the namespace of the Resource
69 * @param {String} type - the type of the Resource
70 * @param {String} [id] - an optional string identifier
71 * @param {Object} [options] - an optional set of options
72 * @param {boolean} [options.disableValidation] - pass true if you want the factory to
73 * return a {@link Resource} instead of a {@link ValidatedResource}. Defaults to false.
74 * @param {String} [options.generate] - Pass one of: <dl>
75 * <dt>sample</dt><dd>return a resource instance with generated sample data.</dd>
76 * <dt>empty</dt><dd>return a resource instance with empty property values.</dd></dl>
77 * @param {boolean} [options.includeOptionalFields] - if <code>options.generate</code>
78 * is specified, whether optional fields should be generated.
79 * @return {Resource} the new instance
80 * @throws {TypeNotFoundException} if the type is not registered with the ModelManager
81 */
82 newResource(ns, type, id, options) {
83 const method = 'newResource';
84 options = options || {};
85
86 const qualifiedName = ModelUtil.getFullyQualifiedName(ns, type);
87 const classDecl = this.modelManager.getType(qualifiedName);
88
89 if(classDecl.isAbstract()) {
90 let formatter = Globalize.messageFormatter('factory-newinstance-abstracttype');
91 throw new Error(formatter({
92 namespace: ns,
93 type: type
94 }));
95 }
96
97 let idField = classDecl.getIdentifierFieldName();
98 if (classDecl.isSystemIdentified()) {
99 id = id === null || id === undefined ? Factory.newId() : id;
100 }
101 if (idField) {
102 if(typeof(id) !== 'string') {
103 let formatter = Globalize.messageFormatter('factory-newinstance-invalididentifier');
104 throw new Error(formatter({
105 namespace: ns,
106 type: type
107 }));
108 }
109
110 if(id.trim().length === 0) {
111 let formatter = Globalize.messageFormatter('factory-newinstance-missingidentifier');
112 throw new Error(formatter({
113 namespace: ns,
114 type: type
115 }));
116 }
117 } else if(id) {
118 throw new Error('Type is not identifiable ' + classDecl.getFullyQualifiedName());
119 }
120
121 let newObj = null;
122 let timestamp = null;
123 if (classDecl instanceof TransactionDeclaration || classDecl instanceof EventDeclaration) {
124 timestamp = dayjs.utc();
125 }
126 if(options.disableValidation) {
127 newObj = new Resource(this.modelManager, classDecl, ns, type, id, timestamp);
128 }
129 else {
130 newObj = new ValidatedResource(this.modelManager, classDecl, ns, type, id, timestamp, new ResourceValidator());
131 }
132 newObj.assignFieldDefaults();
133 this.initializeNewObject(newObj, classDecl, options);
134
135 if (idField) {
136 // if we have an identifier, we set it now
137 newObj[idField] = id;
138 }
139
140 debug(method, 'Factory.newResource created ', id || 'valid');
141 return newObj;
142 }
143
144 /**
145 * Create a new Concept with a given namespace and type name
146 * @param {String} ns - the namespace of the Concept
147 * @param {String} type - the type of the Concept
148 * @param {String} [id] - an optional string identifier
149 * @param {Object} [options] - an optional set of options
150 * @param {boolean} [options.disableValidation] - pass true if you want the factory to
151 * return a {@link Concept} instead of a {@link ValidatedConcept}. Defaults to false.
152 * @param {String} [options.generate] - Pass one of: <dl>
153 * <dt>sample</dt><dd>return a resource instance with generated sample data.</dd>
154 * <dt>empty</dt><dd>return a resource instance with empty property values.</dd></dl>
155 * @param {boolean} [options.includeOptionalFields] - if <code>options.generate</code>
156 * is specified, whether optional fields should be generated.
157 * @return {Resource} the new instance
158 * @throws {TypeNotFoundException} if the type is not registered with the ModelManager
159 */
160 newConcept(ns, type, id, options) {
161 return this.newResource(ns, type, id, options);
162 }
163
164 /**
165 * Create a new Relationship with a given namespace, type and identifier.
166 * A relationship is a typed pointer to an instance. I.e the relationship
167 * with `namespace = 'org.example'`, `type = 'Vehicle'` and `id = 'ABC' creates`
168 * a pointer that points at an instance of org.example.Vehicle with the id
169 * ABC.
170 *
171 * @param {String} ns - the namespace of the Resource
172 * @param {String} type - the type of the Resource
173 * @param {String} id - the identifier
174 * @return {Relationship} - the new relationship instance
175 * @throws {TypeNotFoundException} if the type is not registered with the ModelManager
176 */
177 newRelationship(ns, type, id) {
178 // Load the type declaration to force an error if it doesn't exist
179 const fqn = ModelUtil.getFullyQualifiedName(ns, type);
180 const classDecl = this.modelManager.getType(fqn);
181 if(!classDecl.isIdentified()) {
182 throw new Error(`Cannot create a relationship to ${fqn}, it is not identifiable.`);
183 }
184 return new Relationship(this.modelManager, classDecl, ns, type, id);
185 }
186
187 /**
188 * Create a new transaction object. The identifier of the transaction is set to a UUID.
189 * @param {String} ns - the namespace of the transaction.
190 * @param {String} type - the type of the transaction.
191 * @param {String} [id] - an optional string identifier
192 * @param {Object} [options] - an optional set of options
193 * @param {String} [options.generate] - Pass one of: <dl>
194 * <dt>sample</dt><dd>return a resource instance with generated sample data.</dd>
195 * <dt>empty</dt><dd>return a resource instance with empty property values.</dd></dl>
196 * @param {boolean} [options.includeOptionalFields] - if <code>options.generate</code>
197 * is specified, whether optional fields should be generated.
198 * @return {Resource} A resource for the new transaction.
199 */
200 newTransaction(ns, type, id, options) {
201 if (!ns) {
202 throw new Error('ns not specified');
203 } else if (!type) {
204 throw new Error('type not specified');
205 }
206 let transaction = this.newResource(ns, type, id, options);
207 const classDeclaration = transaction.getClassDeclaration();
208
209 if (!(classDeclaration instanceof TransactionDeclaration)) {
210 throw new Error(transaction.getClassDeclaration().getFullyQualifiedName() + ' is not a transaction');
211 }
212
213 return transaction;
214 }
215
216 /**
217 * Create a new event object. The identifier of the event is
218 * set to a UUID.
219 * @param {String} ns - the namespace of the event.
220 * @param {String} type - the type of the event.
221 * @param {String} [id] - an optional string identifier
222 * @param {Object} [options] - an optional set of options
223 * @param {String} [options.generate] - Pass one of: <dl>
224 * <dt>sample</dt><dd>return a resource instance with generated sample data.</dd>
225 * <dt>empty</dt><dd>return a resource instance with empty property values.</dd></dl>
226 * @param {boolean} [options.includeOptionalFields] - if <code>options.generate</code>
227 * is specified, whether optional fields should be generated.
228 * @return {Resource} A resource for the new event.
229 */
230 newEvent(ns, type, id, options) {
231 if (!ns) {
232 throw new Error('ns not specified');
233 } else if (!type) {
234 throw new Error('type not specified');
235 }
236 let event = this.newResource(ns, type, id, options);
237 const classDeclaration = event.getClassDeclaration();
238
239 if (!(classDeclaration instanceof EventDeclaration)) {
240 throw new Error(event.getClassDeclaration().getFullyQualifiedName() + ' is not an event');
241 }
242
243 return event;
244 }
245
246 /**
247 * PRIVATE IMPLEMENTATION. DO NOT CALL FROM OUTSIDE THIS CLASS.
248 *
249 * Initialize the state of a newly created resource
250 * @private
251 * @param {Typed} newObject - resource to initialize.
252 * @param {ClassDeclaration} classDeclaration - class declaration for the resource.
253 * @param {Object} clientOptions - field generation options supplied by the caller.
254 */
255 initializeNewObject(newObject, classDeclaration, clientOptions) {
256 const generateParams = this.parseGenerateOptions(clientOptions);
257 if (generateParams) {
258 generateParams.stack = new TypedStack(newObject);
259 generateParams.seen = [newObject.getFullyQualifiedType()];
260 const visitor = new InstanceGenerator();
261 classDeclaration.accept(visitor, generateParams);
262 }
263 }
264
265 /**
266 * PRIVATE IMPLEMENTATION. DO NOT CALL FROM OUTSIDE THIS CLASS.
267 *
268 * Parse the client-supplied field generation options and return a corresponding set of InstanceGenerator
269 * options that can be used to initialize a resource.
270 * @private
271 * @param {Object} clientOptions - field generation options supplied by the caller.
272 * @return {Object} InstanceGenerator options.
273 */
274 parseGenerateOptions(clientOptions) {
275 if (!clientOptions.generate) {
276 return null;
277 }
278
279 const generateParams = { };
280 generateParams.modelManager = this.modelManager;
281 generateParams.factory = this;
282
283 if ((/^empty$/i).test(clientOptions.generate)) {
284 generateParams.valueGenerator = ValueGeneratorFactory.empty();
285 } else {
286 // Allow any other value for backwards compatibility with previous (truthy) behavior
287 generateParams.valueGenerator = ValueGeneratorFactory.sample();
288 }
289
290 generateParams.includeOptionalFields = clientOptions.includeOptionalFields ? true : false;
291
292 return generateParams;
293 }
294
295
296 /**
297 * Alternative instanceof that is reliable across different module instances
298 * @see https://github.com/hyperledger/composer-concerto/issues/47
299 *
300 * @param {object} object - The object to test against
301 * @returns {boolean} - True, if the object is an instance of a Factory
302 */
303 static [Symbol.hasInstance](object){
304 return typeof object !== 'undefined' && object !== null && Boolean(object._isFactory);
305 }
306}
307
308module.exports = Factory;