@cap-js/cds-typer 0.11.1 → 0.13.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 +15 -2
- package/lib/cli.js +0 -4
- package/lib/components/enum.js +34 -26
- package/lib/components/inline.js +16 -3
- package/lib/components/resolver.js +17 -6
- package/lib/csn.js +0 -43
- package/lib/file.js +15 -15
- package/lib/visitor.js +24 -10
- package/package.json +3 -4
package/CHANGELOG.md
CHANGED
|
@@ -4,12 +4,25 @@ 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.14.0 - TBD
|
|
8
|
+
|
|
9
|
+
## Version 0.13.0 - 2023-12-06
|
|
10
|
+
### Changes
|
|
11
|
+
- Enums are now generated ecplicitly in the respective _index.js_ files and don't have to extract their values from the model at runtime anymore
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
- The `excluding` clause in projections now actually excludes the specified properties in the generated types
|
|
15
|
+
|
|
16
|
+
## Version 0.12.0 - 2023-11-23
|
|
8
17
|
|
|
9
18
|
### Changed
|
|
19
|
+
- Generate `cds.LargeBinary` as string, buffer, _or readable_ in the case of media content
|
|
10
20
|
|
|
11
21
|
### Added
|
|
22
|
+
- Added support for the `not null` modifier
|
|
23
|
+
|
|
12
24
|
### Fixed
|
|
25
|
+
- Now using names of enum values in generated _index.js_ files if no explicit value is present
|
|
13
26
|
|
|
14
27
|
## Version 0.11.1 - 2023-10-12
|
|
15
28
|
|
|
@@ -17,7 +30,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/).
|
|
|
17
30
|
|
|
18
31
|
### Added
|
|
19
32
|
### Fixed
|
|
20
|
-
Fixed how service names are exported as default export
|
|
33
|
+
- Fixed how service names are exported as default export
|
|
21
34
|
|
|
22
35
|
## Version 0.11.0 - 2023-10-10
|
|
23
36
|
|
package/lib/cli.js
CHANGED
package/lib/components/enum.js
CHANGED
|
@@ -31,8 +31,8 @@ function printEnum(buffer, name, kvs, options = {}) {
|
|
|
31
31
|
buffer.indent()
|
|
32
32
|
const vals = new Set()
|
|
33
33
|
for (const [k, v] of kvs) {
|
|
34
|
-
buffer.add(`${k}: ${
|
|
35
|
-
vals.add(
|
|
34
|
+
buffer.add(`${k}: ${v},`)
|
|
35
|
+
vals.add(v?.val ?? v) // in case of wrapped vals we need to unwrap here for the type
|
|
36
36
|
}
|
|
37
37
|
buffer.outdent()
|
|
38
38
|
buffer.add('} as const;')
|
|
@@ -41,27 +41,32 @@ function printEnum(buffer, name, kvs, options = {}) {
|
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
// in case of strings, wrap in quotes and fallback to key to make sure values are attached for every key
|
|
44
|
-
const enumVal = (key, value, enumType) => enumType === 'cds.String' ? `${value ?? key}` : value
|
|
44
|
+
const enumVal = (key, value, enumType) => enumType === 'cds.String' ? JSON.stringify(`${value ?? key}`) : value
|
|
45
45
|
|
|
46
46
|
/**
|
|
47
|
+
* Converts a CSN type describing an enum into a list of kv-pairs.
|
|
48
|
+
* Values from CSN are unwrapped from their `.val` structure and
|
|
49
|
+
* will fall back to the key if no value is provided.
|
|
50
|
+
*
|
|
47
51
|
* @param {{enum: {[key: name]: string}, type: string}} enumCsn
|
|
48
52
|
* @param {{unwrapVals: boolean}} options if `unwrapVals` is passed,
|
|
49
53
|
* then the CSN structure `{val:x}` is flattened to just `x`.
|
|
50
54
|
* Retaining `val` is closer to the actual CSN structure and should be used where we want
|
|
51
|
-
* to mimic the runtime as closely as possible (
|
|
55
|
+
* to mimic the runtime as closely as possible (inline enum types).
|
|
52
56
|
* Stripping that additional wrapper would be more readable for users.
|
|
57
|
+
*
|
|
53
58
|
* @example
|
|
54
59
|
* ```ts
|
|
55
|
-
* const csn = {enum: {
|
|
56
|
-
*
|
|
57
|
-
*
|
|
60
|
+
* const csn = {enum: {X: {val: 'a'}, Y: {val: 'b'}, Z: {}}}
|
|
61
|
+
* csnToEnumPairs(csn) // -> [['X', 'a'], ['Y': 'b'], ['Z': 'Z']]
|
|
62
|
+
* csnToEnumPairs(csn, {unwrapVals: false}) // -> [['X', {val:'a'}], ['Y': {val:'b'}], ['Z':'Z']]
|
|
58
63
|
* ```
|
|
59
64
|
*/
|
|
60
|
-
const
|
|
65
|
+
const csnToEnumPairs = ({enum: enm, type}, options = {}) => {
|
|
61
66
|
options = {...{unwrapVals: true}, ...options}
|
|
62
67
|
return Object.entries(enm).map(([k, v]) => {
|
|
63
68
|
const val = enumVal(k, v.val, type)
|
|
64
|
-
return [k, options.unwrapVals ? val : { val }]
|
|
69
|
+
return [k, (options.unwrapVals ? val : { val })]
|
|
65
70
|
})
|
|
66
71
|
}
|
|
67
72
|
|
|
@@ -69,7 +74,7 @@ const csnToEnum = ({enum: enm, type}, options = {}) => {
|
|
|
69
74
|
* @param {string} entity
|
|
70
75
|
* @param {string} property
|
|
71
76
|
*/
|
|
72
|
-
const
|
|
77
|
+
const propertyToInlineEnumName = (entity, property) => `${entity}_${property}`
|
|
73
78
|
|
|
74
79
|
/**
|
|
75
80
|
* A type is considered to be an inline enum, iff it has a `.enum` property
|
|
@@ -82,27 +87,30 @@ const propertyToAnonymousEnumName = (entity, property) => `${entity}_${property}
|
|
|
82
87
|
*/
|
|
83
88
|
const isInlineEnumType = (element, csn) => element.enum && !(element.type in csn.definitions)
|
|
84
89
|
|
|
85
|
-
const stringifyEnumImplementation = (name, enm) => `module.exports.${name} = Object.fromEntries(Object.entries(${enm}).map(([k,v]) => [k,v.val]))`
|
|
86
|
-
|
|
87
|
-
/**
|
|
88
|
-
* @param {string} name
|
|
89
|
-
* @param {string} fq
|
|
90
|
-
* @returns {string}
|
|
91
|
-
*/
|
|
92
|
-
const stringifyNamedEnum = (name, fq) => stringifyEnumImplementation(name, `cds.model.definitions['${fq}'].enum`)
|
|
93
90
|
/**
|
|
91
|
+
* Stringifies an enum into a runtime artifact.
|
|
92
|
+
* ```cds
|
|
93
|
+
* type Language: String enum {
|
|
94
|
+
* DE = "German";
|
|
95
|
+
* EN = "English";
|
|
96
|
+
* FR;
|
|
97
|
+
* }
|
|
98
|
+
* ```
|
|
99
|
+
* becomes
|
|
100
|
+
*
|
|
101
|
+
* ```js
|
|
102
|
+
* module.exports.Language = { DE: "German", EN: "English", FR: "FR" }
|
|
103
|
+
* ```
|
|
94
104
|
* @param {string} name
|
|
95
|
-
* @param {string}
|
|
96
|
-
* @param {string} property
|
|
97
|
-
* @returns {string}
|
|
105
|
+
* @param {[string, string][]} kvs a list of key-value pairs. Values that are falsey are replaced by
|
|
98
106
|
*/
|
|
99
|
-
const
|
|
107
|
+
const stringifyEnumImplementation = (name, kvs) => `module.exports.${name} = { ${kvs.map(([k,v]) => `${k}: ${v}`).join(', ')} }`
|
|
108
|
+
|
|
100
109
|
|
|
101
110
|
module.exports = {
|
|
102
111
|
printEnum,
|
|
103
|
-
|
|
104
|
-
|
|
112
|
+
csnToEnumPairs,
|
|
113
|
+
propertyToInlineEnumName,
|
|
105
114
|
isInlineEnumType,
|
|
106
|
-
|
|
107
|
-
stringifyAnonymousEnum
|
|
115
|
+
stringifyEnumImplementation
|
|
108
116
|
}
|
package/lib/components/inline.js
CHANGED
|
@@ -17,6 +17,7 @@ class InlineDeclarationResolver {
|
|
|
17
17
|
* @protected
|
|
18
18
|
* @abstract
|
|
19
19
|
*/
|
|
20
|
+
// eslint-disable-next-line no-unused-vars
|
|
20
21
|
printInlineType(name, type, buffer, statementEnd) { /* abstract */ }
|
|
21
22
|
|
|
22
23
|
/**
|
|
@@ -84,6 +85,17 @@ class InlineDeclarationResolver {
|
|
|
84
85
|
return this.visitor.options.propertiesOptional ? '?:' : ':'
|
|
85
86
|
}
|
|
86
87
|
|
|
88
|
+
/**
|
|
89
|
+
* It returns TypeScript datatype for provided TS property
|
|
90
|
+
* @param {{typeName: string, typeInfo: TypeResolveInfo & { inflection: Inflection } }} type
|
|
91
|
+
* @param {string} typeName name of the TypeScript property
|
|
92
|
+
* @return {string} the datatype to be presented on TypeScript layer
|
|
93
|
+
* @public
|
|
94
|
+
*/
|
|
95
|
+
getPropertyDatatype(type, typeName = type.typeName) {
|
|
96
|
+
return type.typeInfo.isNotNull ? typeName : `${typeName} | null`
|
|
97
|
+
}
|
|
98
|
+
|
|
87
99
|
/** @param {import('../visitor').Visitor} visitor */
|
|
88
100
|
constructor(visitor) {
|
|
89
101
|
this.visitor = visitor
|
|
@@ -111,6 +123,7 @@ class InlineDeclarationResolver {
|
|
|
111
123
|
* @public
|
|
112
124
|
* @abstract
|
|
113
125
|
*/
|
|
126
|
+
// eslint-disable-next-line no-unused-vars
|
|
114
127
|
getTypeLookup(members) { /* abstract */ return '' }
|
|
115
128
|
}
|
|
116
129
|
|
|
@@ -142,7 +155,7 @@ class FlatInlineDeclarationResolver extends InlineDeclarationResolver {
|
|
|
142
155
|
flatten(prefix, type) {
|
|
143
156
|
return type.typeInfo.structuredType
|
|
144
157
|
? Object.entries(type.typeInfo.structuredType).map(([k,v]) => this.flatten(`${this.prefix(prefix)}${k}`, v))
|
|
145
|
-
: [`${prefix}${this.getPropertyTypeSeparator()} ${type
|
|
158
|
+
: [`${prefix}${this.getPropertyTypeSeparator()} ${this.getPropertyDatatype(type)}`]
|
|
146
159
|
}
|
|
147
160
|
|
|
148
161
|
printInlineType(name, type, buffer) {
|
|
@@ -192,9 +205,9 @@ class StructuredInlineDeclarationResolver extends InlineDeclarationResolver {
|
|
|
192
205
|
this.flatten(n, t, buffer)
|
|
193
206
|
}
|
|
194
207
|
buffer.outdent()
|
|
195
|
-
buffer.add(`}${lineEnding}`)
|
|
208
|
+
buffer.add(`}${this.getPropertyDatatype(type, '')}${lineEnding}`)
|
|
196
209
|
} else {
|
|
197
|
-
buffer.add(`${name}${this.getPropertyTypeSeparator()} ${type
|
|
210
|
+
buffer.add(`${name}${this.getPropertyTypeSeparator()} ${this.getPropertyDatatype(type)}${lineEnding}`)
|
|
198
211
|
}
|
|
199
212
|
this.printDepth--
|
|
200
213
|
return buffer
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
3
|
const util = require('../util')
|
|
4
|
-
|
|
4
|
+
// eslint-disable-next-line no-unused-vars
|
|
5
|
+
const { Buffer, SourceFile, Path, Library, baseDefinitions } = require('../file')
|
|
5
6
|
const { deepRequire, createToManyAssociation, createToOneAssociation, createArrayOf, createCompositionOfMany, createCompositionOfOne } = require('./wrappers')
|
|
6
|
-
const { StructuredInlineDeclarationResolver } = require(
|
|
7
|
-
const { isInlineEnumType, propertyToInlineEnumName
|
|
7
|
+
const { StructuredInlineDeclarationResolver } = require('./inline')
|
|
8
|
+
const { isInlineEnumType, propertyToInlineEnumName } = require('./enum')
|
|
8
9
|
|
|
9
10
|
/** @typedef {{ cardinality?: { max?: '*' | number } }} EntityCSN */
|
|
10
11
|
/** @typedef {{ definitions?: Object<string, EntityCSN> }} CSN */
|
|
@@ -22,6 +23,7 @@ const { isInlineEnumType, propertyToInlineEnumName, propertyToAnonymousEnumName
|
|
|
22
23
|
* ```
|
|
23
24
|
* @typedef {{
|
|
24
25
|
* isBuiltin: boolean,
|
|
26
|
+
* isNotNull: boolean,
|
|
25
27
|
* isInlineDeclaration: boolean,
|
|
26
28
|
* isForeignKeyReference: boolean,
|
|
27
29
|
* isArray: boolean,
|
|
@@ -41,7 +43,7 @@ const Builtins = {
|
|
|
41
43
|
String: 'string',
|
|
42
44
|
Binary: 'string',
|
|
43
45
|
LargeString: 'string',
|
|
44
|
-
LargeBinary: 'Buffer | string',
|
|
46
|
+
LargeBinary: 'Buffer | string | {value: import("stream").Readable, $mediaContentType: string, $mediaContentDispositionFilename?: string, $mediaContentDispositionType?: string}',
|
|
45
47
|
Integer: 'number',
|
|
46
48
|
UInt8: 'number',
|
|
47
49
|
Int16: 'number',
|
|
@@ -231,6 +233,8 @@ class Resolver {
|
|
|
231
233
|
|
|
232
234
|
if (toOne && toMany) {
|
|
233
235
|
const target = element.items ?? (typeof element.target === 'string' ? { type: element.target } : element.target)
|
|
236
|
+
/** set `notNull = true` to avoid repeated `| not null` TS construction */
|
|
237
|
+
target.notNull = true
|
|
234
238
|
const { singular, plural } = this.resolveAndRequire(target, file).typeInfo.inflection
|
|
235
239
|
typeName =
|
|
236
240
|
cardinality > 1 ? toMany(plural) : toOne(this.visitor.isSelfReference(target) ? 'this' : singular)
|
|
@@ -254,7 +258,7 @@ class Resolver {
|
|
|
254
258
|
}
|
|
255
259
|
|
|
256
260
|
if (element.type.ref?.length > 1) {
|
|
257
|
-
const [
|
|
261
|
+
const [, ...members] = element.type.ref
|
|
258
262
|
const lookup = this.visitor.inlineDeclarationResolver.getTypeLookup(members)
|
|
259
263
|
typeName = deepRequire(typeInfo.inflection.singular, lookup)
|
|
260
264
|
file.addImport(baseDefinitions.path)
|
|
@@ -348,11 +352,16 @@ class Resolver {
|
|
|
348
352
|
return element
|
|
349
353
|
}
|
|
350
354
|
|
|
355
|
+
const cardinality = this.getMaxCardinality(element)
|
|
356
|
+
|
|
351
357
|
const result = {
|
|
352
358
|
isBuiltin: false, // will be rectified in the corresponding handlers, if needed
|
|
353
359
|
isInlineDeclaration: false,
|
|
354
360
|
isForeignKeyReference: false,
|
|
355
361
|
isArray: false,
|
|
362
|
+
isNotNull: element?.isRefNotNull !== undefined
|
|
363
|
+
? element?.isRefNotNull
|
|
364
|
+
: element?.key || element?.notNull || cardinality > 1,
|
|
356
365
|
}
|
|
357
366
|
|
|
358
367
|
if (element?.type === undefined) {
|
|
@@ -365,7 +374,7 @@ class Resolver {
|
|
|
365
374
|
// we use the singular as the initial declaration of these enums takes place
|
|
366
375
|
// while defining the singular class. Which therefore uses the singular over the plural name.
|
|
367
376
|
const cleanEntityName = util.singular4(element.parent, true)
|
|
368
|
-
const enumName =
|
|
377
|
+
const enumName = propertyToInlineEnumName(cleanEntityName, element.name)
|
|
369
378
|
result.type = enumName
|
|
370
379
|
result.plainName = enumName
|
|
371
380
|
result.isInlineDeclaration = true
|
|
@@ -377,6 +386,8 @@ class Resolver {
|
|
|
377
386
|
// objects and arrays
|
|
378
387
|
if (element?.items) {
|
|
379
388
|
result.isArray = true
|
|
389
|
+
// TODO: re-implement this line once {element.notNull} will be provided for array-like elements
|
|
390
|
+
result.isNotNull = true
|
|
380
391
|
result.isBuiltin = true
|
|
381
392
|
this.resolveType(element.items, file)
|
|
382
393
|
//delete element.items
|
package/lib/csn.js
CHANGED
|
@@ -221,49 +221,6 @@ function propagateForeignKeys(csn) {
|
|
|
221
221
|
}
|
|
222
222
|
}
|
|
223
223
|
|
|
224
|
-
/**
|
|
225
|
-
* Entities inherit their ancestors annotations:
|
|
226
|
-
* https://cap.cloud.sap/docs/cds/cdl#annotation-propagation
|
|
227
|
-
* This is a problem if we annotate @singular/ @plural to an entity A,
|
|
228
|
-
* as we don't want all descendents B, C, ... to share the ancestor's
|
|
229
|
-
* annotated inflexion
|
|
230
|
-
* -> remove all such annotations that appear in a parent as well.
|
|
231
|
-
* BUT: we can't just delete the attributes. Imagine three classes
|
|
232
|
-
* A <- B <- C
|
|
233
|
-
* where A contains a @singular annotation.
|
|
234
|
-
* If we erase the annotation from B, C will still contain it and
|
|
235
|
-
* can not detect that its own annotation was inherited without
|
|
236
|
-
* travelling up the entire inheritance chain up to A.
|
|
237
|
-
* So instead, we monkey patch and maintain a dictionary "erased"
|
|
238
|
-
* when removing an annotation which we also check.
|
|
239
|
-
* @deprecated since we use the xtended flavour for CSN, we don't need to fix this anymore
|
|
240
|
-
*/
|
|
241
|
-
function propagateInflectionAnnotations(csn) {
|
|
242
|
-
const erase = (entity, parent, attr) => {
|
|
243
|
-
if (attr in entity) {
|
|
244
|
-
const ea = entity[attr]
|
|
245
|
-
if (parent[attr] === ea || (parent.erased && parent.erased[attr] === ea)) {
|
|
246
|
-
entity.erased ??= {}
|
|
247
|
-
entity.erased[attr] = ea
|
|
248
|
-
delete entity[attr]
|
|
249
|
-
//this.logger.info(`Removing inherited attribute ${attr} from ${entity.name}.`)
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
for (const entity of Object.values(csn.definitions)) {
|
|
255
|
-
let i = 0
|
|
256
|
-
while (
|
|
257
|
-
(getSingularAnnotation(entity) || getPluralAnnotation(entity)) &&
|
|
258
|
-
i < (entity.includes ?? []).length
|
|
259
|
-
) {
|
|
260
|
-
const parent = csn.definitions[entity.includes[i]]
|
|
261
|
-
Object.values(annotations).flat().forEach(an => erase(entity, parent, an))
|
|
262
|
-
i++
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
|
|
267
224
|
function amendCSN(csn) {
|
|
268
225
|
unrollDraftability(csn)
|
|
269
226
|
propagateForeignKeys(csn)
|
package/lib/file.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
const fs = require('fs').promises
|
|
4
4
|
const { readFileSync } = require('fs')
|
|
5
|
-
const { printEnum,
|
|
5
|
+
const { printEnum, propertyToInlineEnumName, stringifyEnumImplementation } = require('./components/enum')
|
|
6
6
|
const path = require('path')
|
|
7
7
|
|
|
8
8
|
const AUTO_GEN_NOTE = "// This is an automatically generated file. Please do not change its contents manually!"
|
|
@@ -104,8 +104,8 @@ class SourceFile extends File {
|
|
|
104
104
|
this.events = { buffer: new Buffer(), fqs: []}
|
|
105
105
|
/** @type {Buffer} */
|
|
106
106
|
this.types = new Buffer()
|
|
107
|
-
/** @type {{ buffer: Buffer,
|
|
108
|
-
this.enums = { buffer: new Buffer(),
|
|
107
|
+
/** @type {{ buffer: Buffer, data: {kvs: [string[]], name: string, fq: string, property?: string}[]}} */
|
|
108
|
+
this.enums = { buffer: new Buffer(), data: [] }
|
|
109
109
|
/** @type {{ buffer: Buffer }} */
|
|
110
110
|
this.inlineEnums = { buffer: new Buffer() }
|
|
111
111
|
/** @type {Buffer} */
|
|
@@ -226,28 +226,28 @@ class SourceFile extends File {
|
|
|
226
226
|
* @param {string} fq fully qualified name of the enum (entity name within CSN)
|
|
227
227
|
* @param {string} name local name of the enum
|
|
228
228
|
* @param {[string, string][]} kvs list of key-value pairs
|
|
229
|
-
* @param {string} [property] property to which the enum is attached.
|
|
230
|
-
* If given, the enum is considered to be an
|
|
229
|
+
* @param {string} [property] property to which the enum is attached.
|
|
230
|
+
* If given, the enum is considered to be an inline definition of an enum.
|
|
231
231
|
* If not, it is considered to be regular, named enum.
|
|
232
232
|
*/
|
|
233
233
|
addEnum(fq, name, kvs) {
|
|
234
|
-
this.enums.
|
|
234
|
+
this.enums.data.push({ name, fq, kvs })
|
|
235
235
|
printEnum(this.enums.buffer, name, kvs)
|
|
236
236
|
}
|
|
237
237
|
|
|
238
238
|
/**
|
|
239
|
-
* Adds an
|
|
239
|
+
* Adds an inline enum to this file.
|
|
240
240
|
* @param {string} entityCleanName name of the entity the enum is attached to without namespace
|
|
241
241
|
* @param {string} entityFqName name of the entity the enum is attached to with namespace
|
|
242
242
|
*
|
|
243
243
|
* @param {string} propertyName property to which the enum is attached.
|
|
244
244
|
* @param {[string, string][]} kvs list of key-value pairs
|
|
245
|
-
* If given, the enum is considered to be an
|
|
245
|
+
* If given, the enum is considered to be an inline definition of an enum.
|
|
246
246
|
* If not, it is considered to be regular, named enum.
|
|
247
247
|
*
|
|
248
248
|
* @example
|
|
249
249
|
* ```js
|
|
250
|
-
*
|
|
250
|
+
* addInlineEnum('Books.genre', 'Books', 'genre', [['horror','horror']])
|
|
251
251
|
* ```
|
|
252
252
|
* generates
|
|
253
253
|
* ```js
|
|
@@ -266,13 +266,14 @@ class SourceFile extends File {
|
|
|
266
266
|
* }
|
|
267
267
|
* ```
|
|
268
268
|
*/
|
|
269
|
-
|
|
270
|
-
this.enums.
|
|
269
|
+
addInlineEnum(entityCleanName, entityFqName, propertyName, kvs) {
|
|
270
|
+
this.enums.data.push({
|
|
271
271
|
name: entityFqName,
|
|
272
272
|
property: propertyName,
|
|
273
|
+
kvs,
|
|
273
274
|
fq: `${entityCleanName}.${propertyName}`
|
|
274
275
|
})
|
|
275
|
-
printEnum(this.inlineEnums.buffer,
|
|
276
|
+
printEnum(this.inlineEnums.buffer, propertyToInlineEnumName(entityCleanName, propertyName), kvs, {export: false})
|
|
276
277
|
}
|
|
277
278
|
|
|
278
279
|
/**
|
|
@@ -410,9 +411,7 @@ class SourceFile extends File {
|
|
|
410
411
|
.concat(['// actions'])
|
|
411
412
|
.concat(this.actions.names.map(name => `module.exports.${name} = '${name}'`))
|
|
412
413
|
.concat(['// enums'])
|
|
413
|
-
.concat(this.enums.
|
|
414
|
-
? stringifyAnonymousEnum(name, fq, property)
|
|
415
|
-
: stringifyNamedEnum(name, fq)))
|
|
414
|
+
.concat(this.enums.data.map(({name, kvs}) => stringifyEnumImplementation(name, kvs)))
|
|
416
415
|
.join('\n') + '\n'
|
|
417
416
|
}
|
|
418
417
|
}
|
|
@@ -608,6 +607,7 @@ const writeout = async (root, sources) =>
|
|
|
608
607
|
} catch (err) {
|
|
609
608
|
// eslint-disable-next-line no-console
|
|
610
609
|
console.error(`Could not create parent directory ${dir}: ${err}.`)
|
|
610
|
+
console.error(err.stack)
|
|
611
611
|
}
|
|
612
612
|
return dir
|
|
613
613
|
})
|
package/lib/visitor.js
CHANGED
|
@@ -3,12 +3,13 @@
|
|
|
3
3
|
const util = require('./util')
|
|
4
4
|
|
|
5
5
|
const { amendCSN } = require('./csn')
|
|
6
|
+
// eslint-disable-next-line no-unused-vars
|
|
6
7
|
const { SourceFile, baseDefinitions, Buffer } = require('./file')
|
|
7
8
|
const { FlatInlineDeclarationResolver, StructuredInlineDeclarationResolver } = require('./components/inline')
|
|
8
9
|
const { Resolver } = require('./components/resolver')
|
|
9
10
|
const { Logger } = require('./logging')
|
|
10
11
|
const { docify } = require('./components/wrappers')
|
|
11
|
-
const {
|
|
12
|
+
const { csnToEnumPairs, propertyToInlineEnumName, isInlineEnumType } = require('./components/enum')
|
|
12
13
|
|
|
13
14
|
/** @typedef {import('./file').File} File */
|
|
14
15
|
/** @typedef {{ entity: String }} Context */
|
|
@@ -116,7 +117,11 @@ class Visitor {
|
|
|
116
117
|
// instead of using the definition from inferred CSN, we refer to the projected entity from xtended CSN instead.
|
|
117
118
|
// The latter contains the CSN fixes (propagated foreign keys, etc) and none of the localised fields we don't handle yet.
|
|
118
119
|
if (entity.projection) {
|
|
119
|
-
|
|
120
|
+
const targetName = entity.projection.from.ref[0]
|
|
121
|
+
// FIXME: references to types of entity properties may be missing from xtendend flavour (see #103)
|
|
122
|
+
// this should be revisted once we settle on a single flavour.
|
|
123
|
+
const target = this.csn.xtended.definitions[targetName] ?? this.csn.inferred.definitions[targetName]
|
|
124
|
+
this.visitEntity(name, target)
|
|
120
125
|
} else {
|
|
121
126
|
this.logger.error(`Expecting an autoexposed projection within a service. Skipping ${name}`)
|
|
122
127
|
}
|
|
@@ -151,7 +156,9 @@ class Visitor {
|
|
|
151
156
|
buffer.indent()
|
|
152
157
|
|
|
153
158
|
const enums = []
|
|
154
|
-
|
|
159
|
+
const exclusions = new Set(entity.projection?.excluding ?? [])
|
|
160
|
+
const elements = Object.entries(entity.elements ?? {}).filter(([ename]) => !exclusions.has(ename))
|
|
161
|
+
for (const [ename, element] of elements) {
|
|
155
162
|
this.visitElement(ename, element, file, buffer)
|
|
156
163
|
|
|
157
164
|
// make foreign keys explicit
|
|
@@ -160,7 +167,10 @@ class Visitor {
|
|
|
160
167
|
// We don't really have to care for this case, as keys from such structs are _not_ propagated to
|
|
161
168
|
// the containing entity.
|
|
162
169
|
for (const [kname, kelement] of Object.entries(this.csn.xtended.definitions[element.target]?.keys ?? {})) {
|
|
163
|
-
this.
|
|
170
|
+
if (this.resolver.getMaxCardinality(element) === 1) {
|
|
171
|
+
kelement.isRefNotNull = !!element.notNull || !!element.key
|
|
172
|
+
this.visitElement(`${ename}_${kname}`, kelement, file, buffer)
|
|
173
|
+
}
|
|
164
174
|
}
|
|
165
175
|
}
|
|
166
176
|
|
|
@@ -172,8 +182,8 @@ class Visitor {
|
|
|
172
182
|
|
|
173
183
|
buffer.indent()
|
|
174
184
|
for (const e of enums) {
|
|
175
|
-
buffer.add(`static ${e.name} = ${
|
|
176
|
-
file.
|
|
185
|
+
buffer.add(`static ${e.name} = ${propertyToInlineEnumName(clean, e.name)}`)
|
|
186
|
+
file.addInlineEnum(clean, name, e.name, csnToEnumPairs(e, {unwrapVals: true}))
|
|
177
187
|
}
|
|
178
188
|
buffer.add('static actions: {')
|
|
179
189
|
buffer.indent()
|
|
@@ -299,7 +309,7 @@ class Visitor {
|
|
|
299
309
|
.filter(([, type]) => type?.type !== '$self' && !(type.items?.type === '$self'))
|
|
300
310
|
.map(([name, type]) => [
|
|
301
311
|
name,
|
|
302
|
-
this.resolver.resolveAndRequire(type, file)
|
|
312
|
+
this.resolver.visitor.inlineDeclarationResolver.getPropertyDatatype(this.resolver.resolveAndRequire(type, file)),
|
|
303
313
|
])
|
|
304
314
|
: []
|
|
305
315
|
}
|
|
@@ -310,7 +320,9 @@ class Visitor {
|
|
|
310
320
|
const ns = this.resolver.resolveNamespace(name.split('.'))
|
|
311
321
|
const file = this.getNamespaceFile(ns)
|
|
312
322
|
const params = this.#stringifyFunctionParams(func.params, file)
|
|
313
|
-
const returns = this.resolver.
|
|
323
|
+
const returns = this.resolver.visitor.inlineDeclarationResolver.getPropertyDatatype(
|
|
324
|
+
this.resolver.resolveAndRequire(func.returns, file)
|
|
325
|
+
)
|
|
314
326
|
file.addFunction(name.split('.').at(-1), params, returns)
|
|
315
327
|
}
|
|
316
328
|
|
|
@@ -319,7 +331,9 @@ class Visitor {
|
|
|
319
331
|
const ns = this.resolver.resolveNamespace(name.split('.'))
|
|
320
332
|
const file = this.getNamespaceFile(ns)
|
|
321
333
|
const params = this.#stringifyFunctionParams(action.params, file)
|
|
322
|
-
const returns = this.resolver.
|
|
334
|
+
const returns = this.resolver.visitor.inlineDeclarationResolver.getPropertyDatatype(
|
|
335
|
+
this.resolver.resolveAndRequire(action.returns, file)
|
|
336
|
+
)
|
|
323
337
|
file.addAction(name.split('.').at(-1), params, returns)
|
|
324
338
|
}
|
|
325
339
|
|
|
@@ -329,7 +343,7 @@ class Visitor {
|
|
|
329
343
|
const ns = this.resolver.resolveNamespace(name.split('.'))
|
|
330
344
|
const file = this.getNamespaceFile(ns)
|
|
331
345
|
if ('enum' in type) {
|
|
332
|
-
file.addEnum(name, clean,
|
|
346
|
+
file.addEnum(name, clean, csnToEnumPairs(type))
|
|
333
347
|
} else {
|
|
334
348
|
// alias
|
|
335
349
|
file.addType(name, clean, this.resolver.resolveAndRequire(type, file).typeName)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cap-js/cds-typer",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.0",
|
|
4
4
|
"description": "Generates .ts files for a CDS model to receive code completion in VS Code",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"repository": "github:cap-js/cds-typer",
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"test:integration": "jest --projects test/int.jest.config.js",
|
|
18
18
|
"test:all": "jest",
|
|
19
19
|
"test": "npm run test:unit",
|
|
20
|
-
"lint": "eslint",
|
|
20
|
+
"lint": "npx eslint .",
|
|
21
21
|
"cli": "node lib/cli.js",
|
|
22
22
|
"doc:clean": "rm -rf ./doc",
|
|
23
23
|
"doc:prepare": "npm run doc:clean && mkdir -p doc/types",
|
|
@@ -40,10 +40,9 @@
|
|
|
40
40
|
"@sap/cds": ">=6"
|
|
41
41
|
},
|
|
42
42
|
"devDependencies": {
|
|
43
|
+
"@babel/eslint-parser": "^7.23.3",
|
|
43
44
|
"acorn": "^8.10.0",
|
|
44
45
|
"eslint": "^8.15.0",
|
|
45
|
-
"eslint-config-prettier": "^8.5.0",
|
|
46
|
-
"eslint-plugin-prettier": "^4.0.0",
|
|
47
46
|
"jest": "^29",
|
|
48
47
|
"typescript": ">=4.6.4"
|
|
49
48
|
},
|