@cap-js/cds-typer 0.19.0 → 0.20.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 +18 -2
- package/lib/cli.js +4 -4
- package/lib/compile.js +12 -12
- package/lib/components/basedefs.js +64 -0
- package/lib/components/enum.js +22 -22
- package/lib/components/reference.js +1 -1
- package/lib/components/resolver.js +151 -74
- package/lib/components/typescript.js +3 -0
- package/lib/components/wrappers.js +11 -2
- package/lib/csn.js +31 -24
- package/lib/file.js +43 -87
- package/lib/util.js +27 -29
- package/lib/visitor.js +100 -89
- package/package.json +3 -3
package/lib/visitor.js
CHANGED
|
@@ -4,18 +4,20 @@ const util = require('./util')
|
|
|
4
4
|
|
|
5
5
|
const { amendCSN, isView, isUnresolved, propagateForeignKeys, isDraftEnabled, isType, isProjection } = require('./csn')
|
|
6
6
|
// eslint-disable-next-line no-unused-vars
|
|
7
|
-
const { SourceFile, FileRepository,
|
|
7
|
+
const { SourceFile, FileRepository, Buffer } = require('./file')
|
|
8
8
|
const { FlatInlineDeclarationResolver, StructuredInlineDeclarationResolver } = require('./components/inline')
|
|
9
9
|
const { Resolver, resolveBuiltin } = 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
13
|
const { isReferenceType } = require('./components/reference')
|
|
14
|
+
const { empty } = require('./components/typescript')
|
|
15
|
+
const { baseDefinitions } = require('./components/basedefs')
|
|
14
16
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
+
/** @typedef {import('./file').File} File */
|
|
18
|
+
/** @typedef {{ entity: String }} Context */
|
|
17
19
|
|
|
18
|
-
|
|
20
|
+
/**
|
|
19
21
|
* @typedef { {
|
|
20
22
|
* rootDirectory: string,
|
|
21
23
|
* logLevel: number,
|
|
@@ -23,7 +25,7 @@ const { isReferenceType } = require('./components/reference')
|
|
|
23
25
|
* }} CompileParameters
|
|
24
26
|
*/
|
|
25
27
|
|
|
26
|
-
|
|
28
|
+
/**
|
|
27
29
|
* - `propertiesOptional = true` -> all properties are generated as optional ?:. (standard CAP behaviour, where properties be unavailable)
|
|
28
30
|
* - `inlineDeclarations = 'structured'` -> @see inline.StructuredInlineDeclarationResolver
|
|
29
31
|
* - `inlineDeclarations = 'flat'` -> @see inline.FlatInlineDeclarationResolver
|
|
@@ -33,7 +35,7 @@ const { isReferenceType } = require('./components/reference')
|
|
|
33
35
|
* }} VisitorOptions
|
|
34
36
|
*/
|
|
35
37
|
|
|
36
|
-
|
|
38
|
+
/**
|
|
37
39
|
* @typedef {{
|
|
38
40
|
* typeName: string,
|
|
39
41
|
* singular: string,
|
|
@@ -153,10 +155,10 @@ class Visitor {
|
|
|
153
155
|
*/
|
|
154
156
|
#aspectify(name, entity, buffer, options = {}) {
|
|
155
157
|
const clean = options?.cleanName ?? this.resolver.trimNamespace(name)
|
|
156
|
-
const
|
|
157
|
-
const file = this.fileRepository.getNamespaceFile(
|
|
158
|
-
const identSingular =
|
|
159
|
-
const identAspect =
|
|
158
|
+
const namespace = this.resolver.resolveNamespace(name.split('.'))
|
|
159
|
+
const file = this.fileRepository.getNamespaceFile(namespace)
|
|
160
|
+
const identSingular = name => name
|
|
161
|
+
const identAspect = name => `_${name}Aspect`
|
|
160
162
|
|
|
161
163
|
this.contexts.push({ entity: name })
|
|
162
164
|
|
|
@@ -201,36 +203,45 @@ class Visitor {
|
|
|
201
203
|
file.addInlineEnum(clean, name, e.name, csnToEnumPairs(e, {unwrapVals: true}))
|
|
202
204
|
}
|
|
203
205
|
const actions = Object.entries(entity.actions ?? {})
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
206
|
+
if (actions.length) {
|
|
207
|
+
buffer.addIndentedBlock('static readonly actions: {',
|
|
208
|
+
actions.map(([aname, action]) => SourceFile.stringifyLambda({
|
|
209
|
+
name: aname,
|
|
210
|
+
parameters: this.#stringifyFunctionParams(action.params, file),
|
|
211
|
+
returns: action.returns ? this.resolver.resolveAndRequire(action.returns, file).typeName : 'any',
|
|
212
|
+
kind: action.kind
|
|
213
|
+
}))
|
|
214
|
+
, '}') // end of actions
|
|
215
|
+
} else {
|
|
216
|
+
buffer.add(`static readonly actions: ${empty}`)
|
|
217
|
+
}
|
|
211
218
|
}.bind(this))
|
|
212
219
|
}.bind(this), '};') // end of generated class
|
|
213
220
|
}.bind(this), '}') // end of aspect
|
|
214
221
|
|
|
215
222
|
// CLASS WITH ADDED ASPECTS
|
|
216
223
|
file.addImport(baseDefinitions.path)
|
|
217
|
-
const
|
|
218
|
-
.map(
|
|
219
|
-
const
|
|
220
|
-
file.addImport(
|
|
221
|
-
return [
|
|
224
|
+
const ancestors = (entity.includes ?? [])
|
|
225
|
+
.map(parent => {
|
|
226
|
+
const { namespace, name } = this.resolver.untangle(parent)
|
|
227
|
+
file.addImport(namespace)
|
|
228
|
+
return [namespace, name, parent]
|
|
222
229
|
})
|
|
223
|
-
.concat([[undefined, clean]]) // add own aspect without namespace AFTER imports were created
|
|
230
|
+
.concat([[undefined, clean, [namespace, clean].filter(Boolean).join('.')]]) // add own aspect without namespace AFTER imports were created
|
|
224
231
|
.reverse() // reverse so that own aspect A is applied before extensions B,C: B(C(A(Entity)))
|
|
225
|
-
.reduce(
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
232
|
+
.reduce((wrapped, [ns, n, fq]) => {
|
|
233
|
+
// types are not inflected, so don't change those to singular
|
|
234
|
+
const refersToType = isType(this.csn.inferred.definitions[fq])
|
|
235
|
+
const ident = identAspect(refersToType ? n : util.singular4(n))
|
|
236
|
+
|
|
237
|
+
return !ns || ns.isCwd(file.path.asDirectory())
|
|
238
|
+
? `${ident}(${wrapped})`
|
|
239
|
+
: `${ns.asIdentifier()}.${ident}(${wrapped})`
|
|
240
|
+
},
|
|
241
|
+
`${baseDefinitions.path.asIdentifier()}.Entity`
|
|
231
242
|
)
|
|
232
243
|
|
|
233
|
-
buffer.add(`export class ${identSingular(clean)} extends ${
|
|
244
|
+
buffer.add(`export class ${identSingular(clean)} extends ${ancestors} {${this.#staticClassContents(clean, entity).join('\n')}}`)
|
|
234
245
|
//buffer.add(`export type ${clean} = InstanceType<typeof ${identSingular(clean)}>`)
|
|
235
246
|
this.contexts.pop()
|
|
236
247
|
}
|
|
@@ -242,7 +253,7 @@ class Visitor {
|
|
|
242
253
|
#printEntity(name, entity) {
|
|
243
254
|
// static .name has to be defined more forcefully: https://github.com/microsoft/TypeScript/issues/442
|
|
244
255
|
const overrideNameProperty = (clazz, content) => `Object.defineProperty(${clazz}, 'name', { value: '${content}' })`
|
|
245
|
-
const
|
|
256
|
+
const { namespace: ns, name: clean } = this.resolver.untangle(name)
|
|
246
257
|
const file = this.fileRepository.getNamespaceFile(ns)
|
|
247
258
|
// entities are expected to be in plural anyway, so we would favour the regular name.
|
|
248
259
|
// If the user decides to pass a @plural annotation, that gets precedence over the regular name.
|
|
@@ -251,21 +262,19 @@ class Visitor {
|
|
|
251
262
|
let plural = this.resolver.trimNamespace(util.getPluralAnnotation(entity) ? util.plural4(entity, false) : name)
|
|
252
263
|
const singular = this.resolver.trimNamespace(util.singular4(entity, true))
|
|
253
264
|
*/
|
|
254
|
-
|
|
265
|
+
let { singular, plural } = this.resolver.inflect({csn: entity, plainName: clean}, ns.asNamespace())
|
|
255
266
|
|
|
256
267
|
// trimNamespace does not properly detect scoped entities, like A.B where both A and B are
|
|
257
268
|
// entities. So to see if we would run into a naming collision, we forcefully take the last
|
|
258
269
|
// part of the name, so "A.B" and "A.Bs" just become "B" and "Bs" to be compared.
|
|
259
270
|
// FIXME: put this in a util function
|
|
260
|
-
//if (singular.split('.').at(-1) === plural.split('.').at(-1)) {
|
|
261
271
|
if (plural.split('.').at(-1) === `${singular.split('.').at(-1)}_`) {
|
|
262
|
-
//plural += '_'
|
|
263
272
|
this.logger.warning(
|
|
264
273
|
`Derived singular and plural forms for '${singular}' are the same. This usually happens when your CDS entities are named following singular flexion. Consider naming your entities in plural or providing '@singular:'/ '@plural:' annotations to have a clear distinction between the two. Plural form will be renamed to '${plural}' to avoid compilation errors within the output.`
|
|
265
274
|
)
|
|
266
275
|
}
|
|
267
|
-
// as types are not inflected, their singular will always clash and there is also no plural for them anyway
|
|
268
|
-
if (!isType(entity) && singular in this.csn.xtended.definitions) {
|
|
276
|
+
// as types are not inflected, their singular will always clash and there is also no plural for them anyway -> skip
|
|
277
|
+
if (!isType(entity) && `${ns.asNamespace()}.${singular}` in this.csn.xtended.definitions) {
|
|
269
278
|
this.logger.error(
|
|
270
279
|
`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.`
|
|
271
280
|
)
|
|
@@ -285,7 +294,7 @@ class Visitor {
|
|
|
285
294
|
// which would not be possible while already in singular form, as "Book.text" could not be resolved in CSN.
|
|
286
295
|
// edge case: @singular annotation present. singular4 will take care of that.
|
|
287
296
|
file.addInflection(util.singular4(entity, true), plural, clean)
|
|
288
|
-
docify(entity.doc).forEach(d => buffer.add(d))
|
|
297
|
+
docify(entity.doc).forEach(d => { buffer.add(d) })
|
|
289
298
|
|
|
290
299
|
// in case of projections `entity` is empty -> retrieve from inferred csn where the actual properties are rolled out
|
|
291
300
|
const target = isProjection(entity) || isView(entity)
|
|
@@ -309,7 +318,9 @@ class Visitor {
|
|
|
309
318
|
}
|
|
310
319
|
// plural can not be a type alias to $singular[] but needs to be a proper class instead,
|
|
311
320
|
// so it can get passed as value to CQL functions.
|
|
312
|
-
|
|
321
|
+
const additionalProperties = this.#staticClassContents(singular, entity)
|
|
322
|
+
additionalProperties.push('$count?: number')
|
|
323
|
+
buffer.add(`export class ${plural} extends Array<${singular}> {${additionalProperties.join('\n')}}`)
|
|
313
324
|
buffer.add(overrideNameProperty(plural, entity.name))
|
|
314
325
|
}
|
|
315
326
|
buffer.add('')
|
|
@@ -338,39 +349,41 @@ class Visitor {
|
|
|
338
349
|
#stringifyFunctionParamType(type, file) {
|
|
339
350
|
// if type.type is not 'cds.String', 'cds.Integer', ..., then we are actually looking
|
|
340
351
|
// at a named enum type. In that case also resolve that type name
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
#printFunction(name, func) {
|
|
347
|
-
// FIXME: mostly duplicate of printAction -> reuse
|
|
348
|
-
this.logger.debug(`Printing function ${name}:\n${JSON.stringify(func, null, 2)}`)
|
|
349
|
-
const ns = this.resolver.resolveNamespace(name.split('.'))
|
|
350
|
-
const file = this.fileRepository.getNamespaceFile(ns)
|
|
351
|
-
const params = this.#stringifyFunctionParams(func.params, file)
|
|
352
|
-
const returns = this.resolver.visitor.inlineDeclarationResolver.getPropertyDatatype(
|
|
353
|
-
this.resolver.resolveAndRequire(func.returns, file)
|
|
352
|
+
if (type.enum && resolveBuiltin(type.type)) return stringifyEnumType(csnToEnumPairs(type))
|
|
353
|
+
const paramType = this.resolver.resolveAndRequire(type, file)
|
|
354
|
+
return this.inlineDeclarationResolver.getPropertyDatatype(
|
|
355
|
+
paramType,
|
|
356
|
+
paramType.typeInfo.isArray ? paramType.typeName : paramType.typeInfo.inflection.singular
|
|
354
357
|
)
|
|
355
|
-
file.addFunction(name.split('.').at(-1), params, returns)
|
|
356
358
|
}
|
|
357
359
|
|
|
358
|
-
|
|
359
|
-
|
|
360
|
+
/**
|
|
361
|
+
* @param {string} name
|
|
362
|
+
* @param {object} operation
|
|
363
|
+
* @param {'function' | 'action'} kind
|
|
364
|
+
*/
|
|
365
|
+
#printOperation(name, operation, kind) {
|
|
366
|
+
// FIXME: mostly duplicate of printAction -> reuse
|
|
367
|
+
this.logger.debug(`Printing operation ${name}:\n${JSON.stringify(operation, null, 2)}`)
|
|
360
368
|
const ns = this.resolver.resolveNamespace(name.split('.'))
|
|
361
369
|
const file = this.fileRepository.getNamespaceFile(ns)
|
|
362
|
-
const params = this.#stringifyFunctionParams(
|
|
363
|
-
const
|
|
364
|
-
|
|
370
|
+
const params = this.#stringifyFunctionParams(operation.params, file)
|
|
371
|
+
const returnType = this.resolver.resolveAndRequire(operation.returns, file)
|
|
372
|
+
const returns = this.inlineDeclarationResolver.getPropertyDatatype(
|
|
373
|
+
returnType,
|
|
374
|
+
returnType.typeInfo.isArray ? returnType.typeName : returnType.typeInfo.inflection.singular
|
|
365
375
|
)
|
|
366
|
-
file.
|
|
376
|
+
file.addOperation(name.split('.').at(-1), params, returns, kind)
|
|
367
377
|
}
|
|
368
378
|
|
|
369
379
|
#printType(name, type) {
|
|
370
380
|
this.logger.debug(`Printing type ${name}:\n${JSON.stringify(type, null, 2)}`)
|
|
371
|
-
const
|
|
381
|
+
const { namespace: ns, name: clean } = this.resolver.untangle(name)
|
|
372
382
|
const file = this.fileRepository.getNamespaceFile(ns)
|
|
373
|
-
|
|
383
|
+
// skip references to enums.
|
|
384
|
+
// "Base" enums will always have a builtin type (don't skip those).
|
|
385
|
+
// A type referencing an enum E will be considered an enum itself and have .type === E (skip).
|
|
386
|
+
if ('enum' in type && !isReferenceType(type) && resolveBuiltin(type.type)) {
|
|
374
387
|
file.addEnum(name, clean, csnToEnumPairs(type))
|
|
375
388
|
} else {
|
|
376
389
|
// alias
|
|
@@ -381,7 +394,7 @@ class Visitor {
|
|
|
381
394
|
|
|
382
395
|
#printAspect(name, aspect) {
|
|
383
396
|
this.logger.debug(`Printing aspect ${name}`)
|
|
384
|
-
const
|
|
397
|
+
const { namespace: ns, name: clean } = this.resolver.untangle(name)
|
|
385
398
|
const file = this.fileRepository.getNamespaceFile(ns)
|
|
386
399
|
// aspects are technically classes and can therefore be added to the list of defined classes.
|
|
387
400
|
// Still, when using them as mixins for a class, they need to already be defined.
|
|
@@ -393,7 +406,7 @@ class Visitor {
|
|
|
393
406
|
|
|
394
407
|
#printEvent(name, event) {
|
|
395
408
|
this.logger.debug(`Printing event ${name}`)
|
|
396
|
-
const
|
|
409
|
+
const { namespace: ns, name: clean } = this.resolver.untangle(name)
|
|
397
410
|
const file = this.fileRepository.getNamespaceFile(ns)
|
|
398
411
|
file.addEvent(clean, name)
|
|
399
412
|
const buffer = file.events.buffer
|
|
@@ -425,33 +438,31 @@ class Visitor {
|
|
|
425
438
|
*/
|
|
426
439
|
visitEntity(name, entity) {
|
|
427
440
|
switch (entity.kind) {
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
default:
|
|
454
|
-
this.logger.debug(`Unhandled entity kind '${entity.kind}'.`)
|
|
441
|
+
case 'entity':
|
|
442
|
+
this.#printEntity(name, entity)
|
|
443
|
+
break
|
|
444
|
+
case 'action':
|
|
445
|
+
case 'function':
|
|
446
|
+
this.#printOperation(name, entity, entity.kind)
|
|
447
|
+
break
|
|
448
|
+
case 'aspect':
|
|
449
|
+
this.#printAspect(name, entity)
|
|
450
|
+
break
|
|
451
|
+
case 'type': {
|
|
452
|
+
// types like inline definitions can be used very similarly to entities.
|
|
453
|
+
// They can be extended, contain inline enums, etc., so we treat them as entities.
|
|
454
|
+
const handler = entity.elements ? this.#printEntity : this.#printType
|
|
455
|
+
handler.call(this, name, entity)
|
|
456
|
+
break
|
|
457
|
+
}
|
|
458
|
+
case 'event':
|
|
459
|
+
this.#printEvent(name, entity)
|
|
460
|
+
break
|
|
461
|
+
case 'service':
|
|
462
|
+
this.#printService(name, entity)
|
|
463
|
+
break
|
|
464
|
+
default:
|
|
465
|
+
this.logger.debug(`Unhandled entity kind '${entity.kind}'.`)
|
|
455
466
|
}
|
|
456
467
|
}
|
|
457
468
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cap-js/cds-typer",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.20.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",
|
|
@@ -40,10 +40,10 @@
|
|
|
40
40
|
"@sap/cds": ">=7.7"
|
|
41
41
|
},
|
|
42
42
|
"devDependencies": {
|
|
43
|
-
"@babel/eslint-parser": "^7.23.3",
|
|
44
43
|
"@stylistic/eslint-plugin-js": "^1.6.3",
|
|
45
44
|
"acorn": "^8.10.0",
|
|
46
|
-
"eslint": "^
|
|
45
|
+
"eslint": "^9",
|
|
46
|
+
"globals": "^15.0.0",
|
|
47
47
|
"jest": "^29",
|
|
48
48
|
"typescript": ">=4.6.4"
|
|
49
49
|
},
|