@cap-js/cds-typer 0.12.0 → 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 +8 -1
- package/lib/components/enum.js +34 -26
- package/lib/components/resolver.js +4 -4
- package/lib/file.js +15 -15
- package/lib/visitor.js +7 -5
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,7 +4,14 @@ 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
|
|
8
15
|
|
|
9
16
|
## Version 0.12.0 - 2023-11-23
|
|
10
17
|
|
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??k]))`
|
|
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
|
}
|
|
@@ -2,10 +2,10 @@
|
|
|
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(
|
|
5
|
+
const { Buffer, SourceFile, Path, Library, baseDefinitions } = require('../file')
|
|
6
6
|
const { deepRequire, createToManyAssociation, createToOneAssociation, createArrayOf, createCompositionOfMany, createCompositionOfOne } = require('./wrappers')
|
|
7
|
-
const { StructuredInlineDeclarationResolver } = require(
|
|
8
|
-
const { isInlineEnumType,
|
|
7
|
+
const { StructuredInlineDeclarationResolver } = require('./inline')
|
|
8
|
+
const { isInlineEnumType, propertyToInlineEnumName } = require('./enum')
|
|
9
9
|
|
|
10
10
|
/** @typedef {{ cardinality?: { max?: '*' | number } }} EntityCSN */
|
|
11
11
|
/** @typedef {{ definitions?: Object<string, EntityCSN> }} CSN */
|
|
@@ -374,7 +374,7 @@ class Resolver {
|
|
|
374
374
|
// we use the singular as the initial declaration of these enums takes place
|
|
375
375
|
// while defining the singular class. Which therefore uses the singular over the plural name.
|
|
376
376
|
const cleanEntityName = util.singular4(element.parent, true)
|
|
377
|
-
const enumName =
|
|
377
|
+
const enumName = propertyToInlineEnumName(cleanEntityName, element.name)
|
|
378
378
|
result.type = enumName
|
|
379
379
|
result.plainName = enumName
|
|
380
380
|
result.isInlineDeclaration = true
|
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
|
@@ -9,7 +9,7 @@ const { FlatInlineDeclarationResolver, StructuredInlineDeclarationResolver } = r
|
|
|
9
9
|
const { Resolver } = require('./components/resolver')
|
|
10
10
|
const { Logger } = require('./logging')
|
|
11
11
|
const { docify } = require('./components/wrappers')
|
|
12
|
-
const {
|
|
12
|
+
const { csnToEnumPairs, propertyToInlineEnumName, isInlineEnumType } = require('./components/enum')
|
|
13
13
|
|
|
14
14
|
/** @typedef {import('./file').File} File */
|
|
15
15
|
/** @typedef {{ entity: String }} Context */
|
|
@@ -156,7 +156,9 @@ class Visitor {
|
|
|
156
156
|
buffer.indent()
|
|
157
157
|
|
|
158
158
|
const enums = []
|
|
159
|
-
|
|
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) {
|
|
160
162
|
this.visitElement(ename, element, file, buffer)
|
|
161
163
|
|
|
162
164
|
// make foreign keys explicit
|
|
@@ -180,8 +182,8 @@ class Visitor {
|
|
|
180
182
|
|
|
181
183
|
buffer.indent()
|
|
182
184
|
for (const e of enums) {
|
|
183
|
-
buffer.add(`static ${e.name} = ${
|
|
184
|
-
file.
|
|
185
|
+
buffer.add(`static ${e.name} = ${propertyToInlineEnumName(clean, e.name)}`)
|
|
186
|
+
file.addInlineEnum(clean, name, e.name, csnToEnumPairs(e, {unwrapVals: true}))
|
|
185
187
|
}
|
|
186
188
|
buffer.add('static actions: {')
|
|
187
189
|
buffer.indent()
|
|
@@ -341,7 +343,7 @@ class Visitor {
|
|
|
341
343
|
const ns = this.resolver.resolveNamespace(name.split('.'))
|
|
342
344
|
const file = this.getNamespaceFile(ns)
|
|
343
345
|
if ('enum' in type) {
|
|
344
|
-
file.addEnum(name, clean,
|
|
346
|
+
file.addEnum(name, clean, csnToEnumPairs(type))
|
|
345
347
|
} else {
|
|
346
348
|
// alias
|
|
347
349
|
file.addType(name, clean, this.resolver.resolveAndRequire(type, file).typeName)
|