@sap/cds-compiler 5.0.6 → 5.1.2

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 (40) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/bin/cdsc.js +34 -8
  3. package/bin/cdshi.js +2 -1
  4. package/lib/api/main.js +10 -1
  5. package/lib/api/options.js +2 -3
  6. package/lib/base/message-registry.js +14 -0
  7. package/lib/base/messages.js +2 -1
  8. package/lib/base/meta.js +10 -0
  9. package/lib/base/optionProcessorHelper.js +11 -0
  10. package/lib/checks/dbFeatureFlags.js +5 -0
  11. package/lib/compiler/assert-consistency.js +1 -0
  12. package/lib/compiler/define.js +4 -2
  13. package/lib/compiler/extend.js +43 -7
  14. package/lib/compiler/index.js +4 -4
  15. package/lib/compiler/populate.js +58 -11
  16. package/lib/compiler/propagator.js +9 -4
  17. package/lib/compiler/resolve.js +115 -79
  18. package/lib/compiler/shared.js +2 -1
  19. package/lib/compiler/tweak-assocs.js +29 -5
  20. package/lib/edm/edm.js +8 -0
  21. package/lib/edm/edmPreprocessor.js +7 -3
  22. package/lib/gen/Dictionary.json +37 -0
  23. package/lib/json/to-csn.js +2 -0
  24. package/lib/main.js +2 -5
  25. package/lib/model/cloneCsn.js +1 -0
  26. package/lib/model/csnRefs.js +2 -1
  27. package/lib/model/csnUtils.js +0 -12
  28. package/lib/model/revealInternalProperties.js +0 -1
  29. package/lib/modelCompare/compare.js +12 -10
  30. package/lib/optionProcessor.js +2 -0
  31. package/lib/render/toCdl.js +8 -7
  32. package/lib/render/toHdbcds.js +1 -2
  33. package/lib/render/toSql.js +44 -8
  34. package/lib/transform/db/backlinks.js +20 -5
  35. package/lib/transform/db/killAnnotations.js +3 -0
  36. package/lib/transform/db/processSqlServices.js +63 -0
  37. package/lib/transform/draft/odata.js +6 -1
  38. package/lib/transform/forRelationalDB.js +9 -0
  39. package/lib/utils/file.js +77 -4
  40. package/package.json +1 -1
@@ -23,6 +23,7 @@ optionProcessor
23
23
  .option(' --options <file>')
24
24
  .option('-w, --warning <level>', { valid: ['0', '1', '2', '3'] })
25
25
  .option(' --quiet')
26
+ .option('-i, --stdin')
26
27
  .option(' --show-message-id')
27
28
  .option(' --no-message-id')
28
29
  .option(' --no-message-context')
@@ -89,6 +90,7 @@ optionProcessor
89
90
  --cds-home <dir> When set, modules starting with '@sap/cds/' are searched in <dir>
90
91
  --module-lookup-directories <list> Comma separated list of directories to look
91
92
  for CDS modules. Default is 'node_modules/'.
93
+ -i, --stdin Read input from stdin.
92
94
  -- Indicate the end of options (helpful if source names start with "-")
93
95
 
94
96
  Type options
@@ -13,11 +13,11 @@ const { typeParameters, specialFunctions } = require('../compiler/builtins');
13
13
  const { isAnnotationExpression } = require('../base/builtins');
14
14
  const { forEach } = require('../utils/objectUtils');
15
15
  const {
16
- generatedByCompilerVersion,
17
16
  getNormalizedQuery,
18
17
  } = require('../model/csnUtils');
19
18
  const { isBuiltinType } = require('../base/builtins');
20
19
  const { cloneFullCsn } = require('../model/cloneCsn');
20
+ const { getKeysDict } = require('../model/csnRefs');
21
21
 
22
22
  const identifierRegex = /^[$_a-zA-Z][$_a-zA-Z0-9]*$/;
23
23
  const specialFunctionKeywords = Object.create(null);
@@ -62,7 +62,7 @@ function csnToCdl( csn, options, msg ) {
62
62
  const hanaRequiresAbsolutePath = usings.available.includes('hana');
63
63
 
64
64
  const cdlResult = Object.create(null);
65
- cdlResult.model = options.testMode ? '' : `// ${generatedByCompilerVersion()} \n`;
65
+ cdlResult.model = '';
66
66
 
67
67
  const subelementAnnotates = [];
68
68
 
@@ -676,7 +676,7 @@ function csnToCdl( csn, options, msg ) {
676
676
  artifact = artifact.items;
677
677
  }
678
678
 
679
- if (!artifact.elements && !artifact.enum)
679
+ if (!artifact.elements && !artifact.enum && !artifact.keys)
680
680
  return null;
681
681
 
682
682
  const annotate = { annotate: env.path[1] };
@@ -712,15 +712,16 @@ function csnToCdl( csn, options, msg ) {
712
712
  */
713
713
  function collectAnnos( annotateObj, art ) {
714
714
  if (!Object.hasOwnProperty.call(art, 'elements') &&
715
- !Object.hasOwnProperty.call(art, 'enum'))
715
+ !Object.hasOwnProperty.call(art, 'enum') &&
716
+ !Object.hasOwnProperty.call(art, 'keys'))
716
717
  return false;
717
718
 
718
- const dictKey = art.elements ? 'elements' : 'enum';
719
- // Use "elements" for both enums and elements. This is allowed in extensions.
719
+ const dict = art.enum || art.keys && getKeysDict(art) || art.elements;
720
+ // Use "elements" for all. This is allowed in extensions.
720
721
  const collected = { elements: Object.create(null) };
721
722
  let hasAnnotation = false;
722
723
 
723
- forEach(art[dictKey], (elemName, element) => {
724
+ forEach(dict, (elemName, element) => {
724
725
  if (!collected.elements[elemName])
725
726
  collected.elements[elemName] = { };
726
727
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  const {
4
4
  getLastPartOf, getLastPartOfRef,
5
- hasValidSkipOrExists, generatedByCompilerVersion, getNormalizedQuery,
5
+ hasValidSkipOrExists, getNormalizedQuery,
6
6
  getRootArtifactName, getResultingName, getNamespace, forEachMember, getVariableReplacement, hasAnnotationValue,
7
7
  pathName,
8
8
  } = require('../model/csnUtils');
@@ -167,7 +167,6 @@ function toHdbcdsSource( csn, options, messageFunctions ) {
167
167
  if (sourceStr !== '') {
168
168
  const name = plainNames ? artifactName.replace(/\./g, '_').toUpperCase() : artifactName;
169
169
  hdbcds[name] = [
170
- !options.testMode ? `// ${generatedByCompilerVersion()} \n` : '',
171
170
  renderNamespaceDeclaration(name, env),
172
171
  renderUsings(name, env),
173
172
  sourceStr,
@@ -3,7 +3,7 @@
3
3
 
4
4
  const {
5
5
  getLastPartOf, getLastPartOfRef,
6
- hasValidSkipOrExists, generatedByCompilerVersion, getNormalizedQuery,
6
+ hasValidSkipOrExists, getNormalizedQuery,
7
7
  forEachDefinition, getResultingName,
8
8
  getVariableReplacement, pathName,
9
9
  } = require('../model/csnUtils');
@@ -186,8 +186,11 @@ function toSqlDdl( csn, options, messageFunctions ) {
186
186
  deletions: Object.create(null),
187
187
  constraintDeletions: [],
188
188
  migrations: Object.create(null),
189
+ hdbrole: Object.create(null),
189
190
  };
190
191
 
192
+ const sqlServiceEntities = Object.create(null);
193
+
191
194
  // Registries for artifact and element names per CSN section
192
195
  const definitionsDuplicateChecker = new DuplicateChecker(options.sqlMapping);
193
196
  const deletionsDuplicateChecker = new DuplicateChecker();
@@ -252,6 +255,25 @@ function toSqlDdl( csn, options, messageFunctions ) {
252
255
  });
253
256
  }
254
257
 
258
+ // Can only happen for HDI based deployment
259
+ // .hdbrole documentation: https://help.sap.com/docs/SAP_HANA_PLATFORM/3823b0f33420468ba5f1cf7f59bd6bd9/625d7733c30b4666b4a522d7fa68a550.html
260
+ Object.keys(sqlServiceEntities).forEach((sqlServiceName) => {
261
+ const accessRole = {
262
+ role: {
263
+ name: renderArtifactNameWithoutQuotes(`${sqlServiceName }.access`),
264
+ object_privileges: Object.entries(sqlServiceEntities[sqlServiceName]).map(([ name, entity ]) => ({
265
+ name: renderArtifactNameWithoutQuotes(name),
266
+ type: entity.query || entity.projection ? 'VIEW' : 'TABLE',
267
+ privileges: [ 'SELECT' ],
268
+ privileges_with_grant_option: [],
269
+ })),
270
+ },
271
+ };
272
+
273
+ if (accessRole.role.object_privileges.length > 0)
274
+ mainResultObj.hdbrole[`${sqlServiceName }_access`] = JSON.stringify(accessRole, null, 2);
275
+ });
276
+
255
277
  // trigger artifact and element name checks
256
278
  definitionsDuplicateChecker.check(error, options);
257
279
  extensionsDuplicateChecker.check(error);
@@ -264,7 +286,6 @@ function toSqlDdl( csn, options, messageFunctions ) {
264
286
  // (relying on the order of dictionaries above)
265
287
  // FIXME: Should consider inter-view dependencies, too
266
288
  const sql = Object.create(null);
267
- const sqlVersionLine = `-- ${generatedByCompilerVersion()}\n`;
268
289
 
269
290
  // Handle hdbKinds separately from alterTable case
270
291
  const {
@@ -278,10 +299,7 @@ function toSqlDdl( csn, options, messageFunctions ) {
278
299
  // Hack: Other than in 'hdbtable' files, in HANA SQL COLUMN is not mandatory but default.
279
300
  if (options.sqlDialect === 'hana' && hdbKind === 'hdbtable' && sourceString.startsWith('COLUMN '))
280
301
  sourceString = sourceString.slice('COLUMN '.length);
281
- sql[name] = `${options.testMode ? '' : sqlVersionLine}CREATE ${sourceString};`;
282
- }
283
- else if (!options.testMode && options.generatedByComment) {
284
- mainResultObj[hdbKind][name] = sqlVersionLine + mainResultObj[hdbKind][name];
302
+ sql[name] = `CREATE ${sourceString};`;
285
303
  }
286
304
  }
287
305
  if (options.src === 'sql')
@@ -295,7 +313,7 @@ function toSqlDdl( csn, options, messageFunctions ) {
295
313
 
296
314
  forEachKey(alterStmts, (constraintName) => {
297
315
  if (!csn.unchangedConstraints?.has(constraintName))
298
- constraints[constraintName] = `${options.testMode ? '' : sqlVersionLine}${alterStmts[constraintName]}`;
316
+ constraints[constraintName] = `${alterStmts[constraintName]}`;
299
317
  });
300
318
  mainResultObj.constraints = constraints;
301
319
  }
@@ -304,7 +322,7 @@ function toSqlDdl( csn, options, messageFunctions ) {
304
322
  mainResultObj.sql = sql;
305
323
 
306
324
  for (const name in deletions)
307
- deletions[name] = `${options.testMode || !options.generatedByComment ? '' : sqlVersionLine}${deletions[name]}`;
325
+ deletions[name] = `${deletions[name]}`;
308
326
 
309
327
  timetrace.stop('SQL rendering');
310
328
  return mainResultObj;
@@ -325,6 +343,10 @@ function toSqlDdl( csn, options, messageFunctions ) {
325
343
 
326
344
  switch (art.kind) {
327
345
  case 'entity':
346
+ if (art.$sqlService) { // collect entities that are in a sql service so we can render the .hdbrole later
347
+ sqlServiceEntities[art.$sqlService] ??= Object.create(null);
348
+ sqlServiceEntities[art.$sqlService][artifactName] = art;
349
+ }
328
350
  if (art.query || art.projection) {
329
351
  const result = renderView(artifactName, art, env);
330
352
  if (result)
@@ -350,6 +372,20 @@ function toSqlDdl( csn, options, messageFunctions ) {
350
372
  }
351
373
  }
352
374
 
375
+ /**
376
+ * Render the given artifactName according to the sqlMapping, but
377
+ * - uppercased for plain
378
+ * - without enclosing " for quoted/hdbcds
379
+ *
380
+ * @param {string} artifactName
381
+ * @returns {string}
382
+ */
383
+ function renderArtifactNameWithoutQuotes( artifactName ) {
384
+ if (options.sqlMapping === 'plain')
385
+ return renderArtifactName(artifactName).toUpperCase();
386
+ return renderArtifactName(artifactName).slice(1, -1); // trim leading/trailing "
387
+ }
388
+
353
389
  /**
354
390
  * Render an artifact extension into the appropriate dictionary of 'resultObj'.
355
391
  * Only SAP HANA SQL is currently supported.
@@ -193,7 +193,7 @@ function getBacklinkTransformer( csnUtils, messageFunctions, options, pathDelimi
193
193
  // Transform comparison of $self to managed association into AND-combined foreign key comparisons
194
194
  if (assoc.keys) {
195
195
  if (assoc.keys.length)
196
- return transformDollarSelfComparisonWithManagedAssoc(assocOp, assoc, assocName, elemName);
196
+ return transformDollarSelfComparisonWithManagedAssoc(assocOp, assoc, assocName, elemName, art, path);
197
197
 
198
198
  if (options.transformation !== 'effective')
199
199
  elem.$ignore = true;
@@ -202,7 +202,7 @@ function getBacklinkTransformer( csnUtils, messageFunctions, options, pathDelimi
202
202
 
203
203
  // Transform comparison of $self to unmanaged association into "reversed" ON-condition
204
204
  else if (assoc.on) {
205
- return transformDollarSelfComparisonWithUnmanagedAssoc(assocOp, assoc, assocName, elemName);
205
+ return transformDollarSelfComparisonWithUnmanagedAssoc(assocOp, assoc, assocName, elemName, art, path);
206
206
  }
207
207
 
208
208
  throw new ModelError(`Expected either managed or unmanaged association in $self-comparison: ${JSON.stringify(elem.on)}`);
@@ -220,9 +220,11 @@ function getBacklinkTransformer( csnUtils, messageFunctions, options, pathDelimi
220
220
  * @param {CSN.Element} assoc
221
221
  * @param {string} originalAssocName
222
222
  * @param {string} elemName
223
+ * @param {CSN.Artifact} art
224
+ * @param {CSN.Path} path
223
225
  * @returns {Array} New on-condition
224
226
  */
225
- function transformDollarSelfComparisonWithManagedAssoc( assocOp, assoc, originalAssocName, elemName ) {
227
+ function transformDollarSelfComparisonWithManagedAssoc( assocOp, assoc, originalAssocName, elemName, art, path) {
226
228
  const conditions = [];
227
229
  // if the element was structured then it was flattened => change of the delimiter from '.' to '_'
228
230
  // this is done in the flattening, but as we do not alter the onCond itself there should be done here as well
@@ -245,7 +247,9 @@ function getBacklinkTransformer( csnUtils, messageFunctions, options, pathDelimi
245
247
  if (prepend$self)
246
248
  a[1].ref = [ '$self', ...a[1].ref ];
247
249
 
248
-
250
+ // Not without a2j so we can rely on a certain model state
251
+ if (doA2J && prepend$self && art.elements[k.ref[1]] || !prepend$self && !art.elements[k.ref[0]])
252
+ messageFunctions.message('ref-missing-self-counterpart', path, { prop: k.ref[0], name: assocName });
249
253
  conditions.push([ a[0], '=', a[1] ]);
250
254
  });
251
255
 
@@ -268,9 +272,11 @@ function getBacklinkTransformer( csnUtils, messageFunctions, options, pathDelimi
268
272
  * @param {CSN.Element} assoc
269
273
  * @param {string} originalAssocName
270
274
  * @param {string} elemName
275
+ * @param {CSN.Artifact} art
276
+ * @param {CSN.Path} path
271
277
  * @returns {Array} New on-condition
272
278
  */
273
- function transformDollarSelfComparisonWithUnmanagedAssoc( assocOp, assoc, originalAssocName, elemName ) {
279
+ function transformDollarSelfComparisonWithUnmanagedAssoc( assocOp, assoc, originalAssocName, elemName, art, path ) {
274
280
  // if the element was structured then it may have been flattened => change of the delimiter from '.' to '_'
275
281
  // this is done in the flattening, but as we do not alter the onCond itself there should be done here as well
276
282
  elemName = elemName.replace(/\./g, pathDelimiter);
@@ -279,11 +285,14 @@ function getBacklinkTransformer( csnUtils, messageFunctions, options, pathDelimi
279
285
  const newOnCond = cloneCsnNonDict(assoc.on, options);
280
286
  applyTransformationsOnNonDictionary({ on: newOnCond }, 'on', {
281
287
  ref: (parent, prop, ref) => {
288
+ let sourceSide = false;
282
289
  // we are in the "path" from the forwarding assoc => need to remove the first part of the path
283
290
  if (ref[0] === assocName) {
284
291
  ref.shift();
285
292
  if (prepend$self)
286
293
  ref.unshift('$self');
294
+
295
+ sourceSide = true;
287
296
  }
288
297
  else if (ref.length > 1 && ref[0] === '$self' && ref[1] === assocName) {
289
298
  // We could also have a $self in front of the assoc name - so we would need to shift twice
@@ -291,6 +300,8 @@ function getBacklinkTransformer( csnUtils, messageFunctions, options, pathDelimi
291
300
  ref.shift();
292
301
  if (prepend$self)
293
302
  ref.unshift('$self');
303
+
304
+ sourceSide = true;
294
305
  }
295
306
  else { // we are in the backlink assoc "path" => need to push at the beginning the association's id
296
307
  ref.unshift(elemName);
@@ -299,6 +310,10 @@ function getBacklinkTransformer( csnUtils, messageFunctions, options, pathDelimi
299
310
  if (ref[1] === '$self')
300
311
  ref.splice(1, 1);
301
312
  }
313
+
314
+ // Not without a2j so we can rely on a certain model state
315
+ if (doA2J && sourceSide && (prepend$self && !art.elements[ref[1]] || !prepend$self && !art.elements[ref[0]]))
316
+ messageFunctions.message('ref-missing-self-counterpart', path, { '#': 'unmanaged', prop: ref[0], name: assocName });
302
317
  },
303
318
  });
304
319
  return newOnCond;
@@ -1,5 +1,7 @@
1
1
  'use strict';
2
2
 
3
+ const { sqlServiceAnnotation } = require('./processSqlServices');
4
+
3
5
  const requiredAnnos = {
4
6
  '@cds.persistence.skip': true,
5
7
  '@cds.persistence.exists': true,
@@ -22,6 +24,7 @@ const requiredAnnos = {
22
24
  '@cds.autoexposed': true,
23
25
  '@cds.redirection.target': true,
24
26
  '@Core.Computed': true,
27
+ [sqlServiceAnnotation]: true,
25
28
  };
26
29
 
27
30
  /**
@@ -0,0 +1,63 @@
1
+ 'use strict';
2
+
3
+ const { setProp } = require('../../base/model');
4
+
5
+ const sqlServiceAnnotation = '@protocol';
6
+ // Problem: How can we clone a Symbol when sorting?
7
+ // const sqlServiceEntities = Symbol.for('SQL Service enabled entities');
8
+ /**
9
+ * Find all entities in SQL services and mark them with an annotation and
10
+ * remember them in a symbol property for easier processing in toSql-rendering.
11
+ *
12
+ * @param {CSN.Model} csn
13
+ * @returns {Function}
14
+ */
15
+ function processSqlServices(csn) {
16
+ setProp(csn, '$sqlServiceEntities', Object.create(null));
17
+ return function findAndMarkSqlServiceArtifacts(artifact, artifactName) {
18
+ const sqlServiceName = isEntityInSqlService(artifact, artifactName, csn);
19
+ if (sqlServiceName?.length > 0)
20
+ setProp(artifact, '$sqlService', sqlServiceName);
21
+ };
22
+ }
23
+
24
+ /**
25
+ *
26
+ * @param {CSN.Artifact} artifact
27
+ * @returns {boolean}
28
+ */
29
+ function isSqlService(artifact) {
30
+ return artifact.kind === 'service' && artifact[sqlServiceAnnotation] === 'sql';
31
+ }
32
+ /**
33
+ *
34
+ * @param {CSN.Artifact} artifact
35
+ * @param {string} artifactName
36
+ * @param {CSN.Model} csn
37
+ * @returns {string|null}
38
+ */
39
+ function isEntityInSqlService(artifact, artifactName, csn) {
40
+ if (artifact.kind !== 'entity' || !artifactName.includes('.'))
41
+ return null;
42
+
43
+ const nameParts = artifactName.split('.');
44
+ for (let i = nameParts.length; i >= 0; i--) {
45
+ const possibleServiceName = nameParts.slice(0, i).join('.');
46
+ if (!csn.definitions[possibleServiceName])
47
+ continue;
48
+
49
+ const definition = csn.definitions[possibleServiceName];
50
+ if (isSqlService(definition))
51
+ return possibleServiceName;
52
+
53
+ // We don't allow nested services/contexts - if we find one, we don't need to keep searching
54
+ if (definition.kind === 'service' || definition.kind === 'context')
55
+ return null;
56
+ }
57
+
58
+ return null;
59
+ }
60
+
61
+ module.exports = {
62
+ processSqlServices, isSqlService, sqlServiceAnnotation,
63
+ };
@@ -230,14 +230,19 @@ function generateDrafts( csn, options, services, messageFunctions ) {
230
230
 
231
231
  function $draft2$self(member) {
232
232
  Object.keys(member).forEach(pn => {
233
- if(pn[0] === '@')
233
+ if(pn[0] === '@') {
234
+ let refChanged = false;
234
235
  transformExpression(member, pn,{
235
236
  ref: (_parent, _prop, xpr, _path) => {
236
237
  if(xpr[0] === '$draft') {
237
238
  xpr[0] = '$self';
239
+ refChanged = true;
238
240
  }
239
241
  }
240
242
  });
243
+ if (refChanged)
244
+ member[pn]['='] = true;
245
+ }
241
246
  });
242
247
  }
243
248
 
@@ -34,6 +34,7 @@ const backlinks = require('./db/backlinks');
34
34
  const { getDefaultTypeLengths } = require('../render/utils/common');
35
35
  const { featureFlags } = require('./db/featureFlags');
36
36
  const { cloneCsnNonDict, cloneFullCsn } = require('../model/cloneCsn');
37
+ const { processSqlServices } = require('./db/processSqlServices');
37
38
 
38
39
  // By default: Do not process non-entities/views
39
40
  function forEachDefinition(csn, cb) {
@@ -368,6 +369,8 @@ function transformForRelationalDBWithCsn(csn, options, messageFunctions) {
368
369
  removeKeyPropInType,
369
370
  ]);
370
371
 
372
+ // TODO: Might have to do this earlier if we want special rendering for projections?
373
+ const findAndMarkSqlServiceArtifacts = options.sqlDialect === 'hana' && options.src === 'hdi' && csn.meta?.[featureFlags]?.$sqlService ? processSqlServices(csn): () => {}
371
374
 
372
375
  // TODO: Could we maybe merge this with the final applyTransformations?
373
376
  applyTransformations(csn, {
@@ -387,6 +390,8 @@ function transformForRelationalDBWithCsn(csn, options, messageFunctions) {
387
390
  // Attach @cds.persistence.name to artifacts
388
391
  if (!artifact.$ignore && artifact.kind !== 'service' && artifact.kind !== 'context')
389
392
  csnUtils.addStringAnnotationTo('@cds.persistence.name', getArtifactDatabaseNameOf(artifactName, options.sqlMapping, csn, options.sqlDialect), artifact);
393
+
394
+ findAndMarkSqlServiceArtifacts(artifact, artifactName);
390
395
  }], { allowArtifact: artifact => artifact.kind === 'entity'});
391
396
 
392
397
  throwWithAnyError();
@@ -700,6 +705,10 @@ function transformForRelationalDBWithCsn(csn, options, messageFunctions) {
700
705
  newCsn.definitions[artName].technicalConfig = art.technicalConfig;
701
706
 
702
707
  });
708
+
709
+ // To ensure we preserve feature flags
710
+ newCsn.meta = csn.meta;
711
+
703
712
  csn = newCsn;
704
713
  }
705
714
 
package/lib/utils/file.js CHANGED
@@ -4,6 +4,8 @@
4
4
 
5
5
  const fs = require('fs');
6
6
  const util = require('util');
7
+ const path = require('path');
8
+ const os = require('os');
7
9
 
8
10
  /**
9
11
  * Split the given source string into its lines. Respects Unix,
@@ -16,6 +18,74 @@ function splitLines( src ) {
16
18
  return src.split(/\r\n?|\n/);
17
19
  }
18
20
 
21
+ /**
22
+ * Returns the file's normalized extension, e.g. for `file.CDS` -> `cds`
23
+ * Returns null if the given filename is not a string.
24
+ *
25
+ * @param filename
26
+ * @returns {null|string}
27
+ */
28
+ function fileExtension( filename ) {
29
+ if (typeof filename === 'string')
30
+ return path.extname( filename ).slice(1).toLowerCase();
31
+ return null;
32
+ }
33
+
34
+ /**
35
+ * Create a temporary file path using the system's temporary folder and a filename
36
+ * consisting of the given name/extension and a random string.
37
+ *
38
+ * @param {string} name
39
+ * @param {string} extension
40
+ * @returns {string}
41
+ */
42
+ function tmpFilePath( name, extension ) {
43
+ const crypto = require('crypto');
44
+ const id = crypto.randomBytes(32).toString('hex');
45
+ const filename = `${ name }-${ id }.${ extension }`;
46
+ return path.join(os.tmpdir(), filename);
47
+ }
48
+
49
+ /**
50
+ * Read input from the given stream.
51
+ * See https://nodejs.org/api/stream.html#readablereadsize
52
+ *
53
+ * @returns {Promise<string>}
54
+ */
55
+ function readStream( stream ) {
56
+ return new Promise((resolve, reject) => {
57
+ const chunks = [];
58
+
59
+ const listeners = {
60
+ __proto__: null,
61
+ data: onData,
62
+ error: onError,
63
+ end: onEnd,
64
+ };
65
+ for (const name in listeners)
66
+ stream.on(name, listeners[name]);
67
+
68
+ function onData( chunk ) {
69
+ chunks.push(chunk);
70
+ }
71
+
72
+ function onEnd() {
73
+ removeListeners();
74
+ resolve(chunks.join(''));
75
+ }
76
+
77
+ function onError( error ) {
78
+ removeListeners();
79
+ reject(error);
80
+ }
81
+
82
+ function removeListeners() {
83
+ for (const name in listeners)
84
+ stream.removeListener(name, listeners[name]);
85
+ }
86
+ });
87
+ }
88
+
19
89
  /**
20
90
  * Returns filesystem utils readFile(), isFile(), realpath() for _CDS_ usage.
21
91
  * This includes a trace as well as usage of a file cache.
@@ -61,18 +131,18 @@ function cdsFs( fileCache, enableTrace ) {
61
131
  };
62
132
 
63
133
 
64
- function realpathSync( path, cb ) {
134
+ function realpathSync( filepath, cb ) {
65
135
  try {
66
- cb(null, fs.realpathSync(path));
136
+ cb(null, fs.realpathSync(filepath));
67
137
  }
68
138
  catch (err) {
69
139
  cb(err, null);
70
140
  }
71
141
  }
72
142
 
73
- function realpathSyncNative( path, cb ) {
143
+ function realpathSyncNative( filepath, cb ) {
74
144
  try {
75
- cb(null, fs.realpathSync.native(path));
145
+ cb(null, fs.realpathSync.native(filepath));
76
146
  }
77
147
  catch (err) {
78
148
  cb(err, null);
@@ -194,5 +264,8 @@ function cdsFs( fileCache, enableTrace ) {
194
264
 
195
265
  module.exports = {
196
266
  splitLines,
267
+ readStream,
268
+ fileExtension,
269
+ tmpFilePath,
197
270
  cdsFs,
198
271
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sap/cds-compiler",
3
- "version": "5.0.6",
3
+ "version": "5.1.2",
4
4
  "description": "CDS (Core Data Services) compiler and backends",
5
5
  "homepage": "https://cap.cloud.sap/",
6
6
  "author": "SAP SE (https://www.sap.com)",