@cap-js/cds-typer 0.11.0 → 0.12.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 -1
- package/lib/cli.js +0 -4
- package/lib/components/enum.js +1 -1
- package/lib/components/inline.js +16 -3
- package/lib/components/resolver.js +14 -3
- package/lib/csn.js +0 -43
- package/lib/file.js +6 -5
- package/lib/visitor.js +17 -5
- package/package.json +3 -4
package/CHANGELOG.md
CHANGED
|
@@ -4,12 +4,26 @@ 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.13.0 - TBD
|
|
8
|
+
|
|
9
|
+
## Version 0.12.0 - 2023-11-23
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
- Generate `cds.LargeBinary` as string, buffer, _or readable_ in the case of media content
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
- Added support for the `not null` modifier
|
|
16
|
+
|
|
17
|
+
### Fixed
|
|
18
|
+
- Now using names of enum values in generated _index.js_ files if no explicit value is present
|
|
19
|
+
|
|
20
|
+
## Version 0.11.1 - 2023-10-12
|
|
8
21
|
|
|
9
22
|
### Changed
|
|
10
23
|
|
|
11
24
|
### Added
|
|
12
25
|
### Fixed
|
|
26
|
+
- Fixed how service names are exported as default export
|
|
13
27
|
|
|
14
28
|
## Version 0.11.0 - 2023-10-10
|
|
15
29
|
|
package/lib/cli.js
CHANGED
package/lib/components/enum.js
CHANGED
|
@@ -82,7 +82,7 @@ const propertyToAnonymousEnumName = (entity, property) => `${entity}_${property}
|
|
|
82
82
|
*/
|
|
83
83
|
const isInlineEnumType = (element, csn) => element.enum && !(element.type in csn.definitions)
|
|
84
84
|
|
|
85
|
-
const stringifyEnumImplementation = (name, enm) => `module.exports.${name} = Object.fromEntries(Object.entries(${enm}).map(([k,v]) => [k,v.val]))`
|
|
85
|
+
const stringifyEnumImplementation = (name, enm) => `module.exports.${name} = Object.fromEntries(Object.entries(${enm}).map(([k,v]) => [k,v.val??k]))`
|
|
86
86
|
|
|
87
87
|
/**
|
|
88
88
|
* @param {string} name
|
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
|
+
// eslint-disable-next-line no-unused-vars
|
|
4
5
|
const { Buffer, SourceFile, Path, Library, baseDefinitions } = require("../file")
|
|
5
6
|
const { deepRequire, createToManyAssociation, createToOneAssociation, createArrayOf, createCompositionOfMany, createCompositionOfOne } = require('./wrappers')
|
|
6
7
|
const { StructuredInlineDeclarationResolver } = require("./inline")
|
|
7
|
-
const { isInlineEnumType,
|
|
8
|
+
const { isInlineEnumType, propertyToAnonymousEnumName } = 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) {
|
|
@@ -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
|
@@ -335,6 +335,7 @@ class SourceFile extends File {
|
|
|
335
335
|
* @param {string} fq the fully qualified name of the service
|
|
336
336
|
*/
|
|
337
337
|
addService(fq) {
|
|
338
|
+
// FIXME: warn the user when they're trying to add an entity/ type/ enum called "name", which will override our name export
|
|
338
339
|
if (this.services.names.length) {
|
|
339
340
|
throw new Error(`trying to add more than one service to file ${this.path.asDirectory()}. Existing service is ${this.services.names[0]}, trying to add ${fq}`)
|
|
340
341
|
}
|
|
@@ -369,6 +370,7 @@ class SourceFile extends File {
|
|
|
369
370
|
AUTO_GEN_NOTE,
|
|
370
371
|
this.getImports().join(),
|
|
371
372
|
this.preamble.join(),
|
|
373
|
+
this.services.buffer.join(), // must be the very first
|
|
372
374
|
this.types.join(),
|
|
373
375
|
this.enums.buffer.join(),
|
|
374
376
|
this.inlineEnums.buffer.join(), // needs to be before classes
|
|
@@ -376,15 +378,16 @@ class SourceFile extends File {
|
|
|
376
378
|
this.aspects.join(), // needs to be before classes
|
|
377
379
|
this.classes.join(),
|
|
378
380
|
this.events.buffer.join(),
|
|
379
|
-
this.actions.buffer.join()
|
|
380
|
-
this.services.buffer.join() // should be at the end
|
|
381
|
+
this.actions.buffer.join()
|
|
381
382
|
].filter(Boolean).join('\n')
|
|
382
383
|
}
|
|
383
384
|
|
|
384
385
|
toJSExports() {
|
|
385
386
|
return [AUTO_GEN_NOTE, "const cds = require('@sap/cds')", `const csn = cds.entities('${this.path.asNamespace()}')`] // boilerplate
|
|
386
387
|
.concat(
|
|
387
|
-
|
|
388
|
+
// FIXME: move stringification of service into own module
|
|
389
|
+
this.services.names.map(name => `module.exports = { name: '${name}' }`)) // there should be only one
|
|
390
|
+
.concat(this.inflections
|
|
388
391
|
// sorting the entries based on the number of dots in their singular.
|
|
389
392
|
// that makes sure we have defined all parent namespaces before adding subclasses to them e.g.:
|
|
390
393
|
// "module.exports.Books" is defined before "module.exports.Books.text"
|
|
@@ -410,8 +413,6 @@ class SourceFile extends File {
|
|
|
410
413
|
.concat(this.enums.fqs.map(({name, fq, property}) => property
|
|
411
414
|
? stringifyAnonymousEnum(name, fq, property)
|
|
412
415
|
: stringifyNamedEnum(name, fq)))
|
|
413
|
-
// FIXME: move stringification of service into own module
|
|
414
|
-
.concat(this.services.names.map(name => `module.exports.default = { name: '${name}' }`)) // there should be only one
|
|
415
416
|
.join('\n') + '\n'
|
|
416
417
|
}
|
|
417
418
|
}
|
package/lib/visitor.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
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')
|
|
@@ -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
|
}
|
|
@@ -160,7 +165,10 @@ class Visitor {
|
|
|
160
165
|
// We don't really have to care for this case, as keys from such structs are _not_ propagated to
|
|
161
166
|
// the containing entity.
|
|
162
167
|
for (const [kname, kelement] of Object.entries(this.csn.xtended.definitions[element.target]?.keys ?? {})) {
|
|
163
|
-
this.
|
|
168
|
+
if (this.resolver.getMaxCardinality(element) === 1) {
|
|
169
|
+
kelement.isRefNotNull = !!element.notNull || !!element.key
|
|
170
|
+
this.visitElement(`${ename}_${kname}`, kelement, file, buffer)
|
|
171
|
+
}
|
|
164
172
|
}
|
|
165
173
|
}
|
|
166
174
|
|
|
@@ -299,7 +307,7 @@ class Visitor {
|
|
|
299
307
|
.filter(([, type]) => type?.type !== '$self' && !(type.items?.type === '$self'))
|
|
300
308
|
.map(([name, type]) => [
|
|
301
309
|
name,
|
|
302
|
-
this.resolver.resolveAndRequire(type, file)
|
|
310
|
+
this.resolver.visitor.inlineDeclarationResolver.getPropertyDatatype(this.resolver.resolveAndRequire(type, file)),
|
|
303
311
|
])
|
|
304
312
|
: []
|
|
305
313
|
}
|
|
@@ -310,7 +318,9 @@ class Visitor {
|
|
|
310
318
|
const ns = this.resolver.resolveNamespace(name.split('.'))
|
|
311
319
|
const file = this.getNamespaceFile(ns)
|
|
312
320
|
const params = this.#stringifyFunctionParams(func.params, file)
|
|
313
|
-
const returns = this.resolver.
|
|
321
|
+
const returns = this.resolver.visitor.inlineDeclarationResolver.getPropertyDatatype(
|
|
322
|
+
this.resolver.resolveAndRequire(func.returns, file)
|
|
323
|
+
)
|
|
314
324
|
file.addFunction(name.split('.').at(-1), params, returns)
|
|
315
325
|
}
|
|
316
326
|
|
|
@@ -319,7 +329,9 @@ class Visitor {
|
|
|
319
329
|
const ns = this.resolver.resolveNamespace(name.split('.'))
|
|
320
330
|
const file = this.getNamespaceFile(ns)
|
|
321
331
|
const params = this.#stringifyFunctionParams(action.params, file)
|
|
322
|
-
const returns = this.resolver.
|
|
332
|
+
const returns = this.resolver.visitor.inlineDeclarationResolver.getPropertyDatatype(
|
|
333
|
+
this.resolver.resolveAndRequire(action.returns, file)
|
|
334
|
+
)
|
|
323
335
|
file.addAction(name.split('.').at(-1), params, returns)
|
|
324
336
|
}
|
|
325
337
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cap-js/cds-typer",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.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
|
},
|