@cap-js/cds-typer 0.25.0 → 0.27.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,30 @@ 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.26.0 - TBD
7
+ ## Version 0.28.0 - TBD
8
+
9
+ ## Version 0.27.0 - 2024-10-02
10
+ ### Changed
11
+ - Any configuration variable (via CLI or `cds.env`) can now be passed in snake_case in addition to camelCase
12
+ - Action parameters are now generated as optional by default, which is how the runtime treats them. Mandatory parameters have to be marked as `not null` in CDS/CDL, or `notNull` in CSN.
13
+
14
+ ### Fixed
15
+ - Fix build task for projects with spaces
16
+ - Fixa bug where cds-typer would produce redundant type declarations when the model contains an associations to another entity's property
17
+
18
+ ## Version 0.26.0 - 2024-09-11
19
+ ### Added
20
+ - Added a static `.keys` property in all entities. That property is dictionary which holds all properties as keys that are marked as `key` in CDS
21
+ - Added a CLI option `--useEntitiesProxy`. When set to `true`, all entities are wrapped into `Proxy` objects during runtime, allowing top level imports of entity types.
22
+ - Added a static `.kind` property for entities and types, which contains `'entity'` or `'type'` respectively
23
+ - Apps need to provide `@sap/cds` version `8.2` or higher.
24
+ - Apps need to provide `@cap-js/cds-types` version `0.6.4` or higher.
25
+ - Typed methods are now generated for calls of unbound actions. Named and positional call styles are supported, e.g. `service.action({one, two})` and `service.action(one, two)`.
26
+ - Action parameters can be optional in the named call style (`service.action({one:1, ...})`).
27
+ - Actions for ABAP RFC modules cannot be called with positional parameters, but only with named ones. They have 'parameter categories' (import/export/changing/tables) that cannot be called in a flat order.
28
+ - Services now have their own export (named like the service itself). The current default export is not usable in some scenarios from CommonJS modules.
29
+ - Enums and operation parameters can have doc comments
30
+
8
31
 
9
32
  ## Version 0.25.0 - 2024-08-13
10
33
  ### Added
@@ -34,6 +57,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/).
34
57
 
35
58
 
36
59
  ## Version 0.22.0 - 2024-06-20
60
+
37
61
  ### Fixed
38
62
  - Fixed a bug where keys would sometimes inconsistently become nullable
39
63
 
@@ -85,7 +109,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/).
85
109
  - Importing an enum into a service will now generate an alias to the original enum, instead of incorrectly duplicating the definition
86
110
  - Returning entities from actions/ functions and using them as parameters will now properly use the singular inflection instead of returning an array thereof
87
111
  - Aspects are now consistently named and called in their singular form
88
- - Only detect inflection clash if singular and plural share the same namespace. This also no longer reports `sap.common` as erroneous during type creation
112
+ - Only detect inflection clash if singular and plural share the same namespace. This also no longer reports `sap.common` as erroneous during type creation
89
113
 
90
114
  ## Version 0.19.0 - 2024-03-28
91
115
  ### Added
package/README.md CHANGED
@@ -9,6 +9,25 @@ Generates `.ts` files for a CDS model to receive code completion in VS Code.
9
9
 
10
10
  Exhaustive documentation can be found on [CAPire](https://cap.cloud.sap/docs/tools/cds-typer).
11
11
 
12
+ ## Known Restrictions
13
+
14
+ Certain language features of CDS can not be represented in TypeScript.
15
+ Trying to generate types for models using these features will therefore result in incorrect or broken TypeScript code.
16
+
17
+ ### Changing Types
18
+
19
+ While the following is valid CDS, there is no TypeScript equivalent that would allow the type of an inherited property to change (TS2416).
20
+
21
+ ```cds
22
+ entity A {
23
+ foo: Integer
24
+ };
25
+
26
+ entity B: A {
27
+ foo: String
28
+ }
29
+ ```
30
+
12
31
  ## Support, Feedback, Contributing
13
32
 
14
33
  This project is open to feature requests/suggestions, bug reports etc. via [GitHub issues](https://github.com/cap-js/cds-typer/issues). Contribution and feedback are encouraged and always welcome. For more information about how to contribute, the project structure, as well as additional contribution information, see our [Contribution Guidelines](CONTRIBUTING.md).
package/cds-plugin.js CHANGED
@@ -36,7 +36,7 @@ const rmDirIfExists = dir => {
36
36
  * Remove files with given extensions from a directory recursively.
37
37
  * @param {string} dir - The directory to start from.
38
38
  * @param {string[]} exts - The extensions to remove.
39
- * @returns {Promise<void>}
39
+ * @returns {Promise<unknown>}
40
40
  */
41
41
  const rmFiles = async (dir, exts) => fs.existsSync(dir)
42
42
  ? Promise.all(
@@ -68,10 +68,15 @@ cds.build?.register?.('typescript', class extends cds.build.Plugin {
68
68
 
69
69
  get #appFolder () { return cds?.env?.folders?.app ?? 'app' }
70
70
 
71
+ /**
72
+ * cds.env > tsconfig.compilerOptions.paths > '@cds-models' (default)
73
+ */
71
74
  get #modelDirectoryName () {
75
+ const outputDirectory = cds.env.typer?.outputDirectory
76
+ if (outputDirectory) return outputDirectory
72
77
  try {
73
78
  // expected format: { '#cds-models/*': [ './@cds-models/*/index.ts' ] }
74
- // ^^^^^^^^^^^^^^^
79
+ // ^^^^^^^^^^^
75
80
  // relevant part - may be changed by user
76
81
  const config = JSON.parse(fs.readFileSync ('tsconfig.json', 'utf8'))
77
82
  const alias = config.compilerOptions.paths['#cds-models/*'][0]
@@ -89,7 +94,9 @@ cds.build?.register?.('typescript', class extends cds.build.Plugin {
89
94
 
90
95
  async #runCdsTyper () {
91
96
  DEBUG?.('running cds-typer')
92
- await typer.compileFromFile('*', { outputDirectory: this.#modelDirectoryName })
97
+ cds.env.typer ??= {}
98
+ cds.env.typer.outputDirectory ??= this.#modelDirectoryName
99
+ await typer.compileFromFile('*')
93
100
  }
94
101
 
95
102
  async #buildWithConfig () {
@@ -103,7 +110,7 @@ cds.build?.register?.('typescript', class extends cds.build.Plugin {
103
110
  DEBUG?.('building without config')
104
111
  // this will include gen/ that was created by the nodejs task
105
112
  // _within_ the project directory. So we need to remove it afterwards.
106
- await exec(`npx tsc --outDir ${this.task.dest}`)
113
+ await exec(`npx tsc --outDir "${this.task.dest}"`)
107
114
  rmDirIfExists(path.join(this.task.dest, cds.env.build.target))
108
115
  rmDirIfExists(path.join(this.task.dest, this.#appFolder))
109
116
  }
package/lib/cli.js CHANGED
@@ -2,20 +2,147 @@
2
2
  /* eslint-disable no-console */
3
3
  'use strict'
4
4
 
5
+ /**
6
+ * @typedef {import('./typedefs').config.cli.ParameterSchema[number]} Parameter
7
+ */
8
+
5
9
  const cds = require('@sap/cds')
6
10
  const { compileFromFile } = require('./compile')
7
- const { parseCommandlineArgs } = require('./util')
11
+ const { camelToSnake } = require('./util')
8
12
  const { deprecated, _keyFor } = require('./logging')
9
13
  const path = require('path')
10
14
  const { EOL } = require('node:os')
15
+ const { camelSnakeHybrid, configuration } = require('./config')
11
16
 
12
17
  const EOL2 = EOL + EOL
13
18
  const toolName = 'cds-typer'
14
-
15
19
  // @ts-expect-error - nope, it is actually there. Types just seem to be out of sync.
16
20
  const lls = cds.log.levels
21
+ const parameterTypes = {
22
+ boolean:
23
+ /**
24
+ * @param {Parameter} props - additional parameter properties
25
+ * @returns {Parameter}
26
+ */
27
+ props => ({...{
28
+ allowed: ['true', 'false'],
29
+ type: 'boolean',
30
+ postprocess: (/** @type {string} */ value) => value === 'true'
31
+ },
32
+ ...props})
33
+ }
34
+
35
+ /**
36
+ * Adds additional properties to the CLI parameter schema.
37
+ * @param {import('./typedefs').config.cli.ParameterSchema } flags - The CLI parameter schema.
38
+ * @returns {import('./typedefs').config.cli.ParameterSchema} - The enriched schema.
39
+ */
40
+ const enrichFlagSchema = flags => {
41
+ const flagKeys = Object.keys(flags)
42
+ for (const [key, value] of Object.entries(flags)) {
43
+ /** @type {Parameter} */(value).camel = key;
44
+ /** @type {Parameter} */(value).snake = camelToSnake(key)
45
+ }
46
+ // non-enumerable utilities
47
+ Object.defineProperties(flags, {
48
+ hasFlag: {
49
+ value: (/** @type {string} **/ flag) => Object.values(flags).some(f => f.snake === flag || f.camel === flag)
50
+ },
51
+ keys: {
52
+ value: flagKeys
53
+ }
54
+ })
55
+ return camelSnakeHybrid(flags)
56
+ }
57
+
58
+ /**
59
+ * Parses command line arguments into named and positional parameters.
60
+ * Named parameters are expected to start with a double dash (--).
61
+ * If the next argument `B` after a named parameter `A` is not a named parameter itself,
62
+ * `B` is used as value for `A`.
63
+ * If `A` and `B` are both named parameters, `A` is just treated as a flag (and may receive a default value).
64
+ * Only named parameters that occur in validFlags are allowed. Specifying named flags that are not listed there
65
+ * will cause an error.
66
+ * Named parameters that are either not specified or do not have a value assigned to them may draw a default value
67
+ * from their definition in validFlags.
68
+ * @param {string[]} argv - list of command line arguments
69
+ * @param {import('./typedefs').config.cli.ParameterSchema} schema - allowed flags. May specify default values.
70
+ * @returns {import('./typedefs').config.cli.ParsedParameters} - parsed arguments
71
+ */
72
+ const parseCommandlineArgs = (argv, schema) => {
73
+ const isFlag = (/** @type {string} */ arg) => arg.startsWith('--')
74
+ const positional = []
75
+ /** @type {import('./typedefs').config.cli.ParsedParameters['named']} */
76
+ const named = {}
77
+
78
+ let i = 0
79
+ while (i < argv.length) {
80
+ const originalArgName = argv[i] // so our feedback to the user is less confusing
81
+ let arg = originalArgName
82
+ if (isFlag(arg)) {
83
+ arg = camelToSnake(arg.slice(2))
84
+ // @ts-expect-error - cba to add hasFlag to the general dictionary
85
+ if (!schema.hasFlag(arg)) {
86
+ throw new Error(`invalid named flag '${originalArgName}'`)
87
+ }
88
+ const next = argv[i + 1]
89
+ if (next && !isFlag(next)) {
90
+ named[arg] = { value: next, isDefault: false }
91
+ i++
92
+ } else {
93
+ named[arg] = { value: schema[arg].default, isDefault: true }
94
+ }
95
+
96
+ const { allowed, allowedHint } = schema[arg]
97
+ if (allowed && !allowed.includes(named[arg].value)) {
98
+ throw new Error(`invalid value '${named[arg]?.value ?? named[arg]}' for flag ${originalArgName}. Must be one of ${(allowedHint ?? allowed.join(', '))}`)
99
+ }
100
+ } else {
101
+ positional.push(arg)
102
+ }
103
+ i++
104
+ }
105
+
106
+ // enrich with defaults
107
+ /** @type {import('./typedefs').config.cli.ParameterSchema} */
108
+ const defaults = Object.entries(schema)
109
+ .filter(e => !!e[1].default)
110
+ .reduce((dict, [k, v]) => {
111
+ // @ts-expect-error - can't infer type of initial {}
112
+ dict[camelToSnake(k)] = { value: v.default, isDefault: true }
113
+ return dict
114
+ }, {})
115
+
116
+ const namedWithDefaults = {...defaults, ...named}
17
117
 
18
- const flags = {
118
+ // apply postprocessing
119
+ for (const [key, {value}] of Object.entries(namedWithDefaults)) {
120
+ const { postprocess } = schema[key]
121
+ if (typeof postprocess === 'function') {
122
+ namedWithDefaults[key].value = postprocess(value)
123
+ }
124
+ }
125
+
126
+ return {
127
+ named: namedWithDefaults,
128
+ positional,
129
+ }
130
+ }
131
+
132
+ /**
133
+ * Adds CLI parameters to the configuration object.
134
+ * Precedence: CLI > env > default.
135
+ * @param {ReturnType<parseCommandlineArgs>['named']} params - CLI parameters.
136
+ */
137
+ const addCLIParamsToConfig = params => {
138
+ for (const [key, value] of Object.entries(params)) {
139
+ if (!value.isDefault || Object.hasOwn(configuration, key)) {
140
+ configuration[key] = value.value
141
+ }
142
+ }
143
+ }
144
+
145
+ const flags = enrichFlagSchema({
19
146
  outputDirectory: {
20
147
  desc: 'Root directory to write the generated files to.',
21
148
  default: './',
@@ -30,11 +157,24 @@ const flags = {
30
157
  allowedHint: Object.keys(lls).join(' | '), // FIXME: remove once old levels are faded out
31
158
  defaultHint: _keyFor(lls.ERROR),
32
159
  default: cds?.env?.log?.levels?.['cds-typer'] ?? _keyFor(lls.ERROR),
160
+ postprocess: level => {
161
+ const newLogLevel = deprecated[level]
162
+ if (newLogLevel) {
163
+ console.warn(`deprecated log level '${level}', use '${newLogLevel}' instead (changing this automatically for now).`)
164
+ return newLogLevel
165
+ }
166
+ return level
167
+ }
33
168
  },
34
169
  jsConfigPath: {
35
170
  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.`,
36
- type: 'string'
171
+ type: 'string',
172
+ postprocess: file => file && !file.endsWith('jsconfig.json') ? path.join(file, 'jsconfig.json') : path
37
173
  },
174
+ useEntitiesProxy: parameterTypes.boolean({
175
+ desc: `If set to true the 'cds.entities' exports in the generated 'index.js'${EOL}files will be wrapped in 'Proxy' objects\nso static import/require calls can be used everywhere.${EOL}${EOL}WARNING: entity properties can still only be accessed after${EOL}'cds.entities' has been loaded`,
176
+ default: 'false'
177
+ }),
38
178
  version: {
39
179
  desc: 'Prints the version of this tool.'
40
180
  },
@@ -43,17 +183,15 @@ const flags = {
43
183
  allowed: ['flat', 'structured'],
44
184
  default: 'structured'
45
185
  },
46
- propertiesOptional: {
186
+ propertiesOptional: parameterTypes.boolean({
47
187
  desc: `If set to true, properties in entities are${EOL}always generated as optional (a?: T).`,
48
- allowed: ['true', 'false'],
49
188
  default: 'true'
50
- },
51
- IEEE754Compatible: {
189
+ }),
190
+ IEEE754Compatible: parameterTypes.boolean({
52
191
  desc: `If set to true, floating point properties are generated${EOL}as IEEE754 compatible '(number | string)' instead of 'number'.`,
53
- allowed: ['true', 'false'],
54
192
  default: 'false'
55
- }
56
- }
193
+ })
194
+ })
57
195
 
58
196
  const hint = () => 'Missing or invalid parameter(s). Call with --help for more details.'
59
197
  /**
@@ -69,19 +207,22 @@ const help = () => `SYNOPSIS${EOL2}` +
69
207
  indent('cds-typer [cds file | "*"]', ' ') + EOL2 +
70
208
  indent(`Generates type information based on a CDS model.${EOL}Call with at least one positional parameter pointing${EOL}to the (root) CDS file you want to compile.`, ' ') + EOL2 +
71
209
  `OPTIONS${EOL2}` +
72
- Object.entries(flags)
210
+ flags.keys
73
211
  .sort(([a], [b]) => a.localeCompare(b))
74
- .map(([key, value]) => {
212
+ .map(key => {
213
+ const value = flags[key]
75
214
  let s = indent(`--${key}`, ' ')
76
- // @ts-expect-error - not going to check presence of each property. Same for the following expect-errors.
215
+ const snake = camelToSnake(key)
216
+ if (key !== snake) s += EOL + indent(`--${snake}`, ' ')
217
+ // ts-expect-error - not going to check presence of each property. Same for the following expect-errors.
77
218
  if (value.allowedHint) s += ` ${value.allowedHint}`
78
- // @ts-expect-error
219
+ // ts-expect-error
79
220
  else if (value.allowed) s += `: <${value.allowed.join(' | ')}>`
80
221
  else if ('type' in value && value.type) s += `: <${value.type}>`
81
- // @ts-expect-error
222
+ // ts-expect-error
82
223
  if (value.defaultHint || value.default) {
83
224
  s += EOL
84
- // @ts-expect-error
225
+ // ts-expect-error
85
226
  s += indent(`(default: ${value.defaultHint ?? value.default})`, ' ')
86
227
  }
87
228
  s += `${EOL2}${indent(value.desc, ' ')}`
@@ -91,7 +232,9 @@ const help = () => `SYNOPSIS${EOL2}` +
91
232
 
92
233
  const version = () => require('../package.json').version
93
234
 
94
- const main = async (/** @type {any} */ args) => {
235
+ const prepareParameters = (/** @type {any[]} */ argv) => {
236
+ const args = parseCommandlineArgs(argv, flags)
237
+
95
238
  if ('help' in args.named) {
96
239
  console.log(help())
97
240
  process.exit(0)
@@ -104,26 +247,21 @@ const main = async (/** @type {any} */ args) => {
104
247
  console.log(hint())
105
248
  process.exit(1)
106
249
  }
107
- if (args.named.jsConfigPath && !args.named.jsConfigPath.endsWith('jsconfig.json')) {
108
- args.named.jsConfigPath = path.join(args.named.jsConfigPath, 'jsconfig.json')
109
- }
110
- const newLogLevel = deprecated[args.named.logLevel]
111
- if (newLogLevel) {
112
- console.warn(`deprecated log level '${args.named.logLevel}', use '${newLogLevel}' instead (changing this automatically for now).`)
113
- args.named.logLevel = newLogLevel
114
- }
115
250
 
116
- compileFromFile(args.positional, {
117
- // temporary fix until rootDir is faded out
118
- outputDirectory: [args.named.outputDirectory, args.named.rootDir].find(d => d !== './') ?? './',
119
- logLevel: args.named.logLevel,
120
- jsConfigPath: args.named.jsConfigPath,
121
- inlineDeclarations: args.named.inlineDeclarations,
122
- propertiesOptional: args.named.propertiesOptional === 'true',
123
- IEEE754Compatible: args.named.IEEE754Compatible === 'true'
124
- })
251
+ addCLIParamsToConfig(args.named)
252
+ return args
253
+ }
254
+
255
+ const main = async (/** @type {any[]} */ argv) => {
256
+ const { positional } = prepareParameters(argv)
257
+ compileFromFile(positional)
125
258
  }
126
259
 
127
260
  if (require.main === module) {
128
- main(parseCommandlineArgs(process.argv.slice(2), flags))
261
+ main(process.argv.slice(2))
262
+ }
263
+
264
+ module.exports = {
265
+ flags,
266
+ prepareParameters
129
267
  }
package/lib/compile.js CHANGED
@@ -7,10 +7,7 @@ const util = require('./util')
7
7
  const { writeout } = require('./file')
8
8
  const { Visitor } = require('./visitor')
9
9
  const { LOG, setLevel } = require('./logging')
10
-
11
- /**
12
- * @typedef {import('./typedefs').visitor.CompileParameters} CompileParameters
13
- */
10
+ const { configuration } = require('./config')
14
11
 
15
12
  /**
16
13
  * Writes the accompanying jsconfig.json file to the specified paths.
@@ -40,31 +37,28 @@ const writeJsConfig = path => {
40
37
  /**
41
38
  * Compiles a CSN object to Typescript types.
42
39
  * @param {{xtended: import('./typedefs').resolver.CSN, inferred: import('./typedefs').resolver.CSN}} csn - csn tuple
43
- * @param {CompileParameters} parameters - path to root directory for all generated files, min log level
44
40
  */
45
- const compileFromCSN = async (csn, parameters) => {
46
- const envSettings = cds.env?.typer ?? {}
47
- parameters = { ...envSettings, ...parameters }
48
- setLevel(parameters.logLevel)
49
- if (parameters.jsConfigPath) {
50
- writeJsConfig(parameters.jsConfigPath)
41
+ const compileFromCSN = async csn => {
42
+
43
+ setLevel(configuration.logLevel)
44
+ if (configuration.jsConfigPath) {
45
+ writeJsConfig(configuration.jsConfigPath)
51
46
  }
52
47
  return writeout(
53
- parameters.outputDirectory,
54
- Object.values(new Visitor(csn, parameters).getWriteoutFiles())
48
+ configuration.outputDirectory,
49
+ Object.values(new Visitor(csn).getWriteoutFiles())
55
50
  )
56
51
  }
57
52
 
58
53
  /**
59
54
  * Compiles a .cds file to Typescript types.
60
55
  * @param {string | string[]} inputFile - path to input .cds file
61
- * @param {CompileParameters} parameters - path to root directory for all generated files, min log level, etc.
62
56
  */
63
- const compileFromFile = async (inputFile, parameters) => {
57
+ const compileFromFile = async inputFile => {
64
58
  const paths = typeof inputFile === 'string' ? normalize(inputFile) : inputFile.map(f => normalize(f))
65
59
  const xtended = await cds.linked(await cds.load(paths, { docs: true, flavor: 'xtended' }))
66
60
  const inferred = await cds.linked(await cds.load(paths, { docs: true }))
67
- return compileFromCSN({xtended, inferred}, parameters)
61
+ return compileFromCSN({xtended, inferred})
68
62
  }
69
63
 
70
64
  module.exports = {
@@ -42,6 +42,12 @@ export type DeepRequired<T> = {
42
42
  [K in keyof T]: DeepRequired<T[K]>
43
43
  } & Exclude<Required<T>, null>;
44
44
 
45
+ const key = Symbol('key') // to avoid .key showing up in IDE's auto-completion
46
+ export type Key<T> = T & {[key]?: true}
47
+
48
+ export type KeysOf<T> = {
49
+ [K in keyof T as NonNullable<T[K]> extends Key<unknown> ? K : never]-?: Key<{}> // T[K]
50
+ }
45
51
 
46
52
  /**
47
53
  * Dates and timestamps are strings during runtime, so cds-typer represents them as such.
@@ -22,7 +22,7 @@ const stringifyEnumType = kvs => [...uniqueValues(kvs)].join(' | ')
22
22
  /**
23
23
  * @param {string} key - the key of the enum
24
24
  * @param {any} value - the value of the enum
25
- * @param {string | import('@sap/cds').ref} enumType - the type of the enum
25
+ * @param {string | import('../typedefs').resolver.ref} enumType - the type of the enum
26
26
  */
27
27
  const enumVal = (key, value, enumType) => enumType === 'cds.String' ? JSON.stringify(`${value ?? key}`) : value
28
28
 
@@ -50,10 +50,12 @@ const enumVal = (key, value, enumType) => enumType === 'cds.String' ? JSON.strin
50
50
  * @param {string} name - local name of the enum, i.e. the name under which it should be created in the .ts file
51
51
  * @param {[string, string][]} kvs - list of key-value pairs
52
52
  * @param {object} options - options for printing the enum
53
+ * @param {string[]} doc - the enum docs
53
54
  */
54
- function printEnum(buffer, name, kvs, options = {}) {
55
+ function printEnum(buffer, name, kvs, options = {}, doc=[]) {
55
56
  const opts = {...{export: true}, ...options}
56
57
  buffer.add('// enum')
58
+ if (opts.export) doc.forEach(d => { buffer.add(d) })
57
59
  buffer.addIndentedBlock(`${opts.export ? 'export ' : ''}const ${name} = {`, () =>
58
60
  kvs.forEach(([k, v]) => { buffer.add(`${normalise(k)}: ${v},`) })
59
61
  , '} as const;')
@@ -1,3 +1,4 @@
1
+ const { configuration } = require('../config')
1
2
  const { SourceFile, Buffer } = require('../file')
2
3
  const { normalise } = require('./identifier')
3
4
  const { docify } = require('./wrappers')
@@ -94,7 +95,7 @@ class InlineDeclarationResolver {
94
95
  * @returns {'?:'|':'}
95
96
  */
96
97
  getPropertyTypeSeparator() {
97
- return this.visitor.options.propertiesOptional ? '?:' : ':'
98
+ return configuration.propertiesOptional ? '?:' : ':'
98
99
  }
99
100
 
100
101
  /**
@@ -0,0 +1,28 @@
1
+ /* eslint-disable no-undef */
2
+ // this function will be stringified as part of the generated types and is not supposed to be used internally
3
+ // @ts-nocheck
4
+ const proxyAccessFunction = function (fqParts, opts = {}) {
5
+ const { target, customProps } = { target: {}, customProps: [], ...opts }
6
+ const fq = fqParts.filter(p => !!p).join('.')
7
+ return new Proxy(target, {
8
+ get: function (target, prop) {
9
+ if (cds.entities) {
10
+ target.__proto__ = cds.entities(fqParts[0])[fqParts[1]]
11
+ // overwrite/simplify getter after cds.entities is accessible
12
+ this.get = (target, prop) => target[prop]
13
+ return target[prop]
14
+ }
15
+ // we already know the name so we skip the cds.entities proxy access
16
+ if (prop === 'name') return fq
17
+ // custom properties access on 'target' as well as cached _entity property access goes here
18
+ if (Object.hasOwn(target, prop)) return target[prop]
19
+ // inline enums have to be caught here for first time access, as they do not exist on the entity
20
+ if (customProps.includes(prop)) return target[prop]
21
+ // last but not least we pass the property access to cds.entities
22
+ throw new Error(`Property ${prop} does not exist on entity '${fq}' or cds.entities is not yet defined. Ensure the CDS runtime is fully booted before accessing properties.`)
23
+ }
24
+ })
25
+ }.toString()
26
+ /* eslint-enable no-undef */
27
+
28
+ module.exports = { proxyAccessFunction }
@@ -3,6 +3,20 @@
3
3
  // this was derived from baseDefinitions before, but caused a circular dependency
4
4
  const base = '__'
5
5
 
6
+ /**
7
+ * Wraps type into the Key type.
8
+ * @param {string} t - the type name.
9
+ * @returns {string}
10
+ */
11
+ const createKey = t => `${base}.Key<${t}>`
12
+
13
+ /**
14
+ * Wraps type into KeysOf type.
15
+ * @param {string} t - the type name.
16
+ * @returns {string}
17
+ */
18
+ const createKeysOf = t => `${base}.KeysOf<${t}>`
19
+
6
20
  /**
7
21
  * Wraps type into association to scalar.
8
22
  * @param {string} t - the singular type name.
@@ -45,6 +59,24 @@ const createArrayOf = t => `Array<${t}>`
45
59
  */
46
60
  const createObjectOf = t => `{${t}}`
47
61
 
62
+ /**
63
+ * Wraps types into a union type string
64
+ * @param {string[]} types - an array of types
65
+ * @returns {string}
66
+ */
67
+ const createUnionOf = (...types) => types.join(' | ')
68
+
69
+ /**
70
+ * Wraps type into a promise
71
+ * @param {string} t - the type to wrap.
72
+ * @returns {string}
73
+ * @example
74
+ * ```js
75
+ * createPromiseOf('string') // -> 'Promise<string>'
76
+ * ```
77
+ */
78
+ const createPromiseOf = t => `Promise<${t}>`
79
+
48
80
  /**
49
81
  * Wraps type into a deep require (removes all posibilities of undefined recursively).
50
82
  * @param {string} t - the singular type name.
@@ -59,13 +91,20 @@ const deepRequire = (t, lookup = '') => `${base}.DeepRequired<${t}>${lookup}`
59
91
  * @returns {string[]} an array of lines wrapped in doc format. The result is not
60
92
  * concatenated to be properly indented by `buffer.add(...)`.
61
93
  */
62
- const docify = doc => doc
63
- ? ['/**'].concat(doc.split('\n').map(line => `* ${line}`)).concat(['*/'])
64
- : []
94
+ const docify = doc => {
95
+ if (!doc) return []
96
+ const lines = doc.split(/\r?\n/).map(l => l.trim().replaceAll('*/', '*\\/')) // mask any */ with *\/
97
+ if (lines.length === 1) return [`/** ${lines[0]} */`] // one-line doc
98
+ return ['/**'].concat(lines.map(line => `* ${line}`)).concat(['*/'])
99
+ }
65
100
 
66
101
  module.exports = {
67
102
  createArrayOf,
103
+ createKey,
104
+ createKeysOf,
68
105
  createObjectOf,
106
+ createPromiseOf,
107
+ createUnionOf,
69
108
  createToOneAssociation,
70
109
  createToManyAssociation,
71
110
  createCompositionOfOne,