@cap-js/cds-typer 0.26.0 → 0.28.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,10 +4,34 @@ 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.27.0 - TBD
7
+ ## Version 0.29.0 - TBD
8
+
9
+ ## Version 0.28.0 - 24-10-24
10
+ ### Added
11
+ - Schema definition for `cds.typer` options in `package.json` and `.cdsrc-*.json` files
12
+ - Added a static `elements` property to all entities, which allows access to the `LinkedDefinitions` instance of an entity's elements
13
+ - Schema definition for `typescript` cds build task.
14
+ - `.drafts` property of any entity `E` is now of type `DraftOf<E>`, or `DraftsOf<E>` for plurals, respectively. This type exposes dditional properties that are available on drafts during runtime.
15
+
16
+ ### Fixed
17
+ - Entity elements of named structured types are flattened when using the option `--inlineDeclarations flat`
18
+ - `override` modifier on `.kind` property is now only generated if the property is actually inherited, satisfying strict `tsconfig.json`s
19
+ - Properly support mandatory (`not null`) action parameters with `array of` types
20
+ - Static property `.drafts` is only create for entity classes that are actually draft enabled
21
+
22
+ ## Version 0.27.0 - 2024-10-02
23
+ ### Changed
24
+ - Any configuration variable (via CLI or `cds.env`) can now be passed in snake_case in addition to camelCase
25
+ - 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.
26
+
27
+ ### Fixed
28
+ - Fix build task for projects with spaces
29
+ - Fix a bug where cds-typer would produce redundant type declarations when the model contains an associations to another entity's property
30
+ - Reintroduce default value `'.'` for `--outputDirectory`
8
31
 
9
32
  ## Version 0.26.0 - 2024-09-11
10
33
  ### Added
34
+ - 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
11
35
  - 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.
12
36
  - Added a static `.kind` property for entities and types, which contains `'entity'` or `'type'` respectively
13
37
  - Apps need to provide `@sap/cds` version `8.2` or higher.
@@ -47,6 +71,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/).
47
71
 
48
72
 
49
73
  ## Version 0.22.0 - 2024-06-20
74
+
50
75
  ### Fixed
51
76
  - Fixed a bug where keys would sometimes inconsistently become nullable
52
77
 
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}
117
+
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
+ }
17
144
 
18
- const flags = {
145
+ const flags = enrichFlagSchema({
19
146
  outputDirectory: {
20
147
  desc: 'Root directory to write the generated files to.',
21
148
  default: './',
@@ -30,16 +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
  },
38
- useEntitiesProxy: {
174
+ useEntitiesProxy: parameterTypes.boolean({
39
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`,
40
- allowed: ['true', 'false'],
41
176
  default: 'false'
42
- },
177
+ }),
43
178
  version: {
44
179
  desc: 'Prints the version of this tool.'
45
180
  },
@@ -48,17 +183,15 @@ const flags = {
48
183
  allowed: ['flat', 'structured'],
49
184
  default: 'structured'
50
185
  },
51
- propertiesOptional: {
186
+ propertiesOptional: parameterTypes.boolean({
52
187
  desc: `If set to true, properties in entities are${EOL}always generated as optional (a?: T).`,
53
- allowed: ['true', 'false'],
54
188
  default: 'true'
55
- },
56
- IEEE754Compatible: {
189
+ }),
190
+ IEEE754Compatible: parameterTypes.boolean({
57
191
  desc: `If set to true, floating point properties are generated${EOL}as IEEE754 compatible '(number | string)' instead of 'number'.`,
58
- allowed: ['true', 'false'],
59
192
  default: 'false'
60
- }
61
- }
193
+ })
194
+ })
62
195
 
63
196
  const hint = () => 'Missing or invalid parameter(s). Call with --help for more details.'
64
197
  /**
@@ -74,19 +207,22 @@ const help = () => `SYNOPSIS${EOL2}` +
74
207
  indent('cds-typer [cds file | "*"]', ' ') + EOL2 +
75
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 +
76
209
  `OPTIONS${EOL2}` +
77
- Object.entries(flags)
210
+ flags.keys
78
211
  .sort(([a], [b]) => a.localeCompare(b))
79
- .map(([key, value]) => {
212
+ .map(key => {
213
+ const value = flags[key]
80
214
  let s = indent(`--${key}`, ' ')
81
- // @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.
82
218
  if (value.allowedHint) s += ` ${value.allowedHint}`
83
- // @ts-expect-error
219
+ // ts-expect-error
84
220
  else if (value.allowed) s += `: <${value.allowed.join(' | ')}>`
85
221
  else if ('type' in value && value.type) s += `: <${value.type}>`
86
- // @ts-expect-error
222
+ // ts-expect-error
87
223
  if (value.defaultHint || value.default) {
88
224
  s += EOL
89
- // @ts-expect-error
225
+ // ts-expect-error
90
226
  s += indent(`(default: ${value.defaultHint ?? value.default})`, ' ')
91
227
  }
92
228
  s += `${EOL2}${indent(value.desc, ' ')}`
@@ -96,7 +232,9 @@ const help = () => `SYNOPSIS${EOL2}` +
96
232
 
97
233
  const version = () => require('../package.json').version
98
234
 
99
- const main = async (/** @type {any} */ args) => {
235
+ const prepareParameters = (/** @type {any[]} */ argv) => {
236
+ const args = parseCommandlineArgs(argv, flags)
237
+
100
238
  if ('help' in args.named) {
101
239
  console.log(help())
102
240
  process.exit(0)
@@ -109,27 +247,21 @@ const main = async (/** @type {any} */ args) => {
109
247
  console.log(hint())
110
248
  process.exit(1)
111
249
  }
112
- if (args.named.jsConfigPath && !args.named.jsConfigPath.endsWith('jsconfig.json')) {
113
- args.named.jsConfigPath = path.join(args.named.jsConfigPath, 'jsconfig.json')
114
- }
115
- const newLogLevel = deprecated[args.named.logLevel]
116
- if (newLogLevel) {
117
- console.warn(`deprecated log level '${args.named.logLevel}', use '${newLogLevel}' instead (changing this automatically for now).`)
118
- args.named.logLevel = newLogLevel
119
- }
120
250
 
121
- compileFromFile(args.positional, {
122
- // temporary fix until rootDir is faded out
123
- outputDirectory: [args.named.outputDirectory, args.named.rootDir].find(d => d !== './') ?? './',
124
- logLevel: args.named.logLevel,
125
- useEntitiesProxy: args.named.useEntitiesProxy === 'true',
126
- jsConfigPath: args.named.jsConfigPath,
127
- inlineDeclarations: args.named.inlineDeclarations,
128
- propertiesOptional: args.named.propertiesOptional === 'true',
129
- IEEE754Compatible: args.named.IEEE754Compatible === 'true'
130
- })
251
+ addCLIParamsToConfig(args.named)
252
+ return args
253
+ }
254
+
255
+ const main = async (/** @type {any[]} */ argv) => {
256
+ const { positional } = prepareParameters(argv)
257
+ compileFromFile(positional)
131
258
  }
132
259
 
133
260
  if (require.main === module) {
134
- main(parseCommandlineArgs(process.argv.slice(2), flags))
261
+ main(process.argv.slice(2))
262
+ }
263
+
264
+ module.exports = {
265
+ flags,
266
+ prepareParameters
135
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 = {
@@ -13,6 +13,10 @@ const timeRegex = '`${number}${number}:${number}${number}:${number}${number}`'
13
13
  const baseDefinitions = new SourceFile('_')
14
14
  // FIXME: this should be a library someday
15
15
  baseDefinitions.addPreamble(`
16
+ import { type } from '@sap/cds'
17
+
18
+ export type ElementsOf<T> = {[name in keyof Required<T>]: type }
19
+
16
20
  export namespace Association {
17
21
  export type to <T> = T;
18
22
  export namespace to {
@@ -38,10 +42,26 @@ export type EntitySet<T> = T[] & {
38
42
  data (input:object) : T
39
43
  };
40
44
 
45
+ export type DraftEntity<T> = T & {
46
+ IsActiveEntity?: boolean | null
47
+ HasActiveEntity?: boolean | null
48
+ HasDraftEntity?: boolean | null
49
+ DraftAdministrativeData_DraftUUID?: string | null
50
+ }
51
+
52
+ export type DraftOf<T> = { new(...args: any[]): DraftEntity<T> }
53
+ export type DraftsOf<T> = typeof Array<DraftEntity<T>>
54
+
41
55
  export type DeepRequired<T> = {
42
56
  [K in keyof T]: DeepRequired<T[K]>
43
57
  } & Exclude<Required<T>, null>;
44
58
 
59
+ const key = Symbol('key') // to avoid .key showing up in IDE's auto-completion
60
+ export type Key<T> = T & {[key]?: true}
61
+
62
+ export type KeysOf<T> = {
63
+ [K in keyof T as NonNullable<T[K]> extends Key<unknown> ? K : never]-?: Key<{}> // T[K]
64
+ }
45
65
 
46
66
  /**
47
67
  * Dates and timestamps are strings during runtime, so cds-typer represents them as such.
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Create a class member with given modifiers in the right order.
3
+ * @param {object} options - options
4
+ * @param {string} options.name - the name of the member
5
+ * @param {string} [options.type] - the type of the member
6
+ * @param {string} [options.initialiser] - the initialiser for the member
7
+ * @param {string} [options.statementEnd] - the closing character for the member
8
+ * @param {boolean} [options.isDeclare] - whether the member is declared
9
+ * @param {boolean} [options.isStatic] - whether the member is static
10
+ * @param {boolean} [options.isReadonly] - whether the member is readonly
11
+ * @param {boolean} [options.isOverride] - whether the member is an override
12
+ */
13
+ function createMember ({name, type = undefined, initialiser = undefined, statementEnd = ';', isDeclare = false, isStatic = false, isReadonly = false, isOverride = false}) {
14
+ if (isDeclare && isOverride) throw new Error('member cannot have both declare and override modifiers')
15
+
16
+ const parts = []
17
+
18
+ if (isDeclare) parts.push('declare')
19
+ if (isStatic) parts.push('static')
20
+ if (isOverride) parts.push('override')
21
+ if (isReadonly) parts.push('readonly')
22
+
23
+ parts.push(type ? `${name}: ${type}` : name)
24
+
25
+ if (initialiser) parts.push(`= ${initialiser}`)
26
+
27
+ const member = parts.join(' ')
28
+ return statementEnd
29
+ ? `${member}${statementEnd}`
30
+ : member
31
+ }
32
+
33
+ module.exports = {
34
+ createMember
35
+ }
@@ -55,12 +55,12 @@ const enumVal = (key, value, enumType) => enumType === 'cds.String' ? JSON.strin
55
55
  function printEnum(buffer, name, kvs, options = {}, doc=[]) {
56
56
  const opts = {...{export: true}, ...options}
57
57
  buffer.add('// enum')
58
- if (opts.export) doc.forEach(d => { buffer.add(d) })
58
+ if (opts.export) buffer.add(doc)
59
59
  buffer.addIndentedBlock(`${opts.export ? 'export ' : ''}const ${name} = {`, () =>
60
60
  kvs.forEach(([k, v]) => { buffer.add(`${normalise(k)}: ${v},`) })
61
61
  , '} as const;')
62
62
  buffer.add(`${opts.export ? 'export ' : ''}type ${name} = ${stringifyEnumType(kvs)}`)
63
- buffer.add('')
63
+ buffer.blankLine()
64
64
  }
65
65
 
66
66
  /**
@@ -1,8 +1,10 @@
1
+ const { configuration } = require('../config')
1
2
  const { SourceFile, Buffer } = require('../file')
2
3
  const { normalise } = require('./identifier')
3
4
  const { docify } = require('./wrappers')
4
5
 
5
6
  /** @typedef {import('../resolution/resolver').TypeResolveInfo} TypeResolveInfo */
7
+ /** @typedef {import('../resolution/resolver').TypeResolveOptions} TypeResolverOptions */
6
8
  /** @typedef {import('../typedefs').visitor.Inflection} Inflection */
7
9
  /** @typedef {import('../typedefs').resolver.PropertyModifier} PropertyModifier */
8
10
  /** @typedef {import('../visitor').Visitor} Visitor */
@@ -33,9 +35,10 @@ class InlineDeclarationResolver {
33
35
  * @param {any} items - properties of the declaration we are resolving
34
36
  * @param {TypeResolveInfo} into - @see Visitor.resolveType
35
37
  * @param {SourceFile} relativeTo - file to which the resolved type should be relative to
38
+ * @param {TypeResolverOptions} [options] - resolver options
36
39
  * @public
37
40
  */
38
- resolveInlineDeclaration(items, into, relativeTo) {
41
+ resolveInlineDeclaration(items, into, relativeTo, options) {
39
42
  const dummy = new SourceFile(relativeTo.path.asDirectory())
40
43
  dummy.classes.currentIndent = relativeTo.classes.currentIndent
41
44
  dummy.classes.add('{')
@@ -50,7 +53,9 @@ class InlineDeclarationResolver {
50
53
  const se = (typeof subelement === 'string')
51
54
  ? this.visitor.resolver.resolveTypeName(subelement)
52
55
  : subelement
53
- into.structuredType[subname] = this.visitor.visitElement(subname, se, dummy)
56
+ // resolver options need to be passed through, otherwise deep expand of struct types to flat
57
+ // does not work
58
+ into.structuredType[subname] = this.visitor.visitElement({name: subname, element: se, file: dummy, resolverOptions: options})
54
59
  }
55
60
  dummy.classes.outdent()
56
61
  dummy.classes.add('}')
@@ -72,14 +77,15 @@ class InlineDeclarationResolver {
72
77
  * @param {SourceFile} options.file - the namespace file the surrounding entity is being printed into.
73
78
  * @param {Buffer} options.buffer - buffer to add the definition to. If no buffer is passed, the passed file's class buffer is used instead.
74
79
  * @param {PropertyModifier[]} options.modifiers - modifiers to add to each generated property
80
+ * @param {TypeResolverOptions} [options.resolverOptions] - resolver options
75
81
  * @public
76
82
  */
77
- visitElement({name, element, file, buffer = file.classes, modifiers = []}) {
83
+ visitElement({name, element, file, buffer = file.classes, modifiers = [], resolverOptions}) {
78
84
  this.depth++
79
85
  for (const d of docify(element.doc)) {
80
86
  buffer.add(d)
81
87
  }
82
- const type = this.visitor.resolver.resolveAndRequire(element, file)
88
+ const type = this.visitor.resolver.resolveAndRequire(element, file, resolverOptions)
83
89
  this.depth--
84
90
  if (this.depth === 0) {
85
91
  this.printInlineType({fq: name, type, buffer, modifiers})
@@ -94,7 +100,7 @@ class InlineDeclarationResolver {
94
100
  * @returns {'?:'|':'}
95
101
  */
96
102
  getPropertyTypeSeparator() {
97
- return this.visitor.options.propertiesOptional ? '?:' : ':'
103
+ return configuration.propertiesOptional ? '?:' : ':'
98
104
  }
99
105
 
100
106
  /**
@@ -105,7 +111,8 @@ class InlineDeclarationResolver {
105
111
  * @public
106
112
  */
107
113
  getPropertyDatatype(type, typeName = type.typeName) {
108
- return type.typeInfo.isNotNull ? typeName : `${typeName} | null`
114
+ // do not append null if already added to type
115
+ return type.typeInfo.isNotNull ? typeName : typeName.endsWith('| null') ? typeName : `${typeName} | null`
109
116
  }
110
117
 
111
118
  /**
@@ -267,10 +274,9 @@ class StructuredInlineDeclarationResolver extends InlineDeclarationResolver {
267
274
  * @type {InlineDeclarationResolver['printInlineType']}
268
275
  */
269
276
  printInlineType({fq, type, buffer, modifiers, statementEnd}) {
270
- // FIXME: indent not quite right
271
277
  const sub = new Buffer()
272
- sub.currentIndent = buffer.currentIndent
273
- buffer.add(this.flatten({fq, type, buffer: sub, modifiers, statementEnd}).join())
278
+ this.flatten({fq, type, buffer: sub, modifiers, statementEnd})
279
+ buffer.add(sub.parts)
274
280
  }
275
281
 
276
282
  /**
@@ -3,11 +3,11 @@
3
3
  // @ts-nocheck
4
4
  const proxyAccessFunction = function (fqParts, opts = {}) {
5
5
  const { target, customProps } = { target: {}, customProps: [], ...opts }
6
- const fq = fqParts.join('.')
6
+ const fq = fqParts.filter(p => !!p).join('.')
7
7
  return new Proxy(target, {
8
8
  get: function (target, prop) {
9
9
  if (cds.entities) {
10
- target.__proto__ = cds.entities[fq]
10
+ target.__proto__ = cds.entities(fqParts[0])[fqParts[1]]
11
11
  // overwrite/simplify getter after cds.entities is accessible
12
12
  this.get = (target, prop) => target[prop]
13
13
  return target[prop]