@loopback/cli 4.0.0-alpha.7 → 4.0.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 (159) hide show
  1. package/.yo-rc.json +1697 -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 +279 -0
  25. package/generators/controller/templates/src/controllers/controller-rest-template.ts.ejs +150 -0
  26. package/generators/controller/templates/src/controllers/controller-template.ts.ejs +8 -0
  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 +349 -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 +42 -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 +401 -0
  104. package/generators/rest-crud/templates/src/model-endpoints/model.rest-config-template.ts.ejs +10 -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 +189 -0
  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 +746 -22
  137. package/lib/version-helper.js +299 -0
  138. package/package.json +183 -32
  139. package/CHANGELOG.md +0 -64
  140. package/bin/cli.js +0 -65
  141. package/generators/app/templates/index.js +0 -14
  142. package/generators/app/templates/src/application.ts +0 -27
  143. package/generators/app/templates/src/controllers/ping-controller.ts +0 -25
  144. package/generators/app/templates/src/index.ts +0 -25
  145. package/generators/app/templates/test/ping-controller.test.ts +0 -46
  146. package/generators/extension/templates/index.js +0 -8
  147. package/generators/extension/templates/src/component.ts +0 -14
  148. package/generators/extension/templates/src/index.ts +0 -6
  149. package/generators/project/templates/.npmrc +0 -1
  150. package/generators/project/templates/.yo-rc.json +0 -1
  151. package/generators/project/templates/README.md +0 -4
  152. package/generators/project/templates/index.d.ts +0 -6
  153. package/generators/project/templates/index.ts +0 -11
  154. package/generators/project/templates/package.json +0 -79
  155. package/generators/project/templates/package.plain.json +0 -82
  156. package/generators/project/templates/test/mocha.opts +0 -1
  157. package/generators/project/templates/tsconfig.json +0 -29
  158. package/generators/project/templates/tslint.build.json +0 -17
  159. package/generators/project/templates/tslint.json +0 -33
package/lib/utils.js CHANGED
@@ -1,58 +1,782 @@
1
- // Copyright IBM Corp. 2017. All Rights Reserved.
1
+ // Copyright IBM Corp. 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');
17
+ const regenerate = require('regenerate');
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');
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;
46
+
47
+ /**
48
+ * Returns a valid variable name regex;
49
+ * taken from https://gist.github.com/mathiasbynens/6334847
50
+ */
51
+ function generateValidRegex() {
52
+ const get = function (what) {
53
+ return require('unicode-10.0.0/' + what + '/code-points.js');
54
+ };
55
+ const idStart = get('Binary_Property/ID_Start');
56
+ const idContinue = get('Binary_Property/ID_Continue');
57
+ const compileRegex = _.template(
58
+ '^(?:<%= identifierStart %>)(?:<%= identifierPart %>)*$',
59
+ );
60
+ const identifierStart = regenerate(idStart).add('$', '_');
61
+ const identifierPart = regenerate(idContinue).add(
62
+ '$',
63
+ '_',
64
+ '\u200C',
65
+ '\u200D',
66
+ );
67
+ const regex = compileRegex({
68
+ identifierStart: identifierStart.toString(),
69
+ identifierPart: identifierPart.toString(),
70
+ });
71
+ return new RegExp(regex);
72
+ }
73
+ const validRegex = generateValidRegex();
74
+ exports.validRegex = validRegex;
10
75
 
11
76
  /**
12
77
  * validate application (module) name
13
78
  * @param name
14
79
  * @returns {String|Boolean}
15
80
  */
16
- exports.validateAppName = function(name) {
17
- if (name.charAt(0) === '.') {
18
- return util.format('Application name cannot start with .: %s', name);
81
+ exports.validateClassName = function (name) {
82
+ if (!name || name === '') {
83
+ return 'Class name cannot be empty';
84
+ }
85
+ if (name.match(validRegex)) {
86
+ return true;
87
+ }
88
+ if (!isNaN(name.charAt(0))) {
89
+ return util.format('Class name cannot start with a number: %s', name);
90
+ }
91
+ if (name.includes('.')) {
92
+ return util.format('Class name cannot contain .: %s', name);
93
+ }
94
+ if (name.includes(' ')) {
95
+ return util.format('Class name cannot contain spaces: %s', name);
96
+ }
97
+ if (name.includes('-')) {
98
+ return util.format('Class name cannot contain hyphens: %s', name);
19
99
  }
20
100
  if (name.match(/[\/@\s\+%:]/)) {
21
101
  return util.format(
22
- 'Application name cannot contain special characters (/@+%: ): %s',
23
- name
102
+ 'Class name cannot contain special characters (/@+%: ): %s',
103
+ name,
24
104
  );
25
105
  }
26
- if (name.toLowerCase() === 'node_modules') {
27
- return util.format('Application name cannot be node_modules');
106
+ return util.format('Class name is invalid: %s', name);
107
+ };
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
+ );
28
115
  }
29
- if (name.toLowerCase() === 'favicon.ico') {
30
- return util.format('Application name cannot be favicon.ico');
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
+ );
31
121
  }
32
- if (name !== encodeURIComponent(name)) {
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
+
135
+ /**
136
+ * Validate project directory to not exist
137
+ */
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\+%:]/)) {
33
166
  return util.format(
34
- 'Application name cannot contain special characters escaped by' +
35
- ' encodeURIComponent: %s',
36
- name
167
+ 'Key name cannot contain special characters (/@+%: ): %s',
168
+ name,
37
169
  );
38
170
  }
39
171
  return true;
40
172
  };
41
173
 
42
174
  /**
43
- * Validate project directory to not exist
175
+ * validate if the input name is valid. The input name cannot be the same as
176
+ * comparedTo.
44
177
  */
45
- exports.validateyNotExisting = function(path) {
46
- if (fs.existsSync(path)) {
47
- return util.format('Directory %s already exists.', path);
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
+ );
48
206
  }
49
207
  return true;
50
208
  };
51
209
 
52
210
  /**
53
- * Convert a name to class name
211
+ * checks if the belongsTo relation has the same relation name and source key name,
212
+ * which is an invalid case.
54
213
  */
55
- exports.toClassName = function(name) {
56
- const n = name.substring(0, 1).toUpperCase() + name.substring(1);
57
- return n.replace(/[\-\.@\s\+]/g, '_');
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
+ );
242
+ }
243
+ return true;
244
+ };
245
+
246
+ /**
247
+ * Converts a name to class name after validation
248
+ */
249
+ exports.toClassName = function (name) {
250
+ if (name === '') return new Error('no input');
251
+ if (typeof name != 'string' || name == null) return new Error('bad input');
252
+ return pascalCase(camelCase(name));
253
+ };
254
+
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;
58
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
+ };
298
+
299
+ /**
300
+ * Extends conflicter so that it keeps track of conflict status
301
+ */
302
+ exports.StatusConflicter = class StatusConflicter extends Conflicter {
303
+ constructor(adapter, force) {
304
+ super(adapter, force);
305
+ this.generationStatus = {}; // keeps track of file conflict history
306
+ }
307
+
308
+ checkForCollision(filepath, contents, callback) {
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();
321
+ this.generationStatus[filename] = status;
322
+ cb(err, status);
323
+ });
324
+ }
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';