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 | ;
|
16 |
|
17 | const debug = require('debug')('concerto:Factory');
|
18 | const Globalize = require('./globalize');
|
19 |
|
20 | const ModelUtil = require('./modelutil');
|
21 |
|
22 | const InstanceGenerator = require('./serializer/instancegenerator');
|
23 | const ValueGeneratorFactory = require('./serializer/valuegenerator');
|
24 | const ResourceValidator = require('./serializer/resourcevalidator');
|
25 | const TypedStack = require('./serializer/typedstack');
|
26 |
|
27 | const Relationship = require('./model/relationship');
|
28 | const Resource = require('./model/resource');
|
29 | const ValidatedResource = require('./model/validatedresource');
|
30 |
|
31 | const TransactionDeclaration = require('./introspect/transactiondeclaration');
|
32 | const EventDeclaration = require('./introspect/eventdeclaration');
|
33 |
|
34 | const uuid = require('uuid');
|
35 |
|
36 | const dayjs = require('dayjs');
|
37 | const utc = require('dayjs/plugin/utc');
|
38 | dayjs.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 | */
|
47 | class 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 |
|
308 | module.exports = Factory;
|