@cap-js/cds-typer 0.22.0 → 0.23.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,12 +4,20 @@ 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.23.0 - TBD
7
+ ## Version 0.24.0 - TBD
8
+
9
+ ## Version 0.23.0 - 2024-07-04
10
+ ### Fixed
11
+ - Plurals no longer have `is_singular` attached in the resulting .js files
12
+ - Properties are properly propagated beyond just one level of inheritance
8
13
 
9
14
  ## Version 0.22.0 - 2024-06-20
10
15
  ### Fixed
11
16
  - Fixed a bug where keys would sometimes inconsistently become nullable
12
17
 
18
+ ### Changed
19
+ - Logging now internally uses `cds.log` and pipes output into the `cds-typer` logger, which can be configured via `cds.env` in addition to explicitly passing a `--logLevel` parameter to CLI. Users now have to use the levels defined in [`cds.log.levels`](https://cap.cloud.sap/docs/node.js/cds-log#log-levels). The formerly valid levels `WARNING`, `CRITICAL`, and `NONE` are now deprecated and automatically mapped to valid levels for now.
20
+
13
21
  ## Version 0.21.2 - 2024-06-06
14
22
  ### Fixed
15
23
  - The typescript build task will no longer attempt to run unless at least cds 8 is installed
package/cds-plugin.js CHANGED
@@ -33,7 +33,7 @@ const rmDirIfExists = dir => {
33
33
  * @param {string[]} exts - The extensions to remove.
34
34
  * @returns {Promise<void>}
35
35
  */
36
- const rmFiles = async (dir, exts) => fs.existsSync(dir)
36
+ const rmFiles = async (dir, exts) => fs.existsSync(dir)
37
37
  ? Promise.all(
38
38
  (await readdir(dir))
39
39
  .map(async file => {
@@ -66,7 +66,7 @@ cds.build?.register?.('typescript', class extends cds.build.Plugin {
66
66
  get #modelDirectoryName () {
67
67
  try {
68
68
  // expected format: { '#cds-models/*': [ './@cds-models/*/index.ts' ] }
69
- // ^^^^^^^^^^^^^^^
69
+ // ^^^^^^^^^^^^^^^
70
70
  // relevant part - may be changed by user
71
71
  const config = JSON.parse(fs.readFileSync ('tsconfig.json', 'utf8'))
72
72
  const alias = config.compilerOptions.paths['#cds-models/*'][0]
@@ -118,8 +118,8 @@ cds.build?.register?.('typescript', class extends cds.build.Plugin {
118
118
  await rmFiles(this.task.dest, ['.js', '.ts'])
119
119
 
120
120
  try {
121
- await (buildConfigExists()
122
- ? this.#buildWithConfig()
121
+ await (buildConfigExists()
122
+ ? this.#buildWithConfig()
123
123
  : this.#buildWithoutConfig()
124
124
  )
125
125
  } catch (error) {
package/lib/cli.js CHANGED
@@ -2,9 +2,10 @@
2
2
  /* eslint-disable no-console */
3
3
  'use strict'
4
4
 
5
+ const cds = require('@sap/cds')
5
6
  const { compileFromFile } = require('./compile')
6
7
  const { parseCommandlineArgs } = require('./util')
7
- const { Levels } = require('./logging')
8
+ const { deprecated, _keyFor } = require('./logging')
8
9
  const path = require('path')
9
10
  const { EOL } = require('node:os')
10
11
 
@@ -21,9 +22,11 @@ const flags = {
21
22
  desc: 'This text.',
22
23
  },
23
24
  logLevel: {
24
- desc: 'Minimum log level that is printed.',
25
- allowed: Object.keys(Levels),
26
- default: Levels.ERROR,
25
+ desc: `Minimum log level that is printed.${EOL}The default is only used if no explicit value is passed${EOL}and there is no configuration passed via cds.env either.`,
26
+ allowed: Object.keys(cds.log.levels).concat(Object.keys(deprecated)),
27
+ allowedHint: Object.keys(cds.log.levels).join(' | '), // FIXME: remove once old levels are faded out
28
+ defaultHint: _keyFor(cds.log.levels.ERROR),
29
+ default: cds?.env?.log?.levels?.['cds-typer'] ?? _keyFor(cds.log.levels.ERROR),
27
30
  },
28
31
  jsConfigPath: {
29
32
  desc: `Path to where the jsconfig.json should be written.${EOL}If specified, ${toolName} will create a jsconfig.json file and${EOL}set it up to restrict property usage in types entities to${EOL}existing properties only.`,
@@ -60,14 +63,16 @@ const help = () => `SYNOPSIS${EOL2}` +
60
63
  .sort()
61
64
  .map(([key, value]) => {
62
65
  let s = indent(`--${key}`, ' ')
63
- if (value.allowed) {
66
+ if (value.allowedHint) {
67
+ s += ` ${value.allowedHint}`
68
+ } else if (value.allowed) {
64
69
  s += `: <${value.allowed.join(' | ')}>`
65
70
  } else if (value.type) {
66
71
  s += `: <${value.type}>`
67
72
  }
68
- if (value.default) {
73
+ if (value.defaultHint || value.default) {
69
74
  s += EOL
70
- s += indent(`(default: ${value.default})`, ' ')
75
+ s += indent(`(default: ${value.defaultHint ?? value.default})`, ' ')
71
76
  }
72
77
  s += `${EOL2}${indent(value.desc, ' ')}`
73
78
  return s
@@ -92,10 +97,16 @@ const main = async args => {
92
97
  if (args.named.jsConfigPath && !args.named.jsConfigPath.endsWith('jsconfig.json')) {
93
98
  args.named.jsConfigPath = path.join(args.named.jsConfigPath, 'jsconfig.json')
94
99
  }
100
+ const newLogLevel = deprecated[args.named.logLevel]
101
+ if (newLogLevel) {
102
+ console.warn(`deprecated log level '${args.named.logLevel}', use '${newLogLevel}' instead (changing this automatically for now).`)
103
+ args.named.logLevel = newLogLevel
104
+ }
105
+
95
106
  compileFromFile(args.positional, {
96
107
  // temporary fix until rootDir is faded out
97
108
  outputDirectory: [args.named.outputDirectory, args.named.rootDir].find(d => d !== './') ?? './',
98
- logLevel: Levels[args.named.logLevel] ?? args.named.logLevel,
109
+ logLevel: args.named.logLevel,
99
110
  jsConfigPath: args.named.jsConfigPath,
100
111
  inlineDeclarations: args.named.inlineDeclarations,
101
112
  propertiesOptional: args.named.propertiesOptional === 'true',
package/lib/compile.js CHANGED
@@ -5,21 +5,20 @@ const { normalize } = require('path')
5
5
  const cds = require(require.resolve('@sap/cds', { paths: [process.cwd(), __dirname] }))
6
6
  const util = require('./util')
7
7
  const { writeout } = require('./file')
8
- const { Logger } = require('./logging')
9
8
  const { Visitor } = require('./visitor')
9
+ const { LOG, setLevel } = require('./logging')
10
10
 
11
11
  /**
12
- * @typedef {import('./visitor').CompileParameters} CompileParameters
12
+ * @typedef {import('./typedefs').visitor.CompileParameters} CompileParameters
13
13
  */
14
14
 
15
15
  /**
16
16
  * Writes the accompanying jsconfig.json file to the specified paths.
17
17
  * Tries to merge nicely if an existing file is found.
18
18
  * @param {string} path - filepath to jsconfig.json.
19
- * @param {import('./logging').Logger} logger - logger
20
19
  * @private
21
20
  */
22
- const writeJsConfig = (path, logger) => {
21
+ const writeJsConfig = path => {
23
22
  let values = {
24
23
  compilerOptions: {
25
24
  checkJs: true,
@@ -29,7 +28,7 @@ const writeJsConfig = (path, logger) => {
29
28
  if (fs.existsSync(path)) {
30
29
  const currentContents = JSON.parse(fs.readFileSync(path))
31
30
  if (currentContents?.compilerOptions?.checkJs) {
32
- logger.warning(`jsconfig at location ${path} already exists. Attempting to merge.`)
31
+ LOG.warn(`jsconfig at location ${path} already exists. Attempting to merge.`)
33
32
  }
34
33
  util.deepMerge(currentContents, values)
35
34
  values = currentContents
@@ -40,20 +39,19 @@ const writeJsConfig = (path, logger) => {
40
39
 
41
40
  /**
42
41
  * Compiles a CSN object to Typescript types.
43
- * @param {{xtended: CSN, inferred: CSN}} csn
42
+ * @param {{xtended: CSN, inferred: CSN}} csn - csn tuple
44
43
  * @param {CompileParameters} parameters - path to root directory for all generated files, min log level
45
44
  */
46
45
  const compileFromCSN = async (csn, parameters) => {
47
46
  const envSettings = cds.env?.typer ?? {}
48
47
  parameters = { ...envSettings, ...parameters }
49
- const logger = new Logger()
50
- logger.addFrom(parameters.logLevel)
48
+ setLevel(parameters.logLevel)
51
49
  if (parameters.jsConfigPath) {
52
- writeJsConfig(parameters.jsConfigPath, logger)
50
+ writeJsConfig(parameters.jsConfigPath)
53
51
  }
54
52
  return writeout(
55
53
  parameters.outputDirectory,
56
- Object.values(new Visitor(csn, parameters, logger).getWriteoutFiles())
54
+ Object.values(new Visitor(csn, parameters).getWriteoutFiles())
57
55
  )
58
56
  }
59
57
 
@@ -3,7 +3,7 @@ const { normalise } = require('./identifier')
3
3
  /**
4
4
  * Extracts all unique values from a list of enum key-value pairs.
5
5
  * If the value is an object, then the `.val` property is used.
6
- * @param {[string, any | {val: any}][]} kvs
6
+ * @param {[string, any | {val: any}][]} kvs - key value pairs
7
7
  */
8
8
  const uniqueValues = kvs => new Set(kvs.map(([,v]) => v?.val ?? v)) // in case of wrapped vals we need to unwrap here for the type
9
9
 
@@ -44,10 +44,10 @@ const enumVal = (key, value, enumType) => enumType === 'cds.String' ? JSON.strin
44
44
  * @param {Buffer} buffer - Buffer to write into
45
45
  * @param {string} name - local name of the enum, i.e. the name under which it should be created in the .ts file
46
46
  * @param {[string, string][]} kvs - list of key-value pairs
47
- * @param {object} options
47
+ * @param {object} options - options for printing the enum
48
48
  */
49
49
  function printEnum(buffer, name, kvs, options = {}) {
50
- const opts = {...{export: true}, ...options}
50
+ const opts = {...{export: true}, ...options}
51
51
  buffer.add('// enum')
52
52
  buffer.addIndentedBlock(`${opts.export ? 'export ' : ''}const ${name} = {`, () =>
53
53
  kvs.forEach(([k, v]) => { buffer.add(`${normalise(k)}: ${v},`) })
@@ -60,14 +60,14 @@ function printEnum(buffer, name, kvs, options = {}) {
60
60
  * Converts a CSN type describing an enum into a list of kv-pairs.
61
61
  * Values from CSN are unwrapped from their `.val` structure and
62
62
  * will fall back to the key if no value is provided.
63
- * @param {{enum: {[key: name]: string}, type: string}} enumCsn
63
+ * @param {{enum: {[key: name]: string}, type: string}} enumCsn - the CSN type describing the enum
64
64
  * @param {{unwrapVals: boolean}} options - if `unwrapVals` is passed,
65
65
  * then the CSN structure `{val:x}` is flattened to just `x`.
66
66
  * Retaining `val` is closer to the actual CSN structure and should be used where we want
67
67
  * to mimic the runtime as closely as possible (inline enum types).
68
68
  * Stripping that additional wrapper would be more readable for users.
69
69
  * @example
70
- * ```ts
70
+ * ```ts
71
71
  * const csn = {enum: {X: {val: 'a'}, Y: {val: 'b'}, Z: {}}}
72
72
  * csnToEnumPairs(csn) // -> [['X', 'a'], ['Y': 'b'], ['Z': 'Z']]
73
73
  * csnToEnumPairs(csn, {unwrapVals: false}) // -> [['X', {val:'a'}], ['Y': {val:'b'}], ['Z':'Z']]
@@ -82,8 +82,8 @@ const csnToEnumPairs = ({enum: enm, type}, options = {}) => {
82
82
  }
83
83
 
84
84
  /**
85
- * @param {string} entity
86
- * @param {string} property
85
+ * @param {string} entity - the entity to which the property belongs
86
+ * @param {string} property - the property name
87
87
  */
88
88
  const propertyToInlineEnumName = (entity, property) => `${entity}_${property}`
89
89
 
@@ -91,8 +91,8 @@ const propertyToInlineEnumName = (entity, property) => `${entity}_${property}`
91
91
  * A type is considered to be an inline enum, iff it has a `.enum` property
92
92
  * _and_ its type is a CDS primitive, i.e. it is not contained in `cds.definitions`.
93
93
  * If it is contained there, then it is a standard enum declaration that has its own name.
94
- * @param {{type: string}} element
95
- * @param {object} csn
94
+ * @param {{type: string}} element - the element to check
95
+ * @param {object} csn - the CSN model
96
96
  * @returns boolean
97
97
  */
98
98
  const isInlineEnumType = (element, csn) => element.enum && !(element.type in csn.definitions)
@@ -107,12 +107,12 @@ const isInlineEnumType = (element, csn) => element.enum && !(element.type in csn
107
107
  * }
108
108
  * ```
109
109
  * becomes
110
- *
110
+ *
111
111
  * ```js
112
112
  * module.exports.Language = { DE: "German", EN: "English", FR: "FR" }
113
113
  * ```
114
- * @param {string} name
115
- * @param {[string, string][]} kvs - a list of key-value pairs. Values that are falsey are replaced by
114
+ * @param {string} name - the enum name
115
+ * @param {[string, string][]} kvs - a list of key-value pairs. Values that are falsey are replaced by
116
116
  */
117
117
  // ??= for inline enums. If there is some static property of that name, we don't want to override it (for example: ".actions"
118
118
  const stringifyEnumImplementation = (name, kvs) => `module.exports.${name} ??= { ${kvs.map(([k,v]) => `${normalise(k)}: ${v}`).join(', ')} }`
@@ -10,6 +10,14 @@ const normalise = ident => ident && !isValidIdent.test(ident)
10
10
  ? `"${ident}"`
11
11
  : ident
12
12
 
13
+ /**
14
+ * Returns the last part of a dot-separated identifier.
15
+ * @param {string} ident - the identifier to extract the last part from
16
+ * @returns {string} the last part of the identifier
17
+ */
18
+ const last = ident => ident.split('.').at(-1)
19
+
13
20
  module.exports = {
14
- normalise
21
+ normalise,
22
+ last
15
23
  }
@@ -2,6 +2,8 @@ const { SourceFile, Buffer } = require('../file')
2
2
  const { normalise } = require('./identifier')
3
3
  const { docify } = require('./wrappers')
4
4
 
5
+ /** @typedef {import('../resolution/resolver').TypeResolveInfo} TypeResolveInfo */
6
+
5
7
  /**
6
8
  * Inline declarations of types can come in different flavours.
7
9
  * The compiler can therefore be adjusted to print out one or the other
@@ -10,20 +12,20 @@ const { docify } = require('./wrappers')
10
12
  */
11
13
  class InlineDeclarationResolver {
12
14
  /**
13
- * @param {string} name
14
- * @param {import('./resolver').TypeResolveInfo} type
15
- * @param {import('../file').Buffer} buffer
16
- * @param {string} statementEnd
15
+ * @param {string} fq - full qualifier of the type
16
+ * @param {TypeResolveInfo} type - type info so far
17
+ * @param {import('../file').Buffer} buffer - the buffer to write into
18
+ * @param {string} statementEnd - statement ending character
17
19
  * @protected
18
20
  * @abstract
19
21
  */
20
22
  // eslint-disable-next-line no-unused-vars
21
- printInlineType(name, type, buffer, statementEnd) { /* abstract */ }
23
+ printInlineType(fq, type, buffer, statementEnd) { /* abstract */ }
22
24
 
23
25
  /**
24
26
  * Attempts to resolve a type that could reference another type.
25
- * @param {any} items
26
- * @param {import('./resolver').TypeResolveInfo} into - @see Visitor.resolveType
27
+ * @param {any} items - properties of the declaration we are resolving
28
+ * @param {TypeResolveInfo} into - @see Visitor.resolveType
27
29
  * @param {SourceFile} relativeTo - file to which the resolved type should be relative to
28
30
  * @public
29
31
  */
@@ -57,7 +59,7 @@ class InlineDeclarationResolver {
57
59
  /**
58
60
  * Visits a single element in an entity.
59
61
  * @param {string} name - name of the element
60
- * @param {import('./resolver').CSN} element - CSN data belonging to the the element.
62
+ * @param {import('../resolution/resolver').CSN} element - CSN data belonging to the the element.
61
63
  * @param {SourceFile} file - the namespace file the surrounding entity is being printed into.
62
64
  * @param {Buffer} [buffer] - buffer to add the definition to. If no buffer is passed, the passed file's class buffer is used instead.
63
65
  * @public
@@ -87,7 +89,7 @@ class InlineDeclarationResolver {
87
89
 
88
90
  /**
89
91
  * It returns TypeScript datatype for provided TS property
90
- * @param {{typeName: string, typeInfo: TypeResolveInfo & { inflection: Inflection } }} type
92
+ * @param {{typeName: string, typeInfo: TypeResolveInfo & { inflection: Inflection } }} type - type of the property
91
93
  * @param {string} typeName - name of the TypeScript property
92
94
  * @returns {string} the datatype to be presented on TypeScript layer
93
95
  * @public
@@ -96,7 +98,7 @@ class InlineDeclarationResolver {
96
98
  return type.typeInfo.isNotNull ? typeName : `${typeName} | null`
97
99
  }
98
100
 
99
- /** @param {import('../visitor').Visitor} visitor */
101
+ /** @param {import('../visitor').Visitor} visitor - the visitor */
100
102
  constructor(visitor) {
101
103
  this.visitor = visitor
102
104
  // type resolution might recurse. This indicator is used to determine
@@ -114,7 +116,7 @@ class InlineDeclarationResolver {
114
116
  * b: number
115
117
  * }
116
118
  * }
117
- *
119
+ *
118
120
  * T['a']['b'] // number
119
121
  * ```
120
122
  * but especially with inline declarations, the access will differ between flattened and nested representations.
@@ -187,9 +189,9 @@ class FlatInlineDeclarationResolver extends InlineDeclarationResolver {
187
189
  * ```
188
190
  */
189
191
  class StructuredInlineDeclarationResolver extends InlineDeclarationResolver {
190
- constructor(visitor) {
192
+ constructor(visitor) {
191
193
  super(visitor)
192
- this.printDepth = 0
194
+ this.printDepth = 0
193
195
  }
194
196
 
195
197
  flatten(name, type, buffer, statementEnd = ';') {
@@ -12,7 +12,7 @@
12
12
  * y: E.x // <- ref
13
13
  * }
14
14
  * ```
15
- * @param {{type: any}} element
15
+ * @param {{type: any}} element - the element
16
16
  * @returns boolean
17
17
  */
18
18
  const isReferenceType = element => element.type && Object.hasOwn(element.type, 'ref')
@@ -5,35 +5,35 @@ const base = '__'
5
5
 
6
6
  /**
7
7
  * Wraps type into association to scalar.
8
- * @param {string} t - the singular type name.
8
+ * @param {string} t - the singular type name.
9
9
  * @returns {string}
10
10
  */
11
11
  const createToOneAssociation = t => `${base}.Association.to<${t}>`
12
12
 
13
13
  /**
14
14
  * Wraps type into association to vector.
15
- * @param {string} t - the singular type name.
15
+ * @param {string} t - the singular type name.
16
16
  * @returns {string}
17
17
  */
18
18
  const createToManyAssociation = t => `${base}.Association.to.many<${t}>`
19
19
 
20
20
  /**
21
21
  * Wraps type into composition of scalar.
22
- * @param {string} t - the singular type name.
22
+ * @param {string} t - the singular type name.
23
23
  * @returns {string}
24
24
  */
25
25
  const createCompositionOfOne = t => `${base}.Composition.of<${t}>`
26
26
 
27
27
  /**
28
28
  * Wraps type into composition of vector.
29
- * @param {string} t - the singular type name.
29
+ * @param {string} t - the singular type name.
30
30
  * @returns {string}
31
31
  */
32
32
  const createCompositionOfMany = t => `${base}.Composition.of.many<${t}>`
33
33
 
34
34
  /**
35
35
  * Wraps type into an array.
36
- * @param {string} t - the singular type name.
36
+ * @param {string} t - the singular type name.
37
37
  * @returns {string}
38
38
  */
39
39
  const createArrayOf = t => `Array<${t}>`
@@ -47,7 +47,7 @@ const createObjectOf = t => `{${t}}`
47
47
 
48
48
  /**
49
49
  * Wraps type into a deep require (removes all posibilities of undefined recursively).
50
- * @param {string} t - the singular type name.
50
+ * @param {string} t - the singular type name.
51
51
  * @param {string?} lookup - a property lookup of the required type (`['Foo']`)
52
52
  * @returns {string}
53
53
  */
@@ -59,8 +59,8 @@ const deepRequire = (t, lookup = '') => `${base}.DeepRequired<${t}>${lookup}`
59
59
  * @returns {string[]} an array of lines wrapped in doc format. The result is not
60
60
  * concatenated to be properly indented by `buffer.add(...)`.
61
61
  */
62
- const docify = doc => doc
63
- ? ['/**'].concat(doc.split('\n').map(line => `* ${line}`)).concat(['*/'])
62
+ const docify = doc => doc
63
+ ? ['/**'].concat(doc.split('\n').map(line => `* ${line}`)).concat(['*/'])
64
64
  : []
65
65
 
66
66
  module.exports = {
package/lib/csn.js CHANGED
@@ -5,15 +5,20 @@ const annotation = '@odata.draft.enabled'
5
5
  * i.e. ones that have a query, but are not a cds level projection.
6
6
  * Those are still not expanded and we have to retrieve their definition
7
7
  * with all properties from the inferred model.
8
- * @param {any} entity
8
+ * @param {any} entity - the entity
9
9
  */
10
10
  const isView = entity => entity.query && !entity.projection
11
11
 
12
12
  const isProjection = entity => entity.projection
13
13
 
14
- const getViewTarget = entity => entity.query?.SELECT?.from?.ref?.[0]
14
+ /**
15
+ * @param {any} entity - the entity
16
+ * @see isView
17
+ * Unresolved entities have to be looked up from inferred csn.
18
+ */
19
+ const isUnresolved = entity => entity._unresolved === true
15
20
 
16
- const getProjectionTarget = entity => entity.projection?.from?.ref?.[0]
21
+ const isCsnAny = entity => entity?.constructor?.name === 'any'
17
22
 
18
23
  const isDraftEnabled = entity => entity['@odata.draft.enabled'] === true
19
24
 
@@ -22,13 +27,20 @@ const isType = entity => entity?.kind === 'type'
22
27
  const isEntity = entity => entity?.kind === 'entity'
23
28
 
24
29
  /**
25
- * @param {any} entity
26
- * @see isView
27
- * Unresolved entities have to be looked up from inferred csn.
30
+ * Attempts to retrieve the max cardinality of a CSN for an entity.
31
+ * @param {EntityCSN} element - csn of entity to retrieve cardinality for
32
+ * @returns {number} max cardinality of the element.
33
+ * If no cardinality is attached to the element, cardinality is 1.
34
+ * If it is set to '*', result is Infinity.
28
35
  */
29
- const isUnresolved = entity => entity._unresolved === true
36
+ const getMaxCardinality = element => {
37
+ const cardinality = element?.cardinality?.max ?? 1
38
+ return cardinality === '*' ? Infinity : parseInt(cardinality)
39
+ }
30
40
 
31
- const isCsnAny = entity => entity?.constructor?.name === 'any'
41
+ const getViewTarget = entity => entity.query?.SELECT?.from?.ref?.[0]
42
+
43
+ const getProjectionTarget = entity => entity.projection?.from?.ref?.[0]
32
44
 
33
45
  class DraftUnroller {
34
46
  /** @type {Set<string>} */
@@ -36,7 +48,7 @@ class DraftUnroller {
36
48
  /** @type {{[key: string]: boolean}} */
37
49
  #draftable = {}
38
50
  /** @type {{[key: string]: string}} */
39
- #projections
51
+ #projections
40
52
  /** @type {object[]} */
41
53
  #entities
42
54
  #csn
@@ -56,7 +68,7 @@ class DraftUnroller {
56
68
  * @param {object | string} entity - entity to set draftable annotation for.
57
69
  * @param {boolean} value - whether the entity is draftable.
58
70
  */
59
- #setDraftable(entity, value) {
71
+ #setDraftable(entity, value) {
60
72
  if (typeof entity === 'string') entity = this.#getDefinition(entity)
61
73
  if (!entity) return // inline definition -- not found in definitions
62
74
  entity[annotation] = value
@@ -75,10 +87,10 @@ class DraftUnroller {
75
87
  #getDraftable(entityOrName) {
76
88
  const entity = (typeof entityOrName === 'string')
77
89
  ? this.#getDefinition(entityOrName)
78
- : entityOrName
90
+ : entityOrName
79
91
  // assert(typeof entity !== 'string')
80
92
  const name = entity?.name ?? entityOrName
81
- return this.#draftable[name] ??= this.#propagateInheritance(entity)
93
+ return this.#draftable[name] ??= this.#propagateInheritance(entity)
82
94
  }
83
95
 
84
96
  /**
@@ -156,11 +168,11 @@ class DraftUnroller {
156
168
  /**
157
169
  * We are unrolling the @odata.draft.enabled annotations into related entities manually.
158
170
  * This includes three scenarios:
159
- *
171
+ *
160
172
  * (a) aspects via `A: B`, where `B` is draft enabled.
161
173
  * Note that when an entity extends two other entities of which one has drafts enabled and
162
174
  * one has not, then the one that is later in the list of mixins "wins":
163
- * @param {any} csn
175
+ * @param {any} csn - the entity
164
176
  * @example
165
177
  * ```ts
166
178
  * @odata.draft.enabled true
@@ -170,7 +182,7 @@ class DraftUnroller {
170
182
  * entity A: T,F {} // draft not enabled
171
183
  * entity B: F,T {} // draft enabled
172
184
  * ```
173
- *
185
+ *
174
186
  * (b) Draft enabled projections make the entity we project on draft enabled.
175
187
  * @example
176
188
  * ```ts
@@ -178,9 +190,9 @@ class DraftUnroller {
178
190
  * entity A as projection on B {}
179
191
  * entity B {} // draft enabled
180
192
  * ```
181
- *
193
+ *
182
194
  * (c) Entities that are draft enabled propagate this property down through compositions:
183
- *
195
+ *
184
196
  * ```ts
185
197
  * @odata.draft.enabled: true
186
198
  * entity A {
@@ -201,7 +213,7 @@ function unrollDraftability(csn) {
201
213
  *
202
214
  * This explicit propagation is required to add foreign key relations
203
215
  * to referring entities.
204
- * @param {any} csn
216
+ * @param {any} csn - the entity
205
217
  * @example
206
218
  * ```cds
207
219
  * entity A: cuid { key name: String; }
@@ -209,10 +221,10 @@ function unrollDraftability(csn) {
209
221
  * ```
210
222
  * must yield
211
223
  * ```ts
212
- * class A {
224
+ * class A {
213
225
  * ID: UUID // inherited from cuid
214
226
  * name: String;
215
- * }
227
+ * }
216
228
  * class B {
217
229
  * ref: Association.to<A>
218
230
  * ref_ID: UUID
@@ -235,7 +247,7 @@ function propagateForeignKeys(csn) {
235
247
  const remoteKeys = Object.entries(this.associations ?? {})
236
248
  .filter(([,{key}]) => key) // only follow associations that are keys, that way we avoid cycles
237
249
  .flatMap(([kname, key]) => Object.entries(csn.definitions[key.target].keys)
238
- .map(([ckname, ckey]) => [`${kname}_${ckname}`, ckey]))
250
+ .map(([ckname, ckey]) => [`${kname}_${ckname}`, ckey]))
239
251
 
240
252
  this.__keys = Object.fromEntries(ownKeys
241
253
  .concat(inheritedKeys)
@@ -251,7 +263,7 @@ function propagateForeignKeys(csn) {
251
263
 
252
264
  /**
253
265
  *
254
- * @param {any} csn
266
+ * @param {any} csn - complete csn
255
267
  */
256
268
  function amendCSN(csn) {
257
269
  unrollDraftability(csn)
@@ -274,14 +286,15 @@ const getProjectionAliases = entity => {
274
286
  return { aliases, all }
275
287
  }
276
288
 
277
- module.exports = {
278
- amendCSN,
279
- isView,
289
+ module.exports = {
290
+ amendCSN,
291
+ isView,
280
292
  isProjection,
281
293
  isDraftEnabled,
282
294
  isEntity,
283
295
  isUnresolved,
284
296
  isType,
297
+ getMaxCardinality,
285
298
  getProjectionTarget,
286
299
  getProjectionAliases,
287
300
  getViewTarget,