@cap-js/cds-typer 0.12.0 → 0.14.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 +12 -1
- package/lib/components/enum.js +34 -26
- package/lib/components/resolver.js +4 -4
- package/lib/csn.js +15 -1
- package/lib/file.js +15 -15
- package/lib/visitor.js +13 -9
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,7 +4,18 @@ 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.15.0 - TBD
|
|
8
|
+
|
|
9
|
+
## Version 0.14.0 - 2023-12-13
|
|
10
|
+
### Added
|
|
11
|
+
- Entities that are database views now also receive typings
|
|
12
|
+
|
|
13
|
+
## Version 0.13.0 - 2023-12-06
|
|
14
|
+
### Changes
|
|
15
|
+
- 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
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
- The `excluding` clause in projections now actually excludes the specified properties in the generated types
|
|
8
19
|
|
|
9
20
|
## Version 0.12.0 - 2023-11-23
|
|
10
21
|
|
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/csn.js
CHANGED
|
@@ -226,4 +226,18 @@ function amendCSN(csn) {
|
|
|
226
226
|
propagateForeignKeys(csn)
|
|
227
227
|
}
|
|
228
228
|
|
|
229
|
-
|
|
229
|
+
/**
|
|
230
|
+
* FIXME: this is pretty handwavey: we are looking for view-entities,
|
|
231
|
+
* i.e. ones that have a query, but are not a cds level projection.
|
|
232
|
+
* Those are still not expanded and we have to retrieve their definition
|
|
233
|
+
* with all properties from the inferred model.
|
|
234
|
+
*/
|
|
235
|
+
const isView = entity => entity.query && !entity.projection
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* @see isView
|
|
239
|
+
* Unresolved entities have to be looked up from inferred csn.
|
|
240
|
+
*/
|
|
241
|
+
const isUnresolved = entity => entity._unresolved === true
|
|
242
|
+
|
|
243
|
+
module.exports = { amendCSN, isView, isUnresolved }
|
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
|
@@ -2,14 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
const util = require('./util')
|
|
4
4
|
|
|
5
|
-
const { amendCSN } = require('./csn')
|
|
5
|
+
const { amendCSN, isView, isUnresolved } = require('./csn')
|
|
6
6
|
// eslint-disable-next-line no-unused-vars
|
|
7
7
|
const { SourceFile, 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
|
-
const {
|
|
12
|
+
const { csnToEnumPairs, propertyToInlineEnumName, isInlineEnumType } = require('./components/enum')
|
|
13
13
|
|
|
14
14
|
/** @typedef {import('./file').File} File */
|
|
15
15
|
/** @typedef {{ entity: String }} Context */
|
|
@@ -99,10 +99,12 @@ class Visitor {
|
|
|
99
99
|
*/
|
|
100
100
|
visitDefinitions() {
|
|
101
101
|
for (const [name, entity] of Object.entries(this.csn.xtended.definitions)) {
|
|
102
|
-
if (entity
|
|
103
|
-
this.
|
|
104
|
-
} else {
|
|
102
|
+
if (isView(entity)) {
|
|
103
|
+
this.visitEntity(name, this.csn.inferred.definitions[name])
|
|
104
|
+
} else if (!isUnresolved(entity)) {
|
|
105
105
|
this.visitEntity(name, entity)
|
|
106
|
+
} else {
|
|
107
|
+
this.logger.warning(`Skipping unresolved entity: ${name}`)
|
|
106
108
|
}
|
|
107
109
|
}
|
|
108
110
|
// FIXME: optimise
|
|
@@ -156,7 +158,9 @@ class Visitor {
|
|
|
156
158
|
buffer.indent()
|
|
157
159
|
|
|
158
160
|
const enums = []
|
|
159
|
-
|
|
161
|
+
const exclusions = new Set(entity.projection?.excluding ?? [])
|
|
162
|
+
const elements = Object.entries(entity.elements ?? {}).filter(([ename]) => !exclusions.has(ename))
|
|
163
|
+
for (const [ename, element] of elements) {
|
|
160
164
|
this.visitElement(ename, element, file, buffer)
|
|
161
165
|
|
|
162
166
|
// make foreign keys explicit
|
|
@@ -180,8 +184,8 @@ class Visitor {
|
|
|
180
184
|
|
|
181
185
|
buffer.indent()
|
|
182
186
|
for (const e of enums) {
|
|
183
|
-
buffer.add(`static ${e.name} = ${
|
|
184
|
-
file.
|
|
187
|
+
buffer.add(`static ${e.name} = ${propertyToInlineEnumName(clean, e.name)}`)
|
|
188
|
+
file.addInlineEnum(clean, name, e.name, csnToEnumPairs(e, {unwrapVals: true}))
|
|
185
189
|
}
|
|
186
190
|
buffer.add('static actions: {')
|
|
187
191
|
buffer.indent()
|
|
@@ -341,7 +345,7 @@ class Visitor {
|
|
|
341
345
|
const ns = this.resolver.resolveNamespace(name.split('.'))
|
|
342
346
|
const file = this.getNamespaceFile(ns)
|
|
343
347
|
if ('enum' in type) {
|
|
344
|
-
file.addEnum(name, clean,
|
|
348
|
+
file.addEnum(name, clean, csnToEnumPairs(type))
|
|
345
349
|
} else {
|
|
346
350
|
// alias
|
|
347
351
|
file.addType(name, clean, this.resolver.resolveAndRequire(type, file).typeName)
|