@cap-js/cds-typer 0.12.0 → 0.14.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,18 @@ 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.15.0 - TBD
8
+
9
+ ## Version 0.14.0 - 2023-12-13
10
+ ### Added
11
+ - Entities that are database views now also receive typings
12
+
13
+ ## Version 0.13.0 - 2023-12-06
14
+ ### Changes
15
+ - 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
16
+
17
+ ### Added
18
+ - The `excluding` clause in projections now actually excludes the specified properties in the generated types
8
19
 
9
20
  ## Version 0.12.0 - 2023-11-23
10
21
 
@@ -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/csn.js CHANGED
@@ -226,4 +226,18 @@ function amendCSN(csn) {
226
226
  propagateForeignKeys(csn)
227
227
  }
228
228
 
229
- module.exports = { amendCSN }
229
+ /**
230
+ * FIXME: this is pretty handwavey: we are looking for view-entities,
231
+ * i.e. ones that have a query, but are not a cds level projection.
232
+ * Those are still not expanded and we have to retrieve their definition
233
+ * with all properties from the inferred model.
234
+ */
235
+ const isView = entity => entity.query && !entity.projection
236
+
237
+ /**
238
+ * @see isView
239
+ * Unresolved entities have to be looked up from inferred csn.
240
+ */
241
+ const isUnresolved = entity => entity._unresolved === true
242
+
243
+ module.exports = { amendCSN, isView, isUnresolved }
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
@@ -2,14 +2,14 @@
2
2
 
3
3
  const util = require('./util')
4
4
 
5
- const { amendCSN } = require('./csn')
5
+ const { amendCSN, isView, isUnresolved } = require('./csn')
6
6
  // eslint-disable-next-line no-unused-vars
7
7
  const { SourceFile, baseDefinitions, Buffer } = require('./file')
8
8
  const { FlatInlineDeclarationResolver, StructuredInlineDeclarationResolver } = require('./components/inline')
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 */
@@ -99,10 +99,12 @@ class Visitor {
99
99
  */
100
100
  visitDefinitions() {
101
101
  for (const [name, entity] of Object.entries(this.csn.xtended.definitions)) {
102
- if (entity._unresolved === true) {
103
- this.logger.error(`Skipping unresolved entity: ${JSON.stringify(entity)}`)
104
- } else {
102
+ if (isView(entity)) {
103
+ this.visitEntity(name, this.csn.inferred.definitions[name])
104
+ } else if (!isUnresolved(entity)) {
105
105
  this.visitEntity(name, entity)
106
+ } else {
107
+ this.logger.warning(`Skipping unresolved entity: ${name}`)
106
108
  }
107
109
  }
108
110
  // FIXME: optimise
@@ -156,7 +158,9 @@ class Visitor {
156
158
  buffer.indent()
157
159
 
158
160
  const enums = []
159
- for (const [ename, element] of Object.entries(entity.elements ?? {})) {
161
+ const exclusions = new Set(entity.projection?.excluding ?? [])
162
+ const elements = Object.entries(entity.elements ?? {}).filter(([ename]) => !exclusions.has(ename))
163
+ for (const [ename, element] of elements) {
160
164
  this.visitElement(ename, element, file, buffer)
161
165
 
162
166
  // make foreign keys explicit
@@ -180,8 +184,8 @@ class Visitor {
180
184
 
181
185
  buffer.indent()
182
186
  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}))
187
+ buffer.add(`static ${e.name} = ${propertyToInlineEnumName(clean, e.name)}`)
188
+ file.addInlineEnum(clean, name, e.name, csnToEnumPairs(e, {unwrapVals: true}))
185
189
  }
186
190
  buffer.add('static actions: {')
187
191
  buffer.indent()
@@ -341,7 +345,7 @@ class Visitor {
341
345
  const ns = this.resolver.resolveNamespace(name.split('.'))
342
346
  const file = this.getNamespaceFile(ns)
343
347
  if ('enum' in type) {
344
- file.addEnum(name, clean, csnToEnum(type))
348
+ file.addEnum(name, clean, csnToEnumPairs(type))
345
349
  } else {
346
350
  // alias
347
351
  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.14.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",