@loopback/cli 4.0.0-alpha.8 → 4.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (160) hide show
  1. package/.yo-rc.json +1719 -0
  2. package/{generators/project/templates/LICENSE → LICENSE} +2 -1
  3. package/README.md +44 -43
  4. package/bin/cli-main.js +61 -0
  5. package/generators/app/index.js +109 -15
  6. package/generators/app/templates/.dockerignore +5 -0
  7. package/generators/app/templates/Dockerfile +28 -0
  8. package/generators/app/templates/README.md.ejs +130 -0
  9. package/generators/app/templates/public/index.html.ejs +88 -0
  10. package/generators/app/templates/{test → src/__tests__}/README.md +0 -1
  11. package/generators/app/templates/src/__tests__/acceptance/home-page.acceptance.ts.ejs +31 -0
  12. package/generators/app/templates/src/__tests__/acceptance/ping.controller.acceptance.ts.ejs +21 -0
  13. package/generators/app/templates/src/__tests__/acceptance/test-helper.ts.ejs +32 -0
  14. package/generators/app/templates/src/application.ts.ejs +70 -0
  15. package/generators/app/templates/src/controllers/README.md +6 -0
  16. package/generators/app/templates/src/controllers/index.ts.ejs +1 -0
  17. package/generators/app/templates/src/controllers/ping.controller.ts.ejs +55 -0
  18. package/generators/app/templates/src/datasources/README.md +3 -0
  19. package/generators/app/templates/src/index.ts.ejs +39 -0
  20. package/generators/app/templates/src/migrate.ts.ejs +20 -0
  21. package/generators/app/templates/src/models/README.md +3 -0
  22. package/generators/app/templates/src/openapi-spec.ts.ejs +23 -0
  23. package/generators/app/templates/src/sequence.ts.ejs +3 -0
  24. package/generators/controller/index.js +240 -23
  25. package/generators/controller/templates/src/controllers/controller-rest-template.ts.ejs +150 -0
  26. package/generators/controller/templates/src/controllers/{controller-template.ts → controller-template.ts.ejs} +2 -2
  27. package/generators/copyright/fs.js +46 -0
  28. package/generators/copyright/git.js +78 -0
  29. package/generators/copyright/header.js +306 -0
  30. package/generators/copyright/index.js +230 -0
  31. package/generators/copyright/license.js +105 -0
  32. package/generators/datasource/index.js +341 -0
  33. package/generators/datasource/templates/datasource.ts.ejs +22 -0
  34. package/generators/discover/import-discovered-model.js +70 -0
  35. package/generators/discover/index.js +411 -0
  36. package/generators/example/downloader.js +16 -0
  37. package/generators/example/index.js +176 -0
  38. package/generators/extension/index.js +34 -5
  39. package/generators/extension/templates/README.md.ejs +32 -0
  40. package/generators/extension/templates/{test → src/__tests__}/acceptance/README.md +0 -0
  41. package/generators/extension/templates/{test → src/__tests__}/integration/README.md +0 -0
  42. package/generators/extension/templates/{test → src/__tests__}/unit/README.md +0 -0
  43. package/generators/extension/templates/src/component.ts.ejs +22 -0
  44. package/generators/extension/templates/src/controllers/README.md +3 -2
  45. package/generators/extension/templates/src/decorators/README.md +10 -4
  46. package/generators/extension/templates/src/index.ts.ejs +3 -0
  47. package/generators/extension/templates/src/keys.ts.ejs +11 -0
  48. package/generators/extension/templates/src/mixins/README.md +77 -21
  49. package/generators/extension/templates/src/providers/README.md +51 -25
  50. package/generators/extension/templates/src/repositories/README.md +1 -1
  51. package/generators/extension/templates/src/types.ts.ejs +15 -0
  52. package/generators/import-lb3-models/index.js +197 -0
  53. package/generators/import-lb3-models/lb3app-loader.js +31 -0
  54. package/generators/import-lb3-models/migrate-model.js +249 -0
  55. package/generators/import-lb3-models/model-names.js +32 -0
  56. package/generators/interceptor/index.js +178 -0
  57. package/generators/interceptor/templates/interceptor-template.ts.ejs +62 -0
  58. package/generators/model/index.js +536 -0
  59. package/generators/model/property-definition.js +85 -0
  60. package/generators/model/templates/model.ts.ejs +49 -0
  61. package/generators/observer/index.js +132 -0
  62. package/generators/observer/templates/observer-template.ts.ejs +40 -0
  63. package/generators/openapi/README.md +211 -0
  64. package/generators/openapi/index.js +535 -0
  65. package/generators/openapi/schema-helper.js +447 -0
  66. package/generators/openapi/spec-helper.js +484 -0
  67. package/generators/openapi/spec-loader.js +75 -0
  68. package/generators/openapi/templates/src/controllers/controller-template.ts.ejs +43 -0
  69. package/generators/openapi/templates/src/datasources/datasource.ts.ejs +42 -0
  70. package/generators/openapi/templates/src/models/model-template.ts.ejs +71 -0
  71. package/generators/openapi/templates/src/models/type-template.ts.ejs +13 -0
  72. package/generators/openapi/templates/src/services/service-proxy-template.ts.ejs +55 -0
  73. package/generators/openapi/utils.js +322 -0
  74. package/generators/project/templates/.eslintignore +4 -0
  75. package/generators/project/templates/.eslintrc.js.ejs +3 -0
  76. package/generators/project/templates/.mocharc.json +5 -0
  77. package/generators/project/templates/.prettierignore +0 -2
  78. package/generators/project/templates/.prettierrc +2 -1
  79. package/generators/project/templates/.vscode/launch.json +38 -0
  80. package/generators/project/templates/.vscode/settings.json +32 -0
  81. package/generators/project/templates/.vscode/tasks.json +29 -0
  82. package/generators/project/templates/DEVELOPING.md +36 -0
  83. package/generators/project/templates/_.gitignore +3 -5
  84. package/generators/project/templates/package.json.ejs +175 -0
  85. package/generators/project/templates/package.plain.json.ejs +176 -0
  86. package/generators/project/templates/tsconfig.json.ejs +39 -0
  87. package/generators/relation/base-relation.generator.js +220 -0
  88. package/generators/relation/belongs-to-relation.generator.js +196 -0
  89. package/generators/relation/has-many-relation.generator.js +200 -0
  90. package/generators/relation/has-many-through-relation.generator.js +331 -0
  91. package/generators/relation/has-one-relation.generator.js +200 -0
  92. package/generators/relation/index.js +795 -0
  93. package/generators/relation/references-many-relation.generator.js +142 -0
  94. package/generators/relation/templates/controller-relation-template-belongs-to.ts.ejs +38 -0
  95. package/generators/relation/templates/controller-relation-template-has-many-through.ts.ejs +110 -0
  96. package/generators/relation/templates/controller-relation-template-has-many.ts.ejs +110 -0
  97. package/generators/relation/templates/controller-relation-template-has-one.ts.ejs +110 -0
  98. package/generators/relation/utils.generator.js +260 -0
  99. package/generators/repository/index.js +576 -0
  100. package/generators/repository/templates/src/repositories/repository-crud-default-template.ts.ejs +21 -0
  101. package/generators/repository/templates/src/repositories/repository-kv-template.ts.ejs +19 -0
  102. package/generators/rest-crud/crud-rest-component.js +63 -0
  103. package/generators/rest-crud/index.js +423 -0
  104. package/generators/rest-crud/templates/src/model-endpoints/model.rest-config-template.ts.ejs +11 -0
  105. package/generators/service/index.js +351 -0
  106. package/generators/service/templates/local-service-class-template.ts.ejs +10 -0
  107. package/generators/service/templates/local-service-provider-template.ts.ejs +19 -0
  108. package/generators/service/templates/remote-service-proxy-template.ts.ejs +21 -0
  109. package/generators/update/index.js +55 -0
  110. package/intl/cs/messages.json +204 -0
  111. package/intl/de/messages.json +204 -0
  112. package/intl/en/messages.json +204 -0
  113. package/intl/es/messages.json +204 -0
  114. package/intl/fr/messages.json +204 -0
  115. package/intl/it/messages.json +204 -0
  116. package/intl/ja/messages.json +204 -0
  117. package/intl/ko/messages.json +204 -0
  118. package/intl/nl/messages.json +204 -0
  119. package/intl/pl/messages.json +204 -0
  120. package/intl/pt/messages.json +204 -0
  121. package/intl/ru/messages.json +204 -0
  122. package/intl/tr/messages.json +204 -0
  123. package/intl/zh-Hans/messages.json +204 -0
  124. package/intl/zh-Hant/messages.json +204 -0
  125. package/lib/artifact-generator.js +137 -39
  126. package/lib/ast-helper.js +214 -0
  127. package/lib/base-generator.js +509 -0
  128. package/lib/cli.js +233 -0
  129. package/lib/connectors.json +894 -0
  130. package/lib/debug.js +16 -0
  131. package/lib/globalize.js +12 -0
  132. package/lib/model-discoverer.js +118 -0
  133. package/lib/project-generator.js +154 -57
  134. package/lib/tab-completion.js +127 -0
  135. package/lib/update-index.js +44 -0
  136. package/lib/utils.js +689 -20
  137. package/lib/version-helper.js +299 -0
  138. package/package.json +183 -39
  139. package/CHANGELOG.md +0 -75
  140. package/bin/cli.js +0 -66
  141. package/generators/.DS_Store +0 -0
  142. package/generators/app/templates/index.js +0 -14
  143. package/generators/app/templates/src/application.ts +0 -27
  144. package/generators/app/templates/src/controllers/ping-controller.ts +0 -25
  145. package/generators/app/templates/src/index.ts +0 -25
  146. package/generators/app/templates/test/ping-controller.test.ts +0 -46
  147. package/generators/extension/templates/index.js +0 -8
  148. package/generators/extension/templates/src/component.ts +0 -14
  149. package/generators/extension/templates/src/index.ts +0 -6
  150. package/generators/project/templates/.npmrc +0 -1
  151. package/generators/project/templates/.yo-rc.json +0 -1
  152. package/generators/project/templates/README.md +0 -4
  153. package/generators/project/templates/index.d.ts +0 -6
  154. package/generators/project/templates/index.ts +0 -11
  155. package/generators/project/templates/package.json +0 -79
  156. package/generators/project/templates/package.plain.json +0 -82
  157. package/generators/project/templates/test/mocha.opts +0 -1
  158. package/generators/project/templates/tsconfig.json +0 -29
  159. package/generators/project/templates/tslint.build.json +0 -17
  160. package/generators/project/templates/tslint.json +0 -33
package/lib/utils.js CHANGED
@@ -1,35 +1,68 @@
1
- // Copyright IBM Corp. 2017. All Rights Reserved.
1
+ // Copyright IBM Corp. and LoopBack contributors 2017,2020. All Rights Reserved.
2
2
  // Node module: @loopback/cli
3
3
  // This file is licensed under the MIT License.
4
4
  // License text available at https://opensource.org/licenses/MIT
5
5
 
6
6
  'use strict';
7
7
 
8
+ const chalk = require('chalk');
9
+ const debug = require('../lib/debug')('utils');
8
10
  const fs = require('fs');
11
+ const path = require('path');
9
12
  const util = require('util');
13
+ const stream = require('stream');
14
+ const {spawnSync} = require('child_process');
15
+ const readline = require('readline');
16
+ const semver = require('semver');
10
17
  const regenerate = require('regenerate');
11
18
  const _ = require('lodash');
19
+ const pascalCase = require('change-case').pascalCase;
20
+ const lowerCase = require('change-case').lowerCase;
21
+ const promisify = require('util').promisify;
22
+ const toVarName = require('change-case').camelCase;
23
+ const pluralize = require('pluralize');
24
+ const urlSlug = require('url-slug');
25
+ const validate = require('validate-npm-package-name');
12
26
  const Conflicter = require('yeoman-generator/lib/util/conflicter');
27
+ const connectors = require('./connectors.json');
28
+ const tsquery = require('./ast-helper');
29
+ const stringifyObject = require('stringify-object');
30
+ const camelCase = _.camelCase;
31
+ const kebabCase = _.kebabCase;
32
+ const untildify = require('untildify');
33
+ const tildify = require('tildify');
34
+ const readdirAsync = promisify(fs.readdir);
35
+ const toFileName = name => {
36
+ return kebabCase(name).replace(/\-(\d+)$/g, '$1');
37
+ };
38
+
39
+ const RESERVED_PROPERTY_NAMES = ['constructor'];
40
+
41
+ /**
42
+ * Either a reference to util.promisify or its polyfill, depending on
43
+ * your version of Node.
44
+ */
45
+ exports.promisify = promisify;
13
46
 
14
47
  /**
15
48
  * Returns a valid variable name regex;
16
49
  * taken from https://gist.github.com/mathiasbynens/6334847
17
50
  */
18
51
  function generateValidRegex() {
19
- const get = function(what) {
52
+ const get = function (what) {
20
53
  return require('unicode-10.0.0/' + what + '/code-points.js');
21
54
  };
22
- const ID_Start = get('Binary_Property/ID_Start');
23
- const ID_Continue = get('Binary_Property/ID_Continue');
55
+ const idStart = get('Binary_Property/ID_Start');
56
+ const idContinue = get('Binary_Property/ID_Continue');
24
57
  const compileRegex = _.template(
25
- '^(?:<%= identifierStart %>)(?:<%= identifierPart %>)*$'
58
+ '^(?:<%= identifierStart %>)(?:<%= identifierPart %>)*$',
26
59
  );
27
- const identifierStart = regenerate(ID_Start).add('$', '_');
28
- const identifierPart = regenerate(ID_Continue).add(
60
+ const identifierStart = regenerate(idStart).add('$', '_');
61
+ const identifierPart = regenerate(idContinue).add(
29
62
  '$',
30
63
  '_',
31
64
  '\u200C',
32
- '\u200D'
65
+ '\u200D',
33
66
  );
34
67
  const regex = compileRegex({
35
68
  identifierStart: identifierStart.toString(),
@@ -45,7 +78,7 @@ exports.validRegex = validRegex;
45
78
  * @param name
46
79
  * @returns {String|Boolean}
47
80
  */
48
- exports.validateClassName = function(name) {
81
+ exports.validateClassName = function (name) {
49
82
  if (!name || name === '') {
50
83
  return 'Class name cannot be empty';
51
84
  }
@@ -67,18 +100,145 @@ exports.validateClassName = function(name) {
67
100
  if (name.match(/[\/@\s\+%:]/)) {
68
101
  return util.format(
69
102
  'Class name cannot contain special characters (/@+%: ): %s',
70
- name
103
+ name,
71
104
  );
72
105
  }
73
106
  return util.format('Class name is invalid: %s', name);
74
107
  };
75
108
 
109
+ exports.logNamingIssues = function (name, log) {
110
+ if (name.includes('_')) {
111
+ log(
112
+ chalk.red('>>> ') +
113
+ `Underscores _ in the class name will get removed: ${name}`,
114
+ );
115
+ }
116
+ if (name.match(/[\u00C0-\u024F\u1E00-\u1EFF]/)) {
117
+ log(
118
+ chalk.red('>>> ') +
119
+ `Accented chars in the class name will get replaced: ${name}`,
120
+ );
121
+ }
122
+ };
123
+
124
+ exports.logClassCreation = function (type, typePlural, name, log) {
125
+ log(
126
+ `${exports.toClassName(type)} ${chalk.yellow(
127
+ name,
128
+ )} will be created in src/${typePlural}/${chalk.yellow(
129
+ exports.toFileName(name) + '.' + `${type}.ts`,
130
+ )}`,
131
+ );
132
+ log();
133
+ };
134
+
76
135
  /**
77
136
  * Validate project directory to not exist
78
137
  */
79
- exports.validateyNotExisting = function(path) {
80
- if (fs.existsSync(path)) {
81
- return util.format('Directory %s already exists.', path);
138
+ exports.validateNotExisting = function (projDir) {
139
+ if (fs.existsSync(projDir)) {
140
+ return util.format('Directory %s already exists.', projDir);
141
+ }
142
+ return true;
143
+ };
144
+
145
+ /**
146
+ * validate source key or foreign key for relations
147
+ */
148
+ /* istanbul ignore next */
149
+ exports.validateKeyName = function (name) {
150
+ if (!name || name === '') {
151
+ return 'Key name cannot be empty';
152
+ }
153
+ if (!isNaN(name.charAt(0))) {
154
+ return util.format('Key name cannot start with a number: %s', name);
155
+ }
156
+ if (name.includes('.')) {
157
+ return util.format('Key name cannot contain .: %s', name);
158
+ }
159
+ if (name.includes(' ')) {
160
+ return util.format('Key name cannot contain spaces: %s', name);
161
+ }
162
+ if (name.includes('-')) {
163
+ return util.format('Key name cannot contain hyphens: %s', name);
164
+ }
165
+ if (name.match(/[\/@\s\+%:]/)) {
166
+ return util.format(
167
+ 'Key name cannot contain special characters (/@+%: ): %s',
168
+ name,
169
+ );
170
+ }
171
+ return true;
172
+ };
173
+
174
+ /**
175
+ * validate if the input name is valid. The input name cannot be the same as
176
+ * comparedTo.
177
+ */
178
+ /* istanbul ignore next */
179
+ exports.validateKeyToKeyFrom = function (input, comparedTo) {
180
+ if (!input || input === '') {
181
+ return 'Key name cannot be empty';
182
+ }
183
+ if (input === comparedTo) {
184
+ return util.format(
185
+ 'Through model cannot have two identical foreign keys: %s',
186
+ input,
187
+ );
188
+ }
189
+ if (!isNaN(input.charAt(0))) {
190
+ return util.format('Key name cannot start with a number: %s', input);
191
+ }
192
+ if (input.includes('.')) {
193
+ return util.format('Key name cannot contain .: %s', input);
194
+ }
195
+ if (input.includes(' ')) {
196
+ return util.format('Key name cannot contain spaces: %s', input);
197
+ }
198
+ if (input.includes('-')) {
199
+ return util.format('Key name cannot contain hyphens: %s', input);
200
+ }
201
+ if (input.match(/[\/@\s\+%:]/)) {
202
+ return util.format(
203
+ 'Key name cannot contain special characters (/@+%: ): %s',
204
+ input,
205
+ );
206
+ }
207
+ return true;
208
+ };
209
+
210
+ /**
211
+ * checks if the belongsTo relation has the same relation name and source key name,
212
+ * which is an invalid case.
213
+ */
214
+ /* istanbul ignore next */
215
+ exports.validateRelationName = function (name, type, foreignKeyName) {
216
+ if (!name || name === '') {
217
+ return 'Relation name cannot be empty';
218
+ }
219
+ if (type === 'belongsTo' && name === foreignKeyName) {
220
+ return util.format(
221
+ 'Relation name cannot be the same as the source key name: %s',
222
+ name,
223
+ );
224
+ }
225
+ if (!isNaN(name.charAt(0))) {
226
+ return util.format('Relation name cannot start with a number: %s', name);
227
+ }
228
+ if (name.includes('.')) {
229
+ return util.format('Relation name cannot contain .: %s', name);
230
+ }
231
+ if (name.includes(' ')) {
232
+ return util.format('Relation name cannot contain spaces: %s', name);
233
+ }
234
+ if (name.includes('-')) {
235
+ return util.format('Relation name cannot contain hyphens: %s', name);
236
+ }
237
+ if (name.match(/[\/@\s\+%:]/)) {
238
+ return util.format(
239
+ 'Relation name cannot contain special characters (/@+%: ): %s',
240
+ name,
241
+ );
82
242
  }
83
243
  return true;
84
244
  };
@@ -86,13 +246,55 @@ exports.validateyNotExisting = function(path) {
86
246
  /**
87
247
  * Converts a name to class name after validation
88
248
  */
89
- exports.toClassName = function(name) {
90
- if (name == '') return new Error('no input');
249
+ exports.toClassName = function (name) {
250
+ if (name === '') return new Error('no input');
91
251
  if (typeof name != 'string' || name == null) return new Error('bad input');
92
- return name.substring(0, 1).toUpperCase() + name.substring(1);
252
+ return pascalCase(camelCase(name));
93
253
  };
94
254
 
95
- exports.kebabCase = _.kebabCase;
255
+ exports.lowerCase = lowerCase;
256
+ exports.toFileName = toFileName;
257
+ exports.pascalCase = pascalCase;
258
+ exports.camelCase = camelCase;
259
+ exports.toVarName = toVarName;
260
+ exports.pluralize = pluralize;
261
+ exports.urlSlug = urlSlug;
262
+ exports.untildify = untildify;
263
+ exports.tildify = tildify;
264
+
265
+ exports.validate = function (name) {
266
+ const isValid = validate(name).validForNewPackages;
267
+ if (!isValid) return 'Invalid npm package name: ' + name;
268
+ return isValid;
269
+ };
270
+
271
+ /**
272
+ * Adds a backslash to the start of the word if not already present
273
+ * @param {string} httpPath
274
+ */
275
+ exports.prependBackslash = httpPath => httpPath.replace(/^\/?/, '/');
276
+
277
+ /**
278
+ * Validates whether a given string is a valid url slug or not.
279
+ * Allows slugs with backslash in front of them to be validated as well
280
+ * @param {string} name Slug to validate
281
+ */
282
+ exports.validateUrlSlug = function (name) {
283
+ const backslashIfNeeded = name.charAt(0) === '/' ? '/' : '';
284
+ if (backslashIfNeeded === '/') {
285
+ name = name.slice(1);
286
+ }
287
+ const separators = ['-', '.', '_', '~', ''];
288
+ const possibleSlugs = separators.map(separator =>
289
+ urlSlug(name, {
290
+ separator,
291
+ transformer: false,
292
+ }),
293
+ );
294
+ if (!possibleSlugs.includes(name))
295
+ return `Invalid url slug. Suggested slug: ${backslashIfNeeded}${possibleSlugs[0]}`;
296
+ return true;
297
+ };
96
298
 
97
299
  /**
98
300
  * Extends conflicter so that it keeps track of conflict status
@@ -104,10 +306,477 @@ exports.StatusConflicter = class StatusConflicter extends Conflicter {
104
306
  }
105
307
 
106
308
  checkForCollision(filepath, contents, callback) {
107
- super.checkForCollision(filepath, contents, (err, status) => {
108
- let filename = filepath.split('/').pop();
309
+ // https://github.com/yeoman/generator/pull/1210
310
+ let options = filepath;
311
+ let cb = callback;
312
+ if (typeof contents === 'function') {
313
+ // The signature is `checkForCollision(options, cb)`
314
+ cb = contents;
315
+ } else {
316
+ // The signature is `checkForCollision(filepath, contents, cb)`
317
+ options = {path: filepath, contents};
318
+ }
319
+ super.checkForCollision(options, (err, status) => {
320
+ const filename = options.path.split('/').pop();
109
321
  this.generationStatus[filename] = status;
110
- callback(err, status);
322
+ cb(err, status);
111
323
  });
112
324
  }
113
325
  };
326
+
327
+ /**
328
+ * Find all artifacts in the given path whose type matches the provided
329
+ * filetype.
330
+ * For example, a fileType of "model" will search the target path for matches to
331
+ * "*.model.js"
332
+ * @param {string} path The directory path to search. This search is *not*
333
+ * recursive.
334
+ * @param {string} artifactType The type of the artifact in string form.
335
+ * @param {Function=} reader An optional reader function to retrieve the
336
+ * paths. Must return a Promise.
337
+ * @returns {Promise<string[]>} The filtered list of paths.
338
+ */
339
+ exports.findArtifactPaths = async function (dir, artifactType, reader) {
340
+ const readdir = reader || readdirAsync;
341
+ debug('Finding %j artifact paths at %s', artifactType, dir);
342
+
343
+ try {
344
+ // Wrapping readdir in case it's not a promise.
345
+ const files = await readdir(dir);
346
+ return files.filter(
347
+ f =>
348
+ _.endsWith(f, `${artifactType}.js`) ||
349
+ _.endsWith(f, `${artifactType}.ts`),
350
+ );
351
+ } catch (err) {
352
+ if (err.code === 'ENOENT') {
353
+ // Target directory was not found (e.g. "src/models" does not exist yet).
354
+ return [];
355
+ }
356
+ throw err;
357
+ }
358
+ };
359
+ /**
360
+ * Parses the files of the target directory and returns matching JavaScript
361
+ * or TypeScript artifact files. NOTE: This function does not examine the
362
+ * contents of these files!
363
+ * @param {string} dir The target directory from which to load artifacts.
364
+ * @param {string} artifactType The artifact type (ex. "model", "repository")
365
+ * @param {boolean} addSuffix Whether or not to append the artifact type to the
366
+ * results. (ex. [Foo,Bar] -> [FooRepository, BarRepository])
367
+ * @param {Function} reader An alternate function to replace the promisified
368
+ * fs.readdir (useful for testing and for custom overrides).
369
+ */
370
+ exports.getArtifactList = async function (
371
+ dir,
372
+ artifactType,
373
+ addSuffix,
374
+ reader,
375
+ ) {
376
+ const paths = await exports.findArtifactPaths(dir, artifactType, reader);
377
+ debug('Artifacts paths found:', paths);
378
+ return paths.map(p => {
379
+ const firstWord = _.first(_.split(_.last(_.split(p, path.sep)), '.'));
380
+ const result = pascalCase(exports.toClassName(firstWord));
381
+ return addSuffix
382
+ ? exports.toClassName(result) + exports.toClassName(artifactType)
383
+ : exports.toClassName(result);
384
+ });
385
+ };
386
+
387
+ /**
388
+ * Check package.json and dependencies.json to find out versions for generated
389
+ * dependencies
390
+ */
391
+ exports.getDependencies = function () {
392
+ const pkg = require('../package.json');
393
+ let version = pkg.version;
394
+ // First look for config.loopbackVersion
395
+ if (pkg.config && pkg.config.loopbackVersion) {
396
+ version = pkg.config.loopbackVersion;
397
+ }
398
+ // Set it to be `^x.y.0`
399
+ const loopbackVersion =
400
+ '^' + semver.major(version) + '.' + semver.minor(version) + '.0';
401
+
402
+ const deps = {};
403
+ const dependencies = (pkg.config && pkg.config.templateDependencies) || {};
404
+ for (const i in dependencies) {
405
+ // Default to loopback version if the version for a given dependency is ""
406
+ deps[i] = dependencies[i] || loopbackVersion;
407
+ }
408
+ return deps;
409
+ };
410
+
411
+ /**
412
+ * Rename EJS files
413
+ */
414
+ exports.renameEJS = function () {
415
+ const renameStream = new stream.Transform({objectMode: true});
416
+
417
+ renameStream._transform = function (file, enc, callback) {
418
+ const filePath = file.relative;
419
+ const dirname = path.dirname(filePath);
420
+ let extname = path.extname(filePath);
421
+ let basename = path.basename(filePath, extname);
422
+
423
+ // extname already contains a leading '.'
424
+ const fileName = `${basename}${extname}`;
425
+ const result = fileName.match(/(.+)(.ts|.json|.js|.md|.html)\.ejs$/);
426
+ if (result) {
427
+ extname = result[2];
428
+ basename = result[1];
429
+ file.path = path.join(file.base, dirname, basename + extname);
430
+ }
431
+ callback(null, file);
432
+ };
433
+
434
+ return renameStream;
435
+ };
436
+
437
+ /**
438
+ * Get a validate function for object/array type
439
+ * @param {String} type 'object' OR 'array'
440
+ */
441
+ exports.validateStringObject = function (type) {
442
+ return function validateStringified(val) {
443
+ if (val === null || val === '') {
444
+ return true;
445
+ }
446
+
447
+ const err = `The value must be a stringified ${type}`;
448
+
449
+ if (typeof val !== 'string') {
450
+ return err;
451
+ }
452
+
453
+ try {
454
+ const result = JSON.parse(val);
455
+ if (type === 'array' && !Array.isArray(result)) {
456
+ return err;
457
+ }
458
+ } catch (e) {
459
+ return err;
460
+ }
461
+
462
+ return true;
463
+ };
464
+ };
465
+
466
+ /**
467
+ * Use readline to read text from stdin
468
+ */
469
+ exports.readTextFromStdin = function () {
470
+ const rl = readline.createInterface({
471
+ input: process.stdin,
472
+ });
473
+
474
+ const lines = [];
475
+ let err;
476
+ return new Promise((resolve, reject) => {
477
+ rl.on('SIGINT', () => {
478
+ err = new Error('Canceled by user');
479
+ rl.close();
480
+ })
481
+ .on('line', line => {
482
+ if (line === 'EOF') {
483
+ rl.close();
484
+ } else {
485
+ lines.push(line);
486
+ }
487
+ })
488
+ .on('close', () => {
489
+ if (err) reject(err);
490
+ else resolve(lines.join('\n'));
491
+ })
492
+ .on('error', e => {
493
+ err = e;
494
+ rl.close();
495
+ });
496
+ });
497
+ };
498
+
499
+ /*
500
+ * Validate property name
501
+ * @param {String} name The user input
502
+ * @returns {String|Boolean}
503
+ */
504
+ exports.checkPropertyName = function (name) {
505
+ const result = exports.validateRequiredName(name);
506
+ if (result !== true) return result;
507
+ if (RESERVED_PROPERTY_NAMES.includes(name)) {
508
+ return `${name} is a reserved keyword. Please use another name`;
509
+ }
510
+ return true;
511
+ };
512
+
513
+ /**
514
+ * Validate required name for properties, data sources, or connectors
515
+ * Empty name could not pass
516
+ * @param {String} name The user input
517
+ * @returns {String|Boolean}
518
+ */
519
+ exports.validateRequiredName = function (name) {
520
+ if (!name) {
521
+ return 'Name is required';
522
+ }
523
+ return validateValue(name, /[\/@\s\+%:\.]/);
524
+ };
525
+
526
+ function validateValue(name, unallowedCharacters) {
527
+ if (!unallowedCharacters) {
528
+ unallowedCharacters = /[\/@\s\+%:\.]/;
529
+ }
530
+ if (name.match(unallowedCharacters)) {
531
+ return `Name cannot contain special characters ${unallowedCharacters}: ${name}`;
532
+ }
533
+ if (name !== encodeURIComponent(name)) {
534
+ return `Name cannot contain special characters escaped by encodeURIComponent: ${name}`;
535
+ }
536
+ return true;
537
+ }
538
+ /**
539
+ * Returns the modelName in the directory file format for the model
540
+ * @param {string} modelName
541
+ */
542
+ exports.getModelFileName = function (modelName) {
543
+ return `${toFileName(modelName)}.model.ts`;
544
+ };
545
+
546
+ /**
547
+ * Returns the repositoryName in the directory file format for the repository
548
+ * @param {string} repositoryName
549
+ */
550
+ exports.getRepositoryFileName = function (repositoryName) {
551
+ return `${toFileName(repositoryName)}.repository.ts`;
552
+ };
553
+
554
+ /**
555
+ * Returns the rest-config in the directory file format for the model endpoint
556
+ * @param {string} modelName
557
+ */
558
+ exports.getRestConfigFileName = function (modelName) {
559
+ return `${toFileName(modelName)}.rest-config.ts`;
560
+ };
561
+
562
+ /**
563
+ * Returns the serviceName in the directory file format for the service
564
+ * @param {string} serviceName
565
+ */
566
+ exports.getServiceFileName = function (serviceName) {
567
+ return `${toFileName(serviceName)}.service.ts`;
568
+ };
569
+
570
+ /**
571
+ * Returns the observerName in the directory file format for the observer
572
+ * @param {string} observerName
573
+ */
574
+ exports.getObserverFileName = function (observerName) {
575
+ return `${toFileName(observerName)}.observer.ts`;
576
+ };
577
+
578
+ /**
579
+ * Returns the interceptorName in the directory file format for the interceptor
580
+ * @param {string} interceptorName
581
+ */
582
+ exports.getInterceptorFileName = function (interceptorName) {
583
+ return `${toFileName(interceptorName)}.interceptor.ts`;
584
+ };
585
+
586
+ /**
587
+ *
588
+ * Returns the connector property for the datasource file
589
+ * @param {string} datasourcesDir path for sources
590
+ * @param {string} dataSourceClass class name for the datasoure
591
+ */
592
+ exports.getDataSourceConnectorName = function (
593
+ datasourcesDir,
594
+ dataSourceClass,
595
+ ) {
596
+ if (!dataSourceClass) {
597
+ return false;
598
+ }
599
+ const config = exports.getDataSourceConfig(datasourcesDir, dataSourceClass);
600
+ return config.connector;
601
+ };
602
+
603
+ /**
604
+ *
605
+ * load the connectors available and check if the basedModel matches any
606
+ * connectorType supplied for the given connector name
607
+ * @param {string} connectorType single or a comma separated string array
608
+ * @param {string} datasourcesDir path for sources
609
+ * @param {string} dataSourceClass class name for the datasoure
610
+ */
611
+ exports.isConnectorOfType = function (
612
+ connectorType,
613
+ datasourcesDir,
614
+ dataSourceClass,
615
+ ) {
616
+ debug('calling isConnectorType %o for %s', connectorType, dataSourceClass);
617
+
618
+ if (!dataSourceClass) {
619
+ return false;
620
+ }
621
+
622
+ const config = exports.getDataSourceConfig(datasourcesDir, dataSourceClass);
623
+
624
+ for (const connector of Object.values(connectors)) {
625
+ const matchedConnector =
626
+ config.connector === connector.name ||
627
+ config.connector === `loopback-connector-${connector.name}`;
628
+
629
+ if (matchedConnector) return connectorType.includes(connector.baseModel);
630
+ }
631
+
632
+ // Maybe for other connectors that are not in the supported list
633
+ return null;
634
+ };
635
+
636
+ /**
637
+ * Load the datasource configuration. Supports both the current TypeScript-based
638
+ * flavor and legacy JSON-based configuration.
639
+ * @param {string} datasourcesDir path for sources
640
+ * @param {string} dataSourceClass class name for the datasource
641
+ */
642
+ exports.getDataSourceConfig = function getDataSourceConfig(
643
+ datasourcesDir,
644
+ dataSourceClass,
645
+ ) {
646
+ const config =
647
+ readDataSourceConfigFromTypeScript(datasourcesDir, dataSourceClass) ||
648
+ // Support legacy JSON-based configuration.
649
+ // TODO(semver-major) Print a deprecation warning for JSON-based datasources
650
+ // or stop supporting them entirely.
651
+ readDataSourceConfigFromJSON(datasourcesDir, dataSourceClass);
652
+
653
+ debug('datasource %s has config %o', dataSourceClass, config);
654
+ return config;
655
+ };
656
+
657
+ function readDataSourceConfigFromTypeScript(datasourcesDir, dataSourceClass) {
658
+ const srcFile = path.join(
659
+ datasourcesDir,
660
+ exports.dataSourceToArtifactFileName(dataSourceClass),
661
+ );
662
+ debug(
663
+ 'Reading datasource config for class %s from %s',
664
+ dataSourceClass,
665
+ srcFile,
666
+ );
667
+
668
+ const fileContent = fs.readFileSync(srcFile, 'utf-8');
669
+ return tsquery.getDataSourceConfig(fileContent);
670
+ }
671
+
672
+ function readDataSourceConfigFromJSON(datasourcesDir, dataSourceClass) {
673
+ const datasourceJSONFile = path.join(
674
+ datasourcesDir,
675
+ exports.dataSourceToJSONFileName(dataSourceClass),
676
+ );
677
+
678
+ debug(`Reading datasource config from JSON file ${datasourceJSONFile}`);
679
+ try {
680
+ return JSON.parse(fs.readFileSync(datasourceJSONFile, 'utf8'));
681
+ } catch (err) {
682
+ err.message = `Cannot load ${datasourceJSONFile}: ${err.message}`;
683
+ throw err;
684
+ }
685
+ }
686
+
687
+ /**
688
+ *
689
+ * returns the name property inside the datasource json file
690
+ * @param {string} datasourcesDir path for sources
691
+ * @param {string} dataSourceClass class name for the datasoure
692
+ */
693
+ exports.getDataSourceName = function (datasourcesDir, dataSourceClass) {
694
+ if (!dataSourceClass) {
695
+ return false;
696
+ }
697
+ const config = exports.getDataSourceConfig(datasourcesDir, dataSourceClass);
698
+ return config.name;
699
+ };
700
+
701
+ exports.dataSourceToJSONFileName = function (dataSourceClass) {
702
+ return path.join(
703
+ toFileName(dataSourceClass.replace('Datasource', '')) +
704
+ '.datasource.config.json',
705
+ );
706
+ };
707
+
708
+ exports.dataSourceToArtifactFileName = function (dataSourceClass) {
709
+ return (
710
+ toFileName(dataSourceClass.replace('Datasource', '')) + '.datasource.ts'
711
+ );
712
+ };
713
+
714
+ exports.stringifyObject = function (data, options = {}) {
715
+ return stringifyObject(data, {
716
+ indent: ' ', // two spaces
717
+ singleQuotes: true,
718
+ inlineCharacterLimit: 80,
719
+ ...options,
720
+ });
721
+ };
722
+
723
+ exports.stringifyModelSettings = function (modelSettings) {
724
+ if (!modelSettings || !Object.keys(modelSettings).length) return '';
725
+ return exports.stringifyObject({settings: modelSettings});
726
+ };
727
+
728
+ /**
729
+ * Wrap a single line
730
+ * @param {string} line Text for the a line
731
+ * @param {number} maxLineLength - Maximum line length before wrapping
732
+ */
733
+ function wrapLine(line, maxLineLength) {
734
+ if (line === '') return line;
735
+ let lineLength = 0;
736
+ const words = line.split(/\s+/g);
737
+ return words.reduce((result, word) => {
738
+ if (lineLength + word.length >= maxLineLength) {
739
+ lineLength = word.length;
740
+ return `${result}\n${word}`;
741
+ } else {
742
+ lineLength += word.length + (result ? 1 : 0);
743
+ return result ? `${result} ${word}` : `${word}`;
744
+ }
745
+ }, '');
746
+ }
747
+
748
+ /**
749
+ * Wrap the text into lines respecting the max line length
750
+ * @param {string} text - Text string
751
+ * @param {number} maxLineLength - Maximum line length before wrapping
752
+ */
753
+ function wrapText(text, maxLineLength = 80) {
754
+ let lines = text.split('\n');
755
+ lines = lines.map(line => wrapLine(line, maxLineLength));
756
+ return lines.join('\n');
757
+ }
758
+
759
+ exports.wrapLine = wrapLine;
760
+ exports.wrapText = wrapText;
761
+
762
+ /**
763
+ * Check if `yarn` is installed
764
+ */
765
+ let yarnInstalled = undefined;
766
+ function isYarnAvailable() {
767
+ if (yarnInstalled == null) {
768
+ yarnInstalled = spawnSync('yarn', ['help'], {stdio: false}).status === 0;
769
+ }
770
+ return yarnInstalled;
771
+ }
772
+ exports.isYarnAvailable = isYarnAvailable;
773
+
774
+ // literal strings with artifacts directory locations
775
+ exports.repositoriesDir = 'repositories';
776
+ exports.datasourcesDir = 'datasources';
777
+ exports.servicesDir = 'services';
778
+ exports.modelsDir = 'models';
779
+ exports.observersDir = 'observers';
780
+ exports.interceptorsDir = 'interceptors';
781
+ exports.modelEndpointsDir = 'model-endpoints';
782
+ exports.sourceRootDir = 'src';