@cap-js/cds-typer 0.15.0 → 0.17.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 +14 -1
- package/lib/cli.js +2 -2
- package/lib/components/enum.js +4 -8
- package/lib/components/reference.js +23 -0
- package/lib/components/resolver.js +12 -3
- package/lib/csn.js +3 -1
- package/lib/file.js +65 -2
- package/lib/visitor.js +85 -104
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,7 +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.
|
|
7
|
+
## Version 0.18.0 - TBD
|
|
8
|
+
|
|
9
|
+
## Version 0.17.0 - 2024-03-05
|
|
10
|
+
### Fixed
|
|
11
|
+
- Fixed a bug where refering to an externally defined enum via the `typeof` syntax would crash the type generation
|
|
12
|
+
|
|
13
|
+
## Version 0.16.0 - 2024-02-01
|
|
14
|
+
### Changed
|
|
15
|
+
- Changed default log level from `NONE` to `ERROR`. See the doc to manually pass in another log level for cds-typer runs
|
|
16
|
+
- Name collisions between automatically generated foreign key fields (`.…_ID`, `.…_code`, etc.) with explicitly named fields will now raise an error
|
|
17
|
+
- Generate CDS types that are actually structured types as if they were entities. This allows the correct representation of mixing aspects and types in CDS inheritance, and also fixes issues with inline enums in such types
|
|
18
|
+
|
|
19
|
+
### Fixed
|
|
20
|
+
- Externally defined enums can now be used as parameter types in actions
|
|
8
21
|
|
|
9
22
|
## Version 0.15.0 - 2023-12-21
|
|
10
23
|
### Added
|
package/lib/cli.js
CHANGED
|
@@ -23,7 +23,7 @@ const flags = {
|
|
|
23
23
|
logLevel: {
|
|
24
24
|
desc: `Minimum log level that is printed.`,
|
|
25
25
|
allowed: Object.keys(Levels),
|
|
26
|
-
default:
|
|
26
|
+
default: Levels.ERROR,
|
|
27
27
|
},
|
|
28
28
|
jsConfigPath: {
|
|
29
29
|
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.`,
|
|
@@ -90,7 +90,7 @@ const main = async (args) => {
|
|
|
90
90
|
compileFromFile(args.positional, {
|
|
91
91
|
// temporary fix until rootDir is faded out
|
|
92
92
|
outputDirectory: [args.named.outputDirectory, args.named.rootDir].find(d => d !== './') ?? './',
|
|
93
|
-
logLevel: Levels[args.named.logLevel],
|
|
93
|
+
logLevel: Levels[args.named.logLevel] ?? args.named.logLevel,
|
|
94
94
|
jsConfigPath: args.named.jsConfigPath,
|
|
95
95
|
inlineDeclarations: args.named.inlineDeclarations,
|
|
96
96
|
propertiesOptional: args.named.propertiesOptional === 'true'
|
package/lib/components/enum.js
CHANGED
|
@@ -29,13 +29,9 @@ const { normalise } = require('./identifier')
|
|
|
29
29
|
function printEnum(buffer, name, kvs, options = {}) {
|
|
30
30
|
const opts = {...{export: true}, ...options}
|
|
31
31
|
buffer.add('// enum')
|
|
32
|
-
buffer.
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
buffer.add(`${normalise(k)}: ${v},`)
|
|
36
|
-
}
|
|
37
|
-
buffer.outdent()
|
|
38
|
-
buffer.add('} as const;')
|
|
32
|
+
buffer.addIndentedBlock(`${opts.export ? 'export ' : ''}const ${name} = {`, () =>
|
|
33
|
+
kvs.forEach(([k, v]) => buffer.add(`${normalise(k)}: ${v},`))
|
|
34
|
+
, '} as const;')
|
|
39
35
|
buffer.add(`${opts.export ? 'export ' : ''}type ${name} = ${stringifyEnumType(kvs)}`)
|
|
40
36
|
buffer.add('')
|
|
41
37
|
}
|
|
@@ -59,7 +55,7 @@ const stringifyEnumType = kvs => [...uniqueValues(kvs)].join(' | ')
|
|
|
59
55
|
const uniqueValues = kvs => new Set(kvs.map(([,v]) => v?.val ?? v)) // in case of wrapped vals we need to unwrap here for the type
|
|
60
56
|
|
|
61
57
|
// in case of strings, wrap in quotes and fallback to key to make sure values are attached for every key
|
|
62
|
-
const
|
|
58
|
+
const enumVal = (key, value, enumType) => enumType === 'cds.String' ? JSON.stringify(`${value ?? key}`) : value
|
|
63
59
|
|
|
64
60
|
/**
|
|
65
61
|
* Converts a CSN type describing an enum into a list of kv-pairs.
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Check if an element references another type.
|
|
3
|
+
* This happens for foreign key relationships
|
|
4
|
+
* and for the typeof syntax.
|
|
5
|
+
*
|
|
6
|
+
* ```cds
|
|
7
|
+
* entity E {
|
|
8
|
+
* x: Integer
|
|
9
|
+
* }
|
|
10
|
+
*
|
|
11
|
+
* entity F {
|
|
12
|
+
* y: E.x // <- ref
|
|
13
|
+
* }
|
|
14
|
+
* ```
|
|
15
|
+
*
|
|
16
|
+
* @param {{type: any}} element
|
|
17
|
+
* @returns boolean
|
|
18
|
+
*/
|
|
19
|
+
const isReferenceType = (element) => element.type && Object.hasOwn(element.type, 'ref')
|
|
20
|
+
|
|
21
|
+
module.exports = {
|
|
22
|
+
isReferenceType
|
|
23
|
+
}
|
|
@@ -6,6 +6,7 @@ const { Buffer, SourceFile, Path, Library, baseDefinitions } = require('../file'
|
|
|
6
6
|
const { deepRequire, createToManyAssociation, createToOneAssociation, createArrayOf, createCompositionOfMany, createCompositionOfOne } = require('./wrappers')
|
|
7
7
|
const { StructuredInlineDeclarationResolver } = require('./inline')
|
|
8
8
|
const { isInlineEnumType, propertyToInlineEnumName } = require('./enum')
|
|
9
|
+
const { isReferenceType } = require('./reference')
|
|
9
10
|
|
|
10
11
|
/** @typedef {{ cardinality?: { max?: '*' | number } }} EntityCSN */
|
|
11
12
|
/** @typedef {{ definitions?: Object<string, EntityCSN> }} CSN */
|
|
@@ -55,7 +56,7 @@ const Builtins = {
|
|
|
55
56
|
Float: 'number',
|
|
56
57
|
Double: 'number',
|
|
57
58
|
Boolean: 'boolean',
|
|
58
|
-
// note: the date-related types
|
|
59
|
+
// note: the date-related types are strings on purpose, which reflects their runtime behaviour
|
|
59
60
|
Date: 'string', // yyyy-mm-dd
|
|
60
61
|
DateTime: 'string', // yyyy-mm-dd + time + TZ (precision: seconds
|
|
61
62
|
Time: 'string',
|
|
@@ -370,10 +371,18 @@ class Resolver {
|
|
|
370
371
|
result.type = '{}'
|
|
371
372
|
result.isInlineDeclaration = true
|
|
372
373
|
} else {
|
|
373
|
-
if (isInlineEnumType(element, this.csn)) {
|
|
374
|
+
if (!isReferenceType(element) && isInlineEnumType(element, this.csn)) {
|
|
374
375
|
// element.parent is only set if the enum is attached to an entity's property.
|
|
375
376
|
// If it is missing then we are dealing with an inline parameter type of an action.
|
|
376
|
-
|
|
377
|
+
// Edge case: element.parent is set, but no .name property is attached. This happens
|
|
378
|
+
// for inline enums inside types:
|
|
379
|
+
// ```cds
|
|
380
|
+
// type T {
|
|
381
|
+
// x : String enum { ... }; // no element.name for x
|
|
382
|
+
// }
|
|
383
|
+
// ```
|
|
384
|
+
// In that case, we currently resolve to the more general type (cds.String, here)
|
|
385
|
+
if (element.parent?.name) {
|
|
377
386
|
result.isInlineDeclaration = true
|
|
378
387
|
// we use the singular as the initial declaration of these enums takes place
|
|
379
388
|
// while defining the singular class. Which therefore uses the singular over the plural name.
|
package/lib/csn.js
CHANGED
|
@@ -234,10 +234,12 @@ function amendCSN(csn) {
|
|
|
234
234
|
*/
|
|
235
235
|
const isView = entity => entity.query && !entity.projection
|
|
236
236
|
|
|
237
|
+
const isDraftEnabled = entity => entity['@odata.draft.enabled'] === true
|
|
238
|
+
|
|
237
239
|
/**
|
|
238
240
|
* @see isView
|
|
239
241
|
* Unresolved entities have to be looked up from inferred csn.
|
|
240
242
|
*/
|
|
241
243
|
const isUnresolved = entity => entity._unresolved === true
|
|
242
244
|
|
|
243
|
-
module.exports = { amendCSN, isView, isUnresolved, propagateForeignKeys }
|
|
245
|
+
module.exports = { amendCSN, isView, isDraftEnabled, isUnresolved, propagateForeignKeys }
|
package/lib/file.js
CHANGED
|
@@ -93,10 +93,13 @@ class Library extends File {
|
|
|
93
93
|
* Source file containing several buffers.
|
|
94
94
|
*/
|
|
95
95
|
class SourceFile extends File {
|
|
96
|
+
/**
|
|
97
|
+
* @param {string | Path} path
|
|
98
|
+
*/
|
|
96
99
|
constructor(path) {
|
|
97
100
|
super()
|
|
98
101
|
/** @type {Path} */
|
|
99
|
-
this.path = new Path(path.split('.'))
|
|
102
|
+
this.path = path instanceof Path ? path : new Path(path.split('.'))
|
|
100
103
|
/** @type {Object} */
|
|
101
104
|
this.imports = {}
|
|
102
105
|
/** @type {Buffer} */
|
|
@@ -480,6 +483,33 @@ class Buffer {
|
|
|
480
483
|
add(part) {
|
|
481
484
|
this.parts.push(this.currentIndent + part)
|
|
482
485
|
}
|
|
486
|
+
|
|
487
|
+
/**
|
|
488
|
+
* Adds an element to the buffer with one level of indent.
|
|
489
|
+
* @param {string | (() => void)} part either a string or a function. If it is a string, it is added to the buffer.
|
|
490
|
+
* If not, it is expected to be a function that manipulates the buffer as a side effect.
|
|
491
|
+
*/
|
|
492
|
+
addIndented(part) {
|
|
493
|
+
this.indent()
|
|
494
|
+
if (typeof part === 'function') {
|
|
495
|
+
part()
|
|
496
|
+
} else if (Array.isArray(part)) {
|
|
497
|
+
part.forEach(p => this.add(p))
|
|
498
|
+
}
|
|
499
|
+
this.outdent()
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
/**
|
|
503
|
+
* Adds an element to a buffer with one level of indent and and opener and closer surrounding it.
|
|
504
|
+
* @param {string} opener the string to put before the indent
|
|
505
|
+
* @param {string} content the content to indent (see {@link addIndented})
|
|
506
|
+
* @param {string} closer the string to put after the indent
|
|
507
|
+
*/
|
|
508
|
+
addIndentedBlock(opener, content, closer) {
|
|
509
|
+
this.add(opener)
|
|
510
|
+
this.addIndented(content)
|
|
511
|
+
this.add(closer)
|
|
512
|
+
}
|
|
483
513
|
}
|
|
484
514
|
|
|
485
515
|
/**
|
|
@@ -548,6 +578,38 @@ class Path {
|
|
|
548
578
|
}
|
|
549
579
|
}
|
|
550
580
|
|
|
581
|
+
// TODO: having the repository pattern in place we can separate (some of) the printing logic from the visitor.
|
|
582
|
+
// Most of it hinges primarily on resolving specific files. We can now pass the repository and the resolver to a printer.
|
|
583
|
+
class FileRepository {
|
|
584
|
+
#files = {}
|
|
585
|
+
|
|
586
|
+
/**
|
|
587
|
+
* @param {string} name
|
|
588
|
+
* @param {SourceFile} file
|
|
589
|
+
*/
|
|
590
|
+
add(name, file) {
|
|
591
|
+
this.#files[name] = file
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
/**
|
|
595
|
+
* Determines the file corresponding to the namespace.
|
|
596
|
+
* If no such file exists yet, it is created first.
|
|
597
|
+
* @param {string | Path} path the name of the namespace (foo.bar.baz)
|
|
598
|
+
* @returns {SourceFile} the file corresponding to that namespace name
|
|
599
|
+
*/
|
|
600
|
+
getNamespaceFile(path) {
|
|
601
|
+
const key = path instanceof Path ? path.asNamespace() : path
|
|
602
|
+
return (this.#files[key] ??= new SourceFile(path))
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
/**
|
|
606
|
+
* @returns {SourceFile[]}
|
|
607
|
+
*/
|
|
608
|
+
getFiles() {
|
|
609
|
+
return Object.values(this.#files)
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
|
|
551
613
|
/**
|
|
552
614
|
* Base definitions used throughout the typing process,
|
|
553
615
|
* such as Associations and Compositions.
|
|
@@ -583,7 +645,7 @@ export type EntitySet<T> = T[] & {
|
|
|
583
645
|
|
|
584
646
|
export type DeepRequired<T> = {
|
|
585
647
|
[K in keyof T]: DeepRequired<T[K]>
|
|
586
|
-
} & Required<T>;
|
|
648
|
+
} & Exclude<Required<T>, null>;
|
|
587
649
|
`)
|
|
588
650
|
|
|
589
651
|
/**
|
|
@@ -618,6 +680,7 @@ module.exports = {
|
|
|
618
680
|
Library,
|
|
619
681
|
Buffer,
|
|
620
682
|
File,
|
|
683
|
+
FileRepository,
|
|
621
684
|
SourceFile,
|
|
622
685
|
Path,
|
|
623
686
|
writeout,
|
package/lib/visitor.js
CHANGED
|
@@ -2,14 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
const util = require('./util')
|
|
4
4
|
|
|
5
|
-
const { amendCSN, isView, isUnresolved, propagateForeignKeys } = require('./csn')
|
|
5
|
+
const { amendCSN, isView, isUnresolved, propagateForeignKeys, isDraftEnabled } = require('./csn')
|
|
6
6
|
// eslint-disable-next-line no-unused-vars
|
|
7
|
-
const { SourceFile, baseDefinitions, Buffer } = require('./file')
|
|
7
|
+
const { SourceFile, FileRepository, 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
12
|
const { csnToEnumPairs, propertyToInlineEnumName, isInlineEnumType, stringifyEnumType } = require('./components/enum')
|
|
13
|
+
const { isReferenceType } = require('./components/reference')
|
|
13
14
|
|
|
14
15
|
/** @typedef {import('./file').File} File */
|
|
15
16
|
/** @typedef {{ entity: String }} Context */
|
|
@@ -54,7 +55,7 @@ class Visitor {
|
|
|
54
55
|
* @returns {File[]} a full list of files to be written
|
|
55
56
|
*/
|
|
56
57
|
getWriteoutFiles() {
|
|
57
|
-
return
|
|
58
|
+
return this.fileRepository.getFiles().concat(this.resolver.getUsedLibraries())
|
|
58
59
|
}
|
|
59
60
|
|
|
60
61
|
/**
|
|
@@ -74,9 +75,9 @@ class Visitor {
|
|
|
74
75
|
/** @type {Resolver} */
|
|
75
76
|
this.resolver = new Resolver(this)
|
|
76
77
|
|
|
77
|
-
/** @type {
|
|
78
|
-
this.
|
|
79
|
-
this.
|
|
78
|
+
/** @type {FileRepository} */
|
|
79
|
+
this.fileRepository = new FileRepository()
|
|
80
|
+
this.fileRepository.add(baseDefinitions.path.asNamespace(), baseDefinitions)
|
|
80
81
|
this.inlineDeclarationResolver =
|
|
81
82
|
this.options.inlineDeclarations === 'structured'
|
|
82
83
|
? new StructuredInlineDeclarationResolver(this)
|
|
@@ -85,16 +86,6 @@ class Visitor {
|
|
|
85
86
|
this.visitDefinitions()
|
|
86
87
|
}
|
|
87
88
|
|
|
88
|
-
/**
|
|
89
|
-
* Determines the file corresponding to the namespace.
|
|
90
|
-
* If no such file exists yet, it is created first.
|
|
91
|
-
* @param {string} path the name of the namespace (foo.bar.baz)
|
|
92
|
-
* @returns {SourceFile} the file corresponding to that namespace name
|
|
93
|
-
*/
|
|
94
|
-
getNamespaceFile(path) {
|
|
95
|
-
return (this.files[path] ??= new SourceFile(path))
|
|
96
|
-
}
|
|
97
|
-
|
|
98
89
|
/**
|
|
99
90
|
* Visits all definitions within the CSN definitions.
|
|
100
91
|
*/
|
|
@@ -163,7 +154,7 @@ class Visitor {
|
|
|
163
154
|
#aspectify(name, entity, buffer, cleanName = undefined) {
|
|
164
155
|
const clean = cleanName ?? this.resolver.trimNamespace(name)
|
|
165
156
|
const ns = this.resolver.resolveNamespace(name.split('.'))
|
|
166
|
-
const file = this.getNamespaceFile(ns)
|
|
157
|
+
const file = this.fileRepository.getNamespaceFile(ns)
|
|
167
158
|
|
|
168
159
|
const identSingular = (name) => name
|
|
169
160
|
const identAspect = (name) => `_${name}Aspect`
|
|
@@ -171,61 +162,54 @@ class Visitor {
|
|
|
171
162
|
this.contexts.push({ entity: name })
|
|
172
163
|
|
|
173
164
|
// CLASS ASPECT
|
|
174
|
-
buffer.
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
165
|
+
buffer.addIndentedBlock(`export function ${identAspect(clean)}<TBase extends new (...args: any[]) => object>(Base: TBase) {`, function () {
|
|
166
|
+
buffer.addIndentedBlock(`return class ${clean} extends Base {`, function () {
|
|
167
|
+
const enums = []
|
|
168
|
+
const exclusions = new Set(entity.projection?.excluding ?? [])
|
|
169
|
+
const elements = Object.entries(entity.elements ?? {}).filter(([ename]) => !exclusions.has(ename))
|
|
170
|
+
for (const [ename, element] of elements) {
|
|
171
|
+
this.visitElement(ename, element, file, buffer)
|
|
172
|
+
|
|
173
|
+
// make foreign keys explicit
|
|
174
|
+
if ('target' in element) {
|
|
175
|
+
// lookup in cds.definitions can fail for inline structs.
|
|
176
|
+
// We don't really have to care for this case, as keys from such structs are _not_ propagated to
|
|
177
|
+
// the containing entity.
|
|
178
|
+
for (const [kname, kelement] of this.#keys(element.target)) {
|
|
179
|
+
if (this.resolver.getMaxCardinality(element) === 1) { // FIXME: kelement?
|
|
180
|
+
const foreignKey = `${ename}_${kname}`
|
|
181
|
+
if (Object.hasOwn(entity.elements, foreignKey)) {
|
|
182
|
+
this.logger.error(`Attempting to generate a foreign key reference called '${foreignKey}' in type definition for entity ${name}. But a property of that name is already defined explicitly. Consider renaming that property.`)
|
|
183
|
+
} else {
|
|
184
|
+
kelement.isRefNotNull = !!element.notNull || !!element.key
|
|
185
|
+
this.visitElement(foreignKey, kelement, file, buffer)
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
194
189
|
}
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
// store inline enums for later handling, as they have to go into one common "static elements" wrapper
|
|
199
|
-
if (isInlineEnumType(element, this.csn.xtended)) {
|
|
200
|
-
enums.push(element)
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
190
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
buffer.add('static actions: {')
|
|
210
|
-
buffer.indent()
|
|
211
|
-
for (const [aname, action] of Object.entries(entity.actions ?? {})) {
|
|
212
|
-
buffer.add(
|
|
213
|
-
SourceFile.stringifyLambda({
|
|
214
|
-
name: aname,
|
|
215
|
-
parameters: this.#stringifyFunctionParams(action.params, file),
|
|
216
|
-
returns: action.returns ? this.resolver.resolveAndRequire(action.returns, file).typeName : 'any'
|
|
217
|
-
//initialiser: `undefined as unknown as typeof ${clean}.${aname}`,
|
|
218
|
-
})
|
|
219
|
-
)
|
|
220
|
-
}
|
|
221
|
-
buffer.outdent()
|
|
222
|
-
buffer.outdent()
|
|
223
|
-
buffer.add('}') // end of actions
|
|
191
|
+
// store inline enums for later handling, as they have to go into one common "static elements" wrapper
|
|
192
|
+
if (isInlineEnumType(element, this.csn.xtended)) {
|
|
193
|
+
enums.push(element)
|
|
194
|
+
}
|
|
195
|
+
}
|
|
224
196
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
197
|
+
buffer.addIndented(function() {
|
|
198
|
+
for (const e of enums) {
|
|
199
|
+
buffer.add(`static ${e.name} = ${propertyToInlineEnumName(clean, e.name)}`)
|
|
200
|
+
file.addInlineEnum(clean, name, e.name, csnToEnumPairs(e, {unwrapVals: true}))
|
|
201
|
+
}
|
|
202
|
+
const actions = Object.entries(entity.actions ?? {})
|
|
203
|
+
buffer.addIndentedBlock('static actions: {',
|
|
204
|
+
actions.map(([aname, action]) => SourceFile.stringifyLambda({
|
|
205
|
+
name: aname,
|
|
206
|
+
parameters: this.#stringifyFunctionParams(action.params, file),
|
|
207
|
+
returns: action.returns ? this.resolver.resolveAndRequire(action.returns, file).typeName : 'any'
|
|
208
|
+
}))
|
|
209
|
+
, '}') // end of actions
|
|
210
|
+
}.bind(this))
|
|
211
|
+
}.bind(this), '};') // end of generated class
|
|
212
|
+
}.bind(this), '}') // end of aspect
|
|
229
213
|
|
|
230
214
|
// CLASS WITH ADDED ASPECTS
|
|
231
215
|
file.addImport(baseDefinitions.path)
|
|
@@ -250,12 +234,8 @@ class Visitor {
|
|
|
250
234
|
this.contexts.pop()
|
|
251
235
|
}
|
|
252
236
|
|
|
253
|
-
#isDraftEnabled(entity) {
|
|
254
|
-
return entity['@odata.draft.enabled'] === true
|
|
255
|
-
}
|
|
256
|
-
|
|
257
237
|
#staticClassContents(clean, entity) {
|
|
258
|
-
return
|
|
238
|
+
return isDraftEnabled(entity) ? [`static drafts: typeof ${clean}`] : []
|
|
259
239
|
}
|
|
260
240
|
|
|
261
241
|
#printEntity(name, entity) {
|
|
@@ -263,7 +243,7 @@ class Visitor {
|
|
|
263
243
|
const overrideNameProperty = (clazz, content) => `Object.defineProperty(${clazz}, 'name', { value: '${content}' })`
|
|
264
244
|
const clean = this.resolver.trimNamespace(name)
|
|
265
245
|
const ns = this.resolver.resolveNamespace(name.split('.'))
|
|
266
|
-
const file = this.getNamespaceFile(ns)
|
|
246
|
+
const file = this.fileRepository.getNamespaceFile(ns)
|
|
267
247
|
// entities are expected to be in plural anyway, so we would favour the regular name.
|
|
268
248
|
// If the user decides to pass a @plural annotation, that gets precedence over the regular name.
|
|
269
249
|
let plural = this.resolver.trimNamespace(util.getPluralAnnotation(entity) ? util.plural4(entity, false) : name)
|
|
@@ -280,7 +260,7 @@ class Visitor {
|
|
|
280
260
|
}
|
|
281
261
|
if (singular in this.csn.xtended.definitions) {
|
|
282
262
|
this.logger.error(
|
|
283
|
-
`Derived singular '${singular}' for your entity '${name}', already exists. The resulting types will be erronous.
|
|
263
|
+
`Derived singular '${singular}' for your entity '${name}', already exists. The resulting types will be erronous. Consider using '@singular:'/ '@plural:' annotations in your model or move the offending declarations into different namespaces to resolve this collision.`
|
|
284
264
|
)
|
|
285
265
|
}
|
|
286
266
|
file.addClass(singular, name)
|
|
@@ -338,7 +318,9 @@ class Visitor {
|
|
|
338
318
|
}
|
|
339
319
|
|
|
340
320
|
#stringifyFunctionParamType(type, file) {
|
|
341
|
-
|
|
321
|
+
// if type.type is not 'cds.String', 'cds.Integer', ..., then we are actually looking
|
|
322
|
+
// at a named enum type. In that case also resolve that type name
|
|
323
|
+
return type.enum && type.type.startsWith('cds.')
|
|
342
324
|
? stringifyEnumType(csnToEnumPairs(type))
|
|
343
325
|
: this.inlineDeclarationResolver.getPropertyDatatype(this.resolver.resolveAndRequire(type, file))
|
|
344
326
|
}
|
|
@@ -347,7 +329,7 @@ class Visitor {
|
|
|
347
329
|
// FIXME: mostly duplicate of printAction -> reuse
|
|
348
330
|
this.logger.debug(`Printing function ${name}:\n${JSON.stringify(func, null, 2)}`)
|
|
349
331
|
const ns = this.resolver.resolveNamespace(name.split('.'))
|
|
350
|
-
const file = this.getNamespaceFile(ns)
|
|
332
|
+
const file = this.fileRepository.getNamespaceFile(ns)
|
|
351
333
|
const params = this.#stringifyFunctionParams(func.params, file)
|
|
352
334
|
const returns = this.resolver.visitor.inlineDeclarationResolver.getPropertyDatatype(
|
|
353
335
|
this.resolver.resolveAndRequire(func.returns, file)
|
|
@@ -358,7 +340,7 @@ class Visitor {
|
|
|
358
340
|
#printAction(name, action) {
|
|
359
341
|
this.logger.debug(`Printing action ${name}:\n${JSON.stringify(action, null, 2)}`)
|
|
360
342
|
const ns = this.resolver.resolveNamespace(name.split('.'))
|
|
361
|
-
const file = this.getNamespaceFile(ns)
|
|
343
|
+
const file = this.fileRepository.getNamespaceFile(ns)
|
|
362
344
|
const params = this.#stringifyFunctionParams(action.params, file)
|
|
363
345
|
const returns = this.resolver.visitor.inlineDeclarationResolver.getPropertyDatatype(
|
|
364
346
|
this.resolver.resolveAndRequire(action.returns, file)
|
|
@@ -368,10 +350,9 @@ class Visitor {
|
|
|
368
350
|
|
|
369
351
|
#printType(name, type) {
|
|
370
352
|
this.logger.debug(`Printing type ${name}:\n${JSON.stringify(type, null, 2)}`)
|
|
371
|
-
const clean = this.resolver.
|
|
372
|
-
const
|
|
373
|
-
|
|
374
|
-
if ('enum' in type) {
|
|
353
|
+
const [ns, clean] = this.resolver.untangle(name)
|
|
354
|
+
const file = this.fileRepository.getNamespaceFile(ns)
|
|
355
|
+
if ('enum' in type && !isReferenceType(type)) { // skip references to enums
|
|
375
356
|
file.addEnum(name, clean, csnToEnumPairs(type))
|
|
376
357
|
} else {
|
|
377
358
|
// alias
|
|
@@ -382,9 +363,8 @@ class Visitor {
|
|
|
382
363
|
|
|
383
364
|
#printAspect(name, aspect) {
|
|
384
365
|
this.logger.debug(`Printing aspect ${name}`)
|
|
385
|
-
const clean = this.resolver.
|
|
386
|
-
const
|
|
387
|
-
const file = this.getNamespaceFile(ns)
|
|
366
|
+
const [ns, clean] = this.resolver.untangle(name)
|
|
367
|
+
const file = this.fileRepository.getNamespaceFile(ns)
|
|
388
368
|
// aspects are technically classes and can therefore be added to the list of defined classes.
|
|
389
369
|
// Still, when using them as mixins for a class, they need to already be defined.
|
|
390
370
|
// So we separate them into another buffer which is printed before the classes.
|
|
@@ -395,28 +375,25 @@ class Visitor {
|
|
|
395
375
|
|
|
396
376
|
#printEvent(name, event) {
|
|
397
377
|
this.logger.debug(`Printing event ${name}`)
|
|
398
|
-
const clean = this.resolver.
|
|
399
|
-
const
|
|
400
|
-
const file = this.getNamespaceFile(ns)
|
|
378
|
+
const [ns, clean] = this.resolver.untangle(name)
|
|
379
|
+
const file = this.fileRepository.getNamespaceFile(ns)
|
|
401
380
|
file.addEvent(clean, name)
|
|
402
381
|
const buffer = file.events.buffer
|
|
403
382
|
buffer.add('// event')
|
|
404
|
-
buffer.
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
this
|
|
412
|
-
buffer.outdent()
|
|
413
|
-
buffer.add('}')
|
|
383
|
+
buffer.addIndentedBlock(`export class ${clean} {`, function() {
|
|
384
|
+
const propOpt = this.options.propertiesOptional
|
|
385
|
+
this.options.propertiesOptional = false
|
|
386
|
+
for (const [ename, element] of Object.entries(event.elements ?? {})) {
|
|
387
|
+
this.visitElement(ename, element, file, buffer)
|
|
388
|
+
}
|
|
389
|
+
this.options.propertiesOptional = propOpt
|
|
390
|
+
}.bind(this), '}')
|
|
414
391
|
}
|
|
415
392
|
|
|
416
393
|
#printService(name, service) {
|
|
417
394
|
this.logger.debug(`Printing service ${name}:\n${JSON.stringify(service, null, 2)}`)
|
|
418
395
|
const ns = this.resolver.resolveNamespace(name)
|
|
419
|
-
const file = this.getNamespaceFile(ns)
|
|
396
|
+
const file = this.fileRepository.getNamespaceFile(ns)
|
|
420
397
|
// service.name is clean of namespace
|
|
421
398
|
file.services.buffer.add(`export default { name: '${service.name}' }`)
|
|
422
399
|
file.addService(service.name)
|
|
@@ -439,12 +416,16 @@ class Visitor {
|
|
|
439
416
|
case 'function':
|
|
440
417
|
this.#printAction(name, entity)
|
|
441
418
|
break
|
|
442
|
-
case 'type':
|
|
443
|
-
this.#printType(name, entity)
|
|
444
|
-
break
|
|
445
419
|
case 'aspect':
|
|
446
420
|
this.#printAspect(name, entity)
|
|
447
421
|
break
|
|
422
|
+
case 'type': {
|
|
423
|
+
// types like inline definitions can be used very similarly to entities.
|
|
424
|
+
// They can be extended, contain inline enums, etc., so we treat them as entities.
|
|
425
|
+
const handler = entity.elements ? this.#printEntity : this.#printType
|
|
426
|
+
handler.bind(this)(name, entity)
|
|
427
|
+
break
|
|
428
|
+
}
|
|
448
429
|
case 'event':
|
|
449
430
|
this.#printEvent(name, entity)
|
|
450
431
|
break
|
|
@@ -452,7 +433,7 @@ class Visitor {
|
|
|
452
433
|
this.#printService(name, entity)
|
|
453
434
|
break
|
|
454
435
|
default:
|
|
455
|
-
this.logger.
|
|
436
|
+
this.logger.debug(`Unhandled entity kind '${entity.kind}'.`)
|
|
456
437
|
}
|
|
457
438
|
}
|
|
458
439
|
|