@cap-js/cds-typer 0.12.0 → 0.13.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.
package/CHANGELOG.md CHANGED
@@ -4,7 +4,14 @@ All notable changes to this project will be documented in this file.
4
4
  This project adheres to [Semantic Versioning](http://semver.org/).
5
5
  The format is based on [Keep a Changelog](http://keepachangelog.com/).
6
6
 
7
- ## Version 0.13.0 - TBD
7
+ ## Version 0.14.0 - TBD
8
+
9
+ ## Version 0.13.0 - 2023-12-06
10
+ ### Changes
11
+ - Enums are now generated ecplicitly in the respective _index.js_ files and don't have to extract their values from the model at runtime anymore
12
+
13
+ ### Added
14
+ - The `excluding` clause in projections now actually excludes the specified properties in the generated types
8
15
 
9
16
  ## Version 0.12.0 - 2023-11-23
10
17
 
@@ -31,8 +31,8 @@ function printEnum(buffer, name, kvs, options = {}) {
31
31
  buffer.indent()
32
32
  const vals = new Set()
33
33
  for (const [k, v] of kvs) {
34
- buffer.add(`${k}: ${JSON.stringify(v)},`)
35
- vals.add(JSON.stringify(v.val ?? v)) // in case of wrapped vals we need to unwrap here for the type
34
+ buffer.add(`${k}: ${v},`)
35
+ vals.add(v?.val ?? v) // in case of wrapped vals we need to unwrap here for the type
36
36
  }
37
37
  buffer.outdent()
38
38
  buffer.add('} as const;')
@@ -41,27 +41,32 @@ function printEnum(buffer, name, kvs, options = {}) {
41
41
  }
42
42
 
43
43
  // in case of strings, wrap in quotes and fallback to key to make sure values are attached for every key
44
- const enumVal = (key, value, enumType) => enumType === 'cds.String' ? `${value ?? key}` : value
44
+ const enumVal = (key, value, enumType) => enumType === 'cds.String' ? JSON.stringify(`${value ?? key}`) : value
45
45
 
46
46
  /**
47
+ * Converts a CSN type describing an enum into a list of kv-pairs.
48
+ * Values from CSN are unwrapped from their `.val` structure and
49
+ * will fall back to the key if no value is provided.
50
+ *
47
51
  * @param {{enum: {[key: name]: string}, type: string}} enumCsn
48
52
  * @param {{unwrapVals: boolean}} options if `unwrapVals` is passed,
49
53
  * then the CSN structure `{val:x}` is flattened to just `x`.
50
54
  * Retaining `val` is closer to the actual CSN structure and should be used where we want
51
- * to mimic the runtime as closely as possible (anoymous enum types).
55
+ * to mimic the runtime as closely as possible (inline enum types).
52
56
  * Stripping that additional wrapper would be more readable for users.
57
+ *
53
58
  * @example
54
59
  * ```ts
55
- * const csn = {enum: {x: {val: 42}, y: {val: -42}}}
56
- * csnToEnum(csn) // -> [['x', 42], ['y': -42]]
57
- * csnToEnum(csn, {unwrapVals: false}) // -> [['x', {val:42}], ['y': {val:-42}]]
60
+ * const csn = {enum: {X: {val: 'a'}, Y: {val: 'b'}, Z: {}}}
61
+ * csnToEnumPairs(csn) // -> [['X', 'a'], ['Y': 'b'], ['Z': 'Z']]
62
+ * csnToEnumPairs(csn, {unwrapVals: false}) // -> [['X', {val:'a'}], ['Y': {val:'b'}], ['Z':'Z']]
58
63
  * ```
59
64
  */
60
- const csnToEnum = ({enum: enm, type}, options = {}) => {
65
+ const csnToEnumPairs = ({enum: enm, type}, options = {}) => {
61
66
  options = {...{unwrapVals: true}, ...options}
62
67
  return Object.entries(enm).map(([k, v]) => {
63
68
  const val = enumVal(k, v.val, type)
64
- return [k, options.unwrapVals ? val : { val }]
69
+ return [k, (options.unwrapVals ? val : { val })]
65
70
  })
66
71
  }
67
72
 
@@ -69,7 +74,7 @@ const csnToEnum = ({enum: enm, type}, options = {}) => {
69
74
  * @param {string} entity
70
75
  * @param {string} property
71
76
  */
72
- const propertyToAnonymousEnumName = (entity, property) => `${entity}_${property}`
77
+ const propertyToInlineEnumName = (entity, property) => `${entity}_${property}`
73
78
 
74
79
  /**
75
80
  * A type is considered to be an inline enum, iff it has a `.enum` property
@@ -82,27 +87,30 @@ const propertyToAnonymousEnumName = (entity, property) => `${entity}_${property}
82
87
  */
83
88
  const isInlineEnumType = (element, csn) => element.enum && !(element.type in csn.definitions)
84
89
 
85
- const stringifyEnumImplementation = (name, enm) => `module.exports.${name} = Object.fromEntries(Object.entries(${enm}).map(([k,v]) => [k,v.val??k]))`
86
-
87
- /**
88
- * @param {string} name
89
- * @param {string} fq
90
- * @returns {string}
91
- */
92
- const stringifyNamedEnum = (name, fq) => stringifyEnumImplementation(name, `cds.model.definitions['${fq}'].enum`)
93
90
  /**
91
+ * Stringifies an enum into a runtime artifact.
92
+ * ```cds
93
+ * type Language: String enum {
94
+ * DE = "German";
95
+ * EN = "English";
96
+ * FR;
97
+ * }
98
+ * ```
99
+ * becomes
100
+ *
101
+ * ```js
102
+ * module.exports.Language = { DE: "German", EN: "English", FR: "FR" }
103
+ * ```
94
104
  * @param {string} name
95
- * @param {string} fq
96
- * @param {string} property
97
- * @returns {string}
105
+ * @param {[string, string][]} kvs a list of key-value pairs. Values that are falsey are replaced by
98
106
  */
99
- const stringifyAnonymousEnum = (name, fq, property) => stringifyEnumImplementation(fq, `cds.model.definitions['${name}'].elements.${property}.enum`)
107
+ const stringifyEnumImplementation = (name, kvs) => `module.exports.${name} = { ${kvs.map(([k,v]) => `${k}: ${v}`).join(', ')} }`
108
+
100
109
 
101
110
  module.exports = {
102
111
  printEnum,
103
- csnToEnum,
104
- propertyToAnonymousEnumName,
112
+ csnToEnumPairs,
113
+ propertyToInlineEnumName,
105
114
  isInlineEnumType,
106
- stringifyNamedEnum,
107
- stringifyAnonymousEnum
115
+ stringifyEnumImplementation
108
116
  }
@@ -2,10 +2,10 @@
2
2
 
3
3
  const util = require('../util')
4
4
  // eslint-disable-next-line no-unused-vars
5
- const { Buffer, SourceFile, Path, Library, baseDefinitions } = require("../file")
5
+ const { Buffer, SourceFile, Path, Library, baseDefinitions } = require('../file')
6
6
  const { deepRequire, createToManyAssociation, createToOneAssociation, createArrayOf, createCompositionOfMany, createCompositionOfOne } = require('./wrappers')
7
- const { StructuredInlineDeclarationResolver } = require("./inline")
8
- const { isInlineEnumType, propertyToAnonymousEnumName } = require('./enum')
7
+ const { StructuredInlineDeclarationResolver } = require('./inline')
8
+ const { isInlineEnumType, propertyToInlineEnumName } = require('./enum')
9
9
 
10
10
  /** @typedef {{ cardinality?: { max?: '*' | number } }} EntityCSN */
11
11
  /** @typedef {{ definitions?: Object<string, EntityCSN> }} CSN */
@@ -374,7 +374,7 @@ class Resolver {
374
374
  // we use the singular as the initial declaration of these enums takes place
375
375
  // while defining the singular class. Which therefore uses the singular over the plural name.
376
376
  const cleanEntityName = util.singular4(element.parent, true)
377
- const enumName = propertyToAnonymousEnumName(cleanEntityName, element.name)
377
+ const enumName = propertyToInlineEnumName(cleanEntityName, element.name)
378
378
  result.type = enumName
379
379
  result.plainName = enumName
380
380
  result.isInlineDeclaration = true
package/lib/file.js CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  const fs = require('fs').promises
4
4
  const { readFileSync } = require('fs')
5
- const { printEnum, stringifyNamedEnum, stringifyAnonymousEnum, propertyToAnonymousEnumName } = require('./components/enum')
5
+ const { printEnum, propertyToInlineEnumName, stringifyEnumImplementation } = require('./components/enum')
6
6
  const path = require('path')
7
7
 
8
8
  const AUTO_GEN_NOTE = "// This is an automatically generated file. Please do not change its contents manually!"
@@ -104,8 +104,8 @@ class SourceFile extends File {
104
104
  this.events = { buffer: new Buffer(), fqs: []}
105
105
  /** @type {Buffer} */
106
106
  this.types = new Buffer()
107
- /** @type {{ buffer: Buffer, fqs: {name: string, fq: string, property?: string}[]}} */
108
- this.enums = { buffer: new Buffer(), fqs: [] }
107
+ /** @type {{ buffer: Buffer, data: {kvs: [string[]], name: string, fq: string, property?: string}[]}} */
108
+ this.enums = { buffer: new Buffer(), data: [] }
109
109
  /** @type {{ buffer: Buffer }} */
110
110
  this.inlineEnums = { buffer: new Buffer() }
111
111
  /** @type {Buffer} */
@@ -226,28 +226,28 @@ class SourceFile extends File {
226
226
  * @param {string} fq fully qualified name of the enum (entity name within CSN)
227
227
  * @param {string} name local name of the enum
228
228
  * @param {[string, string][]} kvs list of key-value pairs
229
- * @param {string} [property] property to which the enum is attached.
230
- * If given, the enum is considered to be an anonymous inline definition of an enum.
229
+ * @param {string} [property] property to which the enum is attached.
230
+ * If given, the enum is considered to be an inline definition of an enum.
231
231
  * If not, it is considered to be regular, named enum.
232
232
  */
233
233
  addEnum(fq, name, kvs) {
234
- this.enums.fqs.push({ name, fq })
234
+ this.enums.data.push({ name, fq, kvs })
235
235
  printEnum(this.enums.buffer, name, kvs)
236
236
  }
237
237
 
238
238
  /**
239
- * Adds an anonymous enum to this file.
239
+ * Adds an inline enum to this file.
240
240
  * @param {string} entityCleanName name of the entity the enum is attached to without namespace
241
241
  * @param {string} entityFqName name of the entity the enum is attached to with namespace
242
242
  *
243
243
  * @param {string} propertyName property to which the enum is attached.
244
244
  * @param {[string, string][]} kvs list of key-value pairs
245
- * If given, the enum is considered to be an anonymous inline definition of an enum.
245
+ * If given, the enum is considered to be an inline definition of an enum.
246
246
  * If not, it is considered to be regular, named enum.
247
247
  *
248
248
  * @example
249
249
  * ```js
250
- * addAnonymousEnum('Books.genre', 'Books', 'genre', [['horror','horror']])
250
+ * addInlineEnum('Books.genre', 'Books', 'genre', [['horror','horror']])
251
251
  * ```
252
252
  * generates
253
253
  * ```js
@@ -266,13 +266,14 @@ class SourceFile extends File {
266
266
  * }
267
267
  * ```
268
268
  */
269
- addAnonymousEnum(entityCleanName, entityFqName, propertyName, kvs) {
270
- this.enums.fqs.push({
269
+ addInlineEnum(entityCleanName, entityFqName, propertyName, kvs) {
270
+ this.enums.data.push({
271
271
  name: entityFqName,
272
272
  property: propertyName,
273
+ kvs,
273
274
  fq: `${entityCleanName}.${propertyName}`
274
275
  })
275
- printEnum(this.inlineEnums.buffer, propertyToAnonymousEnumName(entityCleanName, propertyName), kvs, {export: false})
276
+ printEnum(this.inlineEnums.buffer, propertyToInlineEnumName(entityCleanName, propertyName), kvs, {export: false})
276
277
  }
277
278
 
278
279
  /**
@@ -410,9 +411,7 @@ class SourceFile extends File {
410
411
  .concat(['// actions'])
411
412
  .concat(this.actions.names.map(name => `module.exports.${name} = '${name}'`))
412
413
  .concat(['// enums'])
413
- .concat(this.enums.fqs.map(({name, fq, property}) => property
414
- ? stringifyAnonymousEnum(name, fq, property)
415
- : stringifyNamedEnum(name, fq)))
414
+ .concat(this.enums.data.map(({name, kvs}) => stringifyEnumImplementation(name, kvs)))
416
415
  .join('\n') + '\n'
417
416
  }
418
417
  }
@@ -608,6 +607,7 @@ const writeout = async (root, sources) =>
608
607
  } catch (err) {
609
608
  // eslint-disable-next-line no-console
610
609
  console.error(`Could not create parent directory ${dir}: ${err}.`)
610
+ console.error(err.stack)
611
611
  }
612
612
  return dir
613
613
  })
package/lib/visitor.js CHANGED
@@ -9,7 +9,7 @@ const { FlatInlineDeclarationResolver, StructuredInlineDeclarationResolver } = r
9
9
  const { Resolver } = require('./components/resolver')
10
10
  const { Logger } = require('./logging')
11
11
  const { docify } = require('./components/wrappers')
12
- const { csnToEnum, propertyToAnonymousEnumName, isInlineEnumType } = require('./components/enum')
12
+ const { csnToEnumPairs, propertyToInlineEnumName, isInlineEnumType } = require('./components/enum')
13
13
 
14
14
  /** @typedef {import('./file').File} File */
15
15
  /** @typedef {{ entity: String }} Context */
@@ -156,7 +156,9 @@ class Visitor {
156
156
  buffer.indent()
157
157
 
158
158
  const enums = []
159
- for (const [ename, element] of Object.entries(entity.elements ?? {})) {
159
+ const exclusions = new Set(entity.projection?.excluding ?? [])
160
+ const elements = Object.entries(entity.elements ?? {}).filter(([ename]) => !exclusions.has(ename))
161
+ for (const [ename, element] of elements) {
160
162
  this.visitElement(ename, element, file, buffer)
161
163
 
162
164
  // make foreign keys explicit
@@ -180,8 +182,8 @@ class Visitor {
180
182
 
181
183
  buffer.indent()
182
184
  for (const e of enums) {
183
- buffer.add(`static ${e.name} = ${propertyToAnonymousEnumName(clean, e.name)}`)
184
- file.addAnonymousEnum(clean, name, e.name, csnToEnum(e, {unwrapVals: true}))
185
+ buffer.add(`static ${e.name} = ${propertyToInlineEnumName(clean, e.name)}`)
186
+ file.addInlineEnum(clean, name, e.name, csnToEnumPairs(e, {unwrapVals: true}))
185
187
  }
186
188
  buffer.add('static actions: {')
187
189
  buffer.indent()
@@ -341,7 +343,7 @@ class Visitor {
341
343
  const ns = this.resolver.resolveNamespace(name.split('.'))
342
344
  const file = this.getNamespaceFile(ns)
343
345
  if ('enum' in type) {
344
- file.addEnum(name, clean, csnToEnum(type))
346
+ file.addEnum(name, clean, csnToEnumPairs(type))
345
347
  } else {
346
348
  // alias
347
349
  file.addType(name, clean, this.resolver.resolveAndRequire(type, file).typeName)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cap-js/cds-typer",
3
- "version": "0.12.0",
3
+ "version": "0.13.0",
4
4
  "description": "Generates .ts files for a CDS model to receive code completion in VS Code",
5
5
  "main": "index.js",
6
6
  "repository": "github:cap-js/cds-typer",