@cap-js/cds-typer 0.31.0 → 0.32.1
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 +29 -1
- package/lib/cli.js +1 -1
- package/lib/components/enum.js +3 -3
- package/lib/components/typescript.js +1 -1
- package/lib/csn.js +47 -1
- package/lib/file.js +35 -12
- package/lib/printers/javascript.js +4 -2
- package/lib/printers/wrappers.js +1 -1
- package/lib/resolution/entity.js +16 -0
- package/lib/resolution/resolver.js +36 -23
- package/lib/typedefs.d.ts +5 -2
- package/lib/util.js +2 -2
- package/lib/visitor.js +37 -76
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -4,11 +4,39 @@ All notable changes to this project will be documented in this file.
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
-
### Added
|
|
7
|
+
### Added
|
|
8
|
+
### Changed
|
|
9
|
+
### Deprecated
|
|
10
|
+
### Removed
|
|
11
|
+
### Fixed
|
|
12
|
+
### Security
|
|
13
|
+
|
|
14
|
+
## [0.32.1] - 2025-01-20
|
|
15
|
+
|
|
16
|
+
### Added
|
|
8
17
|
### Changed
|
|
9
18
|
### Deprecated
|
|
10
19
|
### Removed
|
|
11
20
|
### Fixed
|
|
21
|
+
- default value for `inline_declarations` in help command
|
|
22
|
+
- entity scope and namespace are now added in the correct order to inflected type names
|
|
23
|
+
### Security
|
|
24
|
+
|
|
25
|
+
## [0.32.0] - 2025-01-14
|
|
26
|
+
|
|
27
|
+
### Added
|
|
28
|
+
- dedicated classes for inline compositions
|
|
29
|
+
- dedicated text-classes for entities with `localized` elements
|
|
30
|
+
|
|
31
|
+
### Changed
|
|
32
|
+
- prefixed builtin types like `Promise` and `Record` with `globalThis.`, to allow using names of builtin types for entities without collisions
|
|
33
|
+
- default export class representing the service itself is now exported without name
|
|
34
|
+
- bumped peer-dependency to `@cap-js/cds-types` to `>=0.9`
|
|
35
|
+
|
|
36
|
+
### Deprecated
|
|
37
|
+
### Removed
|
|
38
|
+
### Fixed
|
|
39
|
+
- referencing another entity's property of type `cds.String` in an enum will now properly quote the generated values
|
|
12
40
|
### Security
|
|
13
41
|
|
|
14
42
|
## [0.31.0] - 2024-12-16
|
package/lib/cli.js
CHANGED
|
@@ -182,7 +182,7 @@ const flags = enrichFlagSchema({
|
|
|
182
182
|
inlineDeclarations: {
|
|
183
183
|
desc: `Whether to resolve inline type declarations${EOL}flat: (x_a, x_b, ...)${EOL}or structured: (x: {a, b}).`,
|
|
184
184
|
allowed: ['flat', 'structured'],
|
|
185
|
-
default: '
|
|
185
|
+
default: 'flat'
|
|
186
186
|
},
|
|
187
187
|
propertiesOptional: parameterTypes.boolean({
|
|
188
188
|
desc: `If set to true, properties in entities are${EOL}always generated as optional (a?: T).`,
|
package/lib/components/enum.js
CHANGED
|
@@ -67,7 +67,7 @@ function printEnum(buffer, name, kvs, options = {}, doc=[]) {
|
|
|
67
67
|
* Converts a CSN type describing an enum into a list of kv-pairs.
|
|
68
68
|
* Values from CSN are unwrapped from their `.val` structure and
|
|
69
69
|
* will fall back to the key if no value is provided.
|
|
70
|
-
* @param {import('../typedefs').resolver.EnumCSN} enumCsn - the CSN type describing the enum
|
|
70
|
+
* @param {import('../typedefs').resolver.EnumCSN & { resolvedType?: string }} enumCsn - the CSN type describing the enum
|
|
71
71
|
* @param {{unwrapVals: boolean} | {}} options - if `unwrapVals` is passed,
|
|
72
72
|
* then the CSN structure `{val:x}` is flattened to just `x`.
|
|
73
73
|
* Retaining `val` is closer to the actual CSN structure and should be used where we want
|
|
@@ -81,10 +81,10 @@ function printEnum(buffer, name, kvs, options = {}, doc=[]) {
|
|
|
81
81
|
* csnToEnumPairs(csn, {unwrapVals: false}) // -> [['X', {val:'a'}], ['Y': {val:'b'}], ['Z':'Z']]
|
|
82
82
|
* ```
|
|
83
83
|
*/
|
|
84
|
-
const csnToEnumPairs = ({enum: enm, type}, options = {}) => {
|
|
84
|
+
const csnToEnumPairs = ({enum: enm, type, resolvedType}, options = {}) => {
|
|
85
85
|
const actualOptions = {...{unwrapVals: true}, ...options}
|
|
86
86
|
return Object.entries(enm).map(([k, v]) => {
|
|
87
|
-
const val = enumVal(k, v.val, type)
|
|
87
|
+
const val = enumVal(k, v.val, resolvedType ?? type) // if type is a ref, prefer the resolvedType to catch references to cds.Strings
|
|
88
88
|
return [k, (actualOptions.unwrapVals ? val : { val })]
|
|
89
89
|
})
|
|
90
90
|
}
|
package/lib/csn.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
const { LOG } = require('./logging')
|
|
2
|
+
const { annotations } = require('./util')
|
|
2
3
|
|
|
3
4
|
const DRAFT_ENABLED_ANNO = '@odata.draft.enabled'
|
|
4
5
|
/** @type {string[]} */
|
|
@@ -292,6 +293,30 @@ function propagateForeignKeys(csn) {
|
|
|
292
293
|
}
|
|
293
294
|
}
|
|
294
295
|
|
|
296
|
+
/**
|
|
297
|
+
* Clears "correct" singular/plural annotations from inferred model
|
|
298
|
+
* copies the ones from the xtended model.
|
|
299
|
+
*
|
|
300
|
+
* This is done to prevent potential duplicate class names because of annotation propagation.
|
|
301
|
+
* @param {{inferred: CSN, xtended: CSN}} csn - CSN models
|
|
302
|
+
*/
|
|
303
|
+
function propagateInflectionAnnotations(csn) {
|
|
304
|
+
const singularAnno = annotations.singular[0]
|
|
305
|
+
const pluralAnno = annotations.plural[0]
|
|
306
|
+
for (const [name, def] of Object.entries(csn.inferred.definitions)) {
|
|
307
|
+
const xtendedDef = csn.xtended.definitions[name]
|
|
308
|
+
// we keep the annotations from definition specific to the inferred model (e.g. inline compositions)
|
|
309
|
+
if (!xtendedDef) continue
|
|
310
|
+
|
|
311
|
+
// clear annotations from inferred definition
|
|
312
|
+
if (Object.hasOwn(def, singularAnno)) delete def[singularAnno]
|
|
313
|
+
if (Object.hasOwn(def, pluralAnno)) delete def[pluralAnno]
|
|
314
|
+
// transfer annotation from xtended if existing
|
|
315
|
+
if (Object.hasOwn(xtendedDef, singularAnno)) def[singularAnno] = xtendedDef[singularAnno]
|
|
316
|
+
if (Object.hasOwn(xtendedDef, pluralAnno)) def[pluralAnno] = xtendedDef[pluralAnno]
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
295
320
|
/**
|
|
296
321
|
* @param {EntityCSN} entity - the entity
|
|
297
322
|
*/
|
|
@@ -311,6 +336,25 @@ const getProjectionAliases = entity => {
|
|
|
311
336
|
return { aliases, all }
|
|
312
337
|
}
|
|
313
338
|
|
|
339
|
+
/**
|
|
340
|
+
* Heuristic way of looking up a reference type.
|
|
341
|
+
* We currently only support up to two segments,
|
|
342
|
+
* the first referring to the entity, a possible second
|
|
343
|
+
* referring to an element of the entity.
|
|
344
|
+
* @param {CSN} csn - CSN
|
|
345
|
+
* @param {string[]} ref - reference
|
|
346
|
+
* @returns {EntityCSN}
|
|
347
|
+
*/
|
|
348
|
+
function lookUpRefType (csn, ref) {
|
|
349
|
+
if (ref.length > 2) throw new Error(`Unsupported reference type ${ref.join('.')} with ${ref.length} segments. Please report this error.`)
|
|
350
|
+
/** @type {EntityCSN | undefined} */
|
|
351
|
+
let result = csn.definitions[ref[0]] // entity
|
|
352
|
+
if (ref.length === 1) return result
|
|
353
|
+
result = result?.elements?.[ref[1]] // property
|
|
354
|
+
if (!result) throw new Error(`Failed to look up reference type ${ref.join('.')}`)
|
|
355
|
+
return result
|
|
356
|
+
}
|
|
357
|
+
|
|
314
358
|
module.exports = {
|
|
315
359
|
collectDraftEnabledEntities,
|
|
316
360
|
isView,
|
|
@@ -326,5 +370,7 @@ module.exports = {
|
|
|
326
370
|
getProjectionAliases,
|
|
327
371
|
getViewTarget,
|
|
328
372
|
propagateForeignKeys,
|
|
329
|
-
|
|
373
|
+
propagateInflectionAnnotations,
|
|
374
|
+
isCsnAny,
|
|
375
|
+
lookUpRefType
|
|
330
376
|
}
|
package/lib/file.js
CHANGED
|
@@ -257,6 +257,7 @@ class SourceFile extends File {
|
|
|
257
257
|
if (!(name in this.namespaces)) {
|
|
258
258
|
const buffer = new Buffer()
|
|
259
259
|
buffer.closed = false
|
|
260
|
+
buffer.namespace = name
|
|
260
261
|
buffer.add(`export namespace ${name} {`)
|
|
261
262
|
buffer.indent()
|
|
262
263
|
this.namespaces[name] = buffer
|
|
@@ -286,6 +287,8 @@ class SourceFile extends File {
|
|
|
286
287
|
* @param {string} entityFqName - name of the entity the enum is attached to with namespace
|
|
287
288
|
* @param {string} propertyName - property to which the enum is attached.
|
|
288
289
|
* @param {[string, string][]} kvs - list of key-value pairs
|
|
290
|
+
* @param {Buffer} [buffer] - if buffer is of subnamespace the enum will be added there,
|
|
291
|
+
* otherwise to the inline enums of the file
|
|
289
292
|
* @param {string[]} doc - the enum docs
|
|
290
293
|
* If given, the enum is considered to be an inline definition of an enum.
|
|
291
294
|
* If not, it is considered to be regular, named enum.
|
|
@@ -310,16 +313,32 @@ class SourceFile extends File {
|
|
|
310
313
|
* }
|
|
311
314
|
* ```
|
|
312
315
|
*/
|
|
313
|
-
addInlineEnum(entityCleanName, entityFqName, propertyName, kvs, doc=[]) {
|
|
316
|
+
addInlineEnum(entityCleanName, entityFqName, propertyName, kvs, buffer, doc=[]) {
|
|
317
|
+
const namespacedEntity = [buffer?.namespace, entityCleanName].filter(Boolean).join('.')
|
|
314
318
|
this.enums.data.push({
|
|
315
|
-
name: `${
|
|
319
|
+
name: `${namespacedEntity}.${propertyName}`,
|
|
316
320
|
property: propertyName,
|
|
317
321
|
kvs,
|
|
318
|
-
fq: `${
|
|
322
|
+
fq: `${namespacedEntity}.${propertyName}`
|
|
319
323
|
})
|
|
320
|
-
const entityProxy = this.entityProxies[
|
|
324
|
+
const entityProxy = this.entityProxies[namespacedEntity] ?? (this.entityProxies[namespacedEntity] = [])
|
|
321
325
|
entityProxy.push(propertyName)
|
|
322
|
-
|
|
326
|
+
|
|
327
|
+
// REVISIT: find a better way to do this???
|
|
328
|
+
const printEnumToBuffer = (/** @type {Buffer} */buffer) => printEnum(buffer, propertyToInlineEnumName(entityCleanName, propertyName), kvs, {export: false}, doc)
|
|
329
|
+
|
|
330
|
+
if (buffer?.namespace) {
|
|
331
|
+
const tempBuffer = new Buffer()
|
|
332
|
+
// we want to put the enums on class level
|
|
333
|
+
tempBuffer.indent()
|
|
334
|
+
printEnumToBuffer(tempBuffer)
|
|
335
|
+
|
|
336
|
+
// we want to write the enums at the beginning of the namespace
|
|
337
|
+
const [first,...rest] = buffer.parts
|
|
338
|
+
buffer.parts = [first, ...tempBuffer.parts, ...rest]
|
|
339
|
+
} else {
|
|
340
|
+
printEnumToBuffer(this.inlineEnums.buffer)
|
|
341
|
+
}
|
|
323
342
|
}
|
|
324
343
|
|
|
325
344
|
/**
|
|
@@ -401,10 +420,6 @@ class SourceFile extends File {
|
|
|
401
420
|
*/
|
|
402
421
|
getImports() {
|
|
403
422
|
const buffer = new Buffer()
|
|
404
|
-
if (this.services.names.length) {
|
|
405
|
-
// currently only needed to extend cds.Service and would trigger unused-variable-errors in strict configs
|
|
406
|
-
buffer.add('import cds from \'@sap/cds\'') // TODO should go to visitor#printService, but can't express this as Path
|
|
407
|
-
}
|
|
408
423
|
const file = configuration.targetModuleType === 'esm'
|
|
409
424
|
? '/index.js'
|
|
410
425
|
: ''
|
|
@@ -490,12 +505,15 @@ class SourceFile extends File {
|
|
|
490
505
|
|
|
491
506
|
return {
|
|
492
507
|
singularRhs: `createEntityProxy(['${namespace}', '${original}'], { target: { is_singular: true }${customPropsStr} })`,
|
|
493
|
-
pluralRhs: `createEntityProxy(['${namespace}', '${original}'])`,
|
|
508
|
+
pluralRhs: `createEntityProxy(['${namespace}', '${original}'], { target: { is_singular: false }})`,
|
|
494
509
|
}
|
|
495
510
|
} else {
|
|
511
|
+
// standard entity: csn.Books
|
|
512
|
+
// inline entity: csn['Books.texts']
|
|
513
|
+
const csnAccess = original.includes('.') ? `csn['${original}']` : `csn.${original}`
|
|
496
514
|
return {
|
|
497
|
-
singularRhs: `{ is_singular: true, __proto__:
|
|
498
|
-
pluralRhs:
|
|
515
|
+
singularRhs: `{ is_singular: true, __proto__: ${csnAccess} }`,
|
|
516
|
+
pluralRhs: csnAccess
|
|
499
517
|
}
|
|
500
518
|
}
|
|
501
519
|
}
|
|
@@ -589,6 +607,11 @@ class Buffer {
|
|
|
589
607
|
* @type {boolean}
|
|
590
608
|
*/
|
|
591
609
|
this.closed = false
|
|
610
|
+
/**
|
|
611
|
+
* Required for inline enums of inline compositions or text entities
|
|
612
|
+
* @type {string | undefined}
|
|
613
|
+
*/
|
|
614
|
+
this.namespace = undefined
|
|
592
615
|
}
|
|
593
616
|
|
|
594
617
|
/**
|
|
@@ -84,12 +84,14 @@ class ESMPrinter extends JavaScriptPrinter {
|
|
|
84
84
|
|
|
85
85
|
/** @type {JavaScriptPrinter['printDeconstructedImport']} */
|
|
86
86
|
printDeconstructedImport (imports, from) {
|
|
87
|
-
return `import { ${imports.join(', ')} } from '${from}'`
|
|
87
|
+
return `import { ${imports.join(', ')} } from '${from}/index.js'`
|
|
88
88
|
}
|
|
89
89
|
|
|
90
90
|
/** @type {JavaScriptPrinter['printExport']} */
|
|
91
91
|
printExport (name, value) {
|
|
92
|
-
return
|
|
92
|
+
return name.includes('.')
|
|
93
|
+
? `${name} = ${value}`
|
|
94
|
+
: `export const ${name} = ${value}`
|
|
93
95
|
}
|
|
94
96
|
|
|
95
97
|
/** @type {JavaScriptPrinter['printDefaultExport']} */
|
package/lib/printers/wrappers.js
CHANGED
|
@@ -103,7 +103,7 @@ const createIntersectionOf = (...types) => types.join(' & ')
|
|
|
103
103
|
* createPromiseOf('string') // -> 'Promise<string>'
|
|
104
104
|
* ```
|
|
105
105
|
*/
|
|
106
|
-
const createPromiseOf = t => `Promise<${t}>`
|
|
106
|
+
const createPromiseOf = t => `globalThis.Promise<${t}>`
|
|
107
107
|
|
|
108
108
|
/**
|
|
109
109
|
* Wraps type into a deep require (removes all posibilities of undefined recursively).
|
package/lib/resolution/entity.js
CHANGED
|
@@ -61,6 +61,22 @@ class EntityInfo {
|
|
|
61
61
|
/** @type {import('../typedefs').resolver.EntityCSN | undefined} */
|
|
62
62
|
#csn
|
|
63
63
|
|
|
64
|
+
/** @type {Set<string> | undefined} */
|
|
65
|
+
#inheritedElements
|
|
66
|
+
|
|
67
|
+
/** @returns set of inherited elements (e.g. ID of aspect cuid) */
|
|
68
|
+
get inheritedElements() {
|
|
69
|
+
if (this.#inheritedElements) return this.#inheritedElements
|
|
70
|
+
this.#inheritedElements = new Set()
|
|
71
|
+
for (const parentName of this.csn.includes ?? []) {
|
|
72
|
+
const parent = this.#repository.getByFq(parentName)
|
|
73
|
+
for (const element of Object.keys(parent?.csn?.elements ?? {})) {
|
|
74
|
+
this.#inheritedElements.add(element)
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return this.#inheritedElements
|
|
78
|
+
}
|
|
79
|
+
|
|
64
80
|
/** @returns the **inferred** csn for this entity. */
|
|
65
81
|
get csn () {
|
|
66
82
|
return this.#csn ??= this.#resolver.csn.definitions[this.fullyQualifiedName]
|
|
@@ -24,7 +24,7 @@ const { configuration } = require('../config')
|
|
|
24
24
|
/** @typedef {{typeName: string, typeInfo: TypeResolveInfo & { inflection: Inflection }}} ResolveAndRequireInfo */
|
|
25
25
|
|
|
26
26
|
class Resolver {
|
|
27
|
-
get csn() { return this.visitor.csn
|
|
27
|
+
get csn() { return this.visitor.csn }
|
|
28
28
|
|
|
29
29
|
/** @param {Visitor} visitor - the visitor */
|
|
30
30
|
constructor(visitor) {
|
|
@@ -165,11 +165,13 @@ class Resolver {
|
|
|
165
165
|
*/
|
|
166
166
|
const isPropertyOf = (property, entity) => property && Object.hasOwn(entity?.elements ?? {}, property)
|
|
167
167
|
|
|
168
|
-
const defs = this.visitor.csn.
|
|
168
|
+
const defs = this.visitor.csn.definitions
|
|
169
|
+
|
|
170
|
+
// check if name is already an entity, then we do not have a property access, but a nested entity
|
|
171
|
+
if (defs[p]?.kind === 'entity') return []
|
|
172
|
+
|
|
169
173
|
// assume parts to contain [Namespace, Service, Entity1, Entity2, Entity3, property1, property2]
|
|
170
|
-
/** @type {string} */
|
|
171
|
-
// @ts-expect-error - nope, we know there is at least one element
|
|
172
|
-
let qualifier = parts.shift()
|
|
174
|
+
let qualifier = /** @type {string} */ (parts.shift())
|
|
173
175
|
// find first entity from left (Entity1)
|
|
174
176
|
while ((!defs[qualifier] || !isEntity(defs[qualifier])) && parts.length) {
|
|
175
177
|
qualifier += `.${parts.shift()}`
|
|
@@ -240,6 +242,8 @@ class Resolver {
|
|
|
240
242
|
} else {
|
|
241
243
|
// TODO: make sure the resolution still works. Currently, we only cut off the namespace!
|
|
242
244
|
plural = util.getPluralAnnotation(typeInfo.csn) ?? typeInfo.plainName
|
|
245
|
+
// remove leading entity name
|
|
246
|
+
if (plural.includes('.')) plural = last(plural)
|
|
243
247
|
singular = util.getSingularAnnotation(typeInfo.csn) ?? util.singular4(typeInfo.csn, true) // util.singular4(typeInfo.csn, true) // can not use `plural` to honor possible @singular annotation
|
|
244
248
|
|
|
245
249
|
// don't slice off namespace if it isn't part of the inflected name.
|
|
@@ -284,6 +288,8 @@ class Resolver {
|
|
|
284
288
|
const typeInfo = this.resolveType(element, file, options)
|
|
285
289
|
const cardinality = getMaxCardinality(element)
|
|
286
290
|
|
|
291
|
+
/** @type {string|undefined} */
|
|
292
|
+
let typeNamespaceIdent = undefined
|
|
287
293
|
let typeName = typeInfo.plainName ?? typeInfo.type
|
|
288
294
|
|
|
289
295
|
// only applies to builtin types, because the association/ composition _themselves_ are the (builtin) types we are checking, not their generic parameter!
|
|
@@ -311,18 +317,6 @@ class Resolver {
|
|
|
311
317
|
} else {
|
|
312
318
|
let { singular, plural } = targetTypeInfo.typeInfo.inflection
|
|
313
319
|
|
|
314
|
-
// FIXME: super hack!!
|
|
315
|
-
// Inflection currently does not retain the scope of the entity.
|
|
316
|
-
// But we can't just fix it in inflection(...), as that would break several other things
|
|
317
|
-
// So we bandaid-fix it back here, as it is the least intrusive place -- but this should get fixed asap!
|
|
318
|
-
if (target.type) {
|
|
319
|
-
const untangled = this.visitor.entityRepository.getByFqOrThrow(target.type)
|
|
320
|
-
const scope = untangled.scope.join('.')
|
|
321
|
-
if (scope && !singular.startsWith(scope)) {
|
|
322
|
-
singular = `${scope}.${singular}`
|
|
323
|
-
}
|
|
324
|
-
}
|
|
325
|
-
|
|
326
320
|
typeName = cardinality > 1
|
|
327
321
|
? toMany(plural)
|
|
328
322
|
: toOne(this.visitor.isSelfReference(target) ? 'this' : singular)
|
|
@@ -341,9 +335,10 @@ class Resolver {
|
|
|
341
335
|
if (!parent.isCwd(file.path.asDirectory())) {
|
|
342
336
|
file.addImport(parent)
|
|
343
337
|
// prepend namespace
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
typeInfo.inflection.
|
|
338
|
+
typeNamespaceIdent = parent.asIdentifier()
|
|
339
|
+
typeName = [typeNamespaceIdent, typeName].join('.')
|
|
340
|
+
typeInfo.inflection.singular = [typeNamespaceIdent, typeInfo.inflection.singular].join('.')
|
|
341
|
+
typeInfo.inflection.plural = [typeNamespaceIdent, typeInfo.inflection.plural].join('.')
|
|
347
342
|
}
|
|
348
343
|
|
|
349
344
|
if (element.type.ref?.length > 1) {
|
|
@@ -370,8 +365,25 @@ class Resolver {
|
|
|
370
365
|
// handle typeof (unless it has already been handled above)
|
|
371
366
|
const target = element.target?.name ?? element.type?.ref?.join('.') ?? element.type
|
|
372
367
|
if (target && !typeInfo.isDeepRequire) {
|
|
373
|
-
const { propertyAccess } =
|
|
374
|
-
if (
|
|
368
|
+
const { propertyAccess, scope } = this.visitor.entityRepository.getByFq(target) ?? {}
|
|
369
|
+
if (scope?.length && typeInfo.inflection) {
|
|
370
|
+
let { singular, plural } = typeInfo.inflection
|
|
371
|
+
// remove already added namespace, so the scope is added after the namespace
|
|
372
|
+
// i.e. _common.Book.texts instead of Book._common.texts
|
|
373
|
+
if (typeNamespaceIdent) {
|
|
374
|
+
if (singular.startsWith(typeNamespaceIdent)) {
|
|
375
|
+
singular = singular.substring(typeNamespaceIdent.length+1)
|
|
376
|
+
}
|
|
377
|
+
if (plural.startsWith(typeNamespaceIdent)) {
|
|
378
|
+
plural = plural.substring(typeNamespaceIdent.length+1)
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
// update inflections with proper prefix, e.g. Books.text, Books.texts
|
|
382
|
+
typeInfo.inflection = {
|
|
383
|
+
singular: [typeNamespaceIdent, ...scope, singular].filter(Boolean).join('.'),
|
|
384
|
+
plural: [typeNamespaceIdent,...scope, plural].filter(Boolean).join('.')
|
|
385
|
+
}
|
|
386
|
+
} else if (propertyAccess?.length) {
|
|
375
387
|
const element = target.slice(0, -propertyAccess.join('.').length - 1)
|
|
376
388
|
const access = this.visitor.inlineDeclarationResolver.getTypeLookup(propertyAccess)
|
|
377
389
|
// singular, as we have to access the property of the entity
|
|
@@ -452,6 +464,7 @@ class Resolver {
|
|
|
452
464
|
|
|
453
465
|
const cardinality = getMaxCardinality(element)
|
|
454
466
|
|
|
467
|
+
/** @type {TypeResolveInfo} */
|
|
455
468
|
const result = {
|
|
456
469
|
isBuiltin: false, // will be rectified in the corresponding handlers, if needed
|
|
457
470
|
isInlineDeclaration: false,
|
|
@@ -569,7 +582,7 @@ class Resolver {
|
|
|
569
582
|
* @returns @see resolveType
|
|
570
583
|
*/
|
|
571
584
|
resolveTypeName(t, into) {
|
|
572
|
-
const result = into ?? {}
|
|
585
|
+
const result = into ?? /** @type {TypeResolveInfo} */({})
|
|
573
586
|
const path = t.split('.')
|
|
574
587
|
const builtin = this.builtinResolver.resolveBuiltin(path)
|
|
575
588
|
if (builtin === undefined) {
|
package/lib/typedefs.d.ts
CHANGED
|
@@ -13,7 +13,7 @@ export module resolver {
|
|
|
13
13
|
compositions?: { target: string }[]
|
|
14
14
|
doc?: string,
|
|
15
15
|
elements?: { [key: string]: EntityCSN }
|
|
16
|
-
key?:
|
|
16
|
+
key?: boolean // custom!!
|
|
17
17
|
keys?: { [key:string]: any }
|
|
18
18
|
kind: string,
|
|
19
19
|
includes?: string[]
|
|
@@ -25,6 +25,8 @@ export module resolver {
|
|
|
25
25
|
target?: string,
|
|
26
26
|
type: string | ref,
|
|
27
27
|
name: string,
|
|
28
|
+
'@singular'?: string,
|
|
29
|
+
'@plural'?: string,
|
|
28
30
|
'@odata.draft.enabled'?: boolean // custom!
|
|
29
31
|
_unresolved?: boolean
|
|
30
32
|
isRefNotNull?: boolean // custom!
|
|
@@ -46,7 +48,8 @@ export module resolver {
|
|
|
46
48
|
|
|
47
49
|
|
|
48
50
|
export type EnumCSN = EntityCSN & {
|
|
49
|
-
enum: {[key:name]: string}
|
|
51
|
+
enum: {[key:name]: string},
|
|
52
|
+
resolvedType?: string // custom property! When .type points to a ref, the visitor will resolve the ref into this property
|
|
50
53
|
}
|
|
51
54
|
|
|
52
55
|
export type CSN = {
|
package/lib/util.js
CHANGED
|
@@ -15,10 +15,10 @@ if (process.version.startsWith('v14')) {
|
|
|
15
15
|
|
|
16
16
|
const last = /\w+$/
|
|
17
17
|
|
|
18
|
-
const annotations = {
|
|
18
|
+
const annotations = /** @type {const} */ ({
|
|
19
19
|
singular: ['@singular'],
|
|
20
20
|
plural: ['@plural'],
|
|
21
|
-
}
|
|
21
|
+
})
|
|
22
22
|
|
|
23
23
|
/**
|
|
24
24
|
* Converts a camelCase string to snake_case.
|
package/lib/visitor.js
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
|
-
const
|
|
4
|
-
|
|
5
|
-
const { isView, isUnresolved, propagateForeignKeys, collectDraftEnabledEntities, isDraftEnabled, isType, isProjection, getMaxCardinality, isViewOrProjection, isEnum, isEntity } = require('./csn')
|
|
3
|
+
const { propagateForeignKeys, propagateInflectionAnnotations, collectDraftEnabledEntities, isDraftEnabled, isType, getMaxCardinality, isViewOrProjection, isEnum, isEntity, lookUpRefType } = require('./csn')
|
|
6
4
|
// eslint-disable-next-line no-unused-vars
|
|
7
5
|
const { SourceFile, FileRepository, Buffer, Path } = require('./file')
|
|
8
6
|
const { FlatInlineDeclarationResolver, StructuredInlineDeclarationResolver } = require('./components/inline')
|
|
@@ -44,11 +42,12 @@ class Visitor {
|
|
|
44
42
|
* @param {{xtended: CSN, inferred: CSN}} csn - root CSN
|
|
45
43
|
*/
|
|
46
44
|
constructor(csn) {
|
|
47
|
-
propagateForeignKeys(csn.xtended)
|
|
48
45
|
propagateForeignKeys(csn.inferred)
|
|
49
|
-
|
|
46
|
+
propagateInflectionAnnotations(csn)
|
|
50
47
|
collectDraftEnabledEntities(csn.inferred)
|
|
51
|
-
|
|
48
|
+
|
|
49
|
+
// xtendend csn not required after this point -> continue with inferred
|
|
50
|
+
this.csn = csn.inferred
|
|
52
51
|
|
|
53
52
|
/** @type {Context[]} **/
|
|
54
53
|
this.contexts = []
|
|
@@ -74,41 +73,8 @@ class Visitor {
|
|
|
74
73
|
* Visits all definitions within the CSN definitions.
|
|
75
74
|
*/
|
|
76
75
|
visitDefinitions() {
|
|
77
|
-
for (const [name, entity] of Object.entries(this.csn.
|
|
78
|
-
|
|
79
|
-
this.visitEntity(name, this.csn.inferred.definitions[name])
|
|
80
|
-
} else if (isProjection(entity) || !isUnresolved(entity)) {
|
|
81
|
-
this.visitEntity(name, entity)
|
|
82
|
-
} else {
|
|
83
|
-
LOG.warn(`Skipping unresolved entity: ${name}`)
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
// FIXME: optimise
|
|
87
|
-
// We are currently working with two flavours of CSN:
|
|
88
|
-
// xtended, as it is as close as possible to an OOP class hierarchy
|
|
89
|
-
// inferred, as it contains information missing in xtended
|
|
90
|
-
// This is less than optimal and has to be revisited at some point!
|
|
91
|
-
const handledKeys = new Set(Object.keys(this.csn.xtended.definitions))
|
|
92
|
-
// we are looking for autoexposed entities in services
|
|
93
|
-
const missing = Object.entries(this.csn.inferred.definitions).filter(([key]) => !key.endsWith('.texts') &&!handledKeys.has(key))
|
|
94
|
-
for (const [name, entity] of missing) {
|
|
95
|
-
// instead of using the definition from inferred CSN, we refer to the projected entity from xtended CSN instead.
|
|
96
|
-
// The latter contains the CSN fixes (propagated foreign keys, etc) and none of the localised fields we don't handle yet.
|
|
97
|
-
if (entity.projection) {
|
|
98
|
-
const targetName = entity.projection.from.ref[0]
|
|
99
|
-
// FIXME: references to types of entity properties may be missing from xtendend flavour (see #103)
|
|
100
|
-
// this should be revisted once we settle on a single flavour.
|
|
101
|
-
const target = this.csn.xtended.definitions[targetName] ?? this.csn.inferred.definitions[targetName]
|
|
102
|
-
if (target.kind !== 'type') {
|
|
103
|
-
// skip if the target is a property, like in:
|
|
104
|
-
// books: Association to many Author.books ...
|
|
105
|
-
// as this would result in a type definition that
|
|
106
|
-
// name-clashes with the actual declaration of Author
|
|
107
|
-
this.visitEntity(name, target)
|
|
108
|
-
}
|
|
109
|
-
} else {
|
|
110
|
-
LOG.error(`Expecting an autoexposed projection within a service. Skipping ${name}`)
|
|
111
|
-
}
|
|
76
|
+
for (const [name, entity] of Object.entries(this.csn.definitions)) {
|
|
77
|
+
this.visitEntity(name, entity)
|
|
112
78
|
}
|
|
113
79
|
}
|
|
114
80
|
|
|
@@ -119,15 +85,8 @@ class Visitor {
|
|
|
119
85
|
* @returns {[string, object][]} array of key name and key element pairs
|
|
120
86
|
*/
|
|
121
87
|
#keys(fq) {
|
|
122
|
-
// FIXME: this is actually pretty bad, as not only have to propagate keys through
|
|
123
|
-
// both flavours of CSN (see constructor), but we are now also collecting them from
|
|
124
|
-
// both flavours and deduplicating them.
|
|
125
|
-
// xtended contains keys that have been inherited from parents
|
|
126
|
-
// inferred contains keys from queried entities (thing `entity Foo as select from Bar`, where Bar has keys)
|
|
127
|
-
// So we currently need them both.
|
|
128
88
|
return Object.entries({
|
|
129
|
-
...this.csn.
|
|
130
|
-
...this.csn.xtended.definitions[fq]?.keys ?? {}
|
|
89
|
+
...this.csn.definitions[fq]?.keys ?? {}
|
|
131
90
|
})
|
|
132
91
|
}
|
|
133
92
|
|
|
@@ -233,7 +192,7 @@ class Visitor {
|
|
|
233
192
|
// FIXME: replace with resolution/entity::asIdentifier
|
|
234
193
|
const toLocalIdent = ({ns, clean, fq}) => {
|
|
235
194
|
// types are not inflected, so don't change those to singular
|
|
236
|
-
const csn = this.csn.
|
|
195
|
+
const csn = this.csn.definitions[fq]
|
|
237
196
|
const ident = isType(csn)
|
|
238
197
|
? clean
|
|
239
198
|
: this.resolver.inflect({csn, plainName: clean}).singular
|
|
@@ -263,6 +222,7 @@ class Visitor {
|
|
|
263
222
|
.reverse() // reverse so that own aspect A is applied before extensions B,C: B(C(A(Entity)))
|
|
264
223
|
.reduce((wrapped, ancestor) => `${asIdentifier({info: ancestor, wrapper: name => `_${name}Aspect`, relative: file.path})}(${wrapped})`, 'Base')
|
|
265
224
|
|
|
225
|
+
const inheritedElements = !isViewOrProjection(entity) ? info.inheritedElements : null
|
|
266
226
|
this.contexts.push({ entity: fq })
|
|
267
227
|
|
|
268
228
|
// CLASS ASPECT
|
|
@@ -274,10 +234,7 @@ class Visitor {
|
|
|
274
234
|
const resolverOptions = { forceInlineStructs: isEntity(entity) && configuration.inlineDeclarations === 'flat'}
|
|
275
235
|
|
|
276
236
|
for (let [ename, element] of Object.entries(entity.elements ?? [])) {
|
|
277
|
-
if (
|
|
278
|
-
LOG.warn(`referring to .texts property in ${fq}. This is currently not supported and will be ignored.`)
|
|
279
|
-
continue
|
|
280
|
-
}
|
|
237
|
+
if (inheritedElements?.has(ename)) continue
|
|
281
238
|
this.visitElement({name: ename, element, file, buffer, resolverOptions})
|
|
282
239
|
|
|
283
240
|
// make foreign keys explicit
|
|
@@ -292,7 +249,8 @@ class Visitor {
|
|
|
292
249
|
LOG.error(`Attempting to generate a foreign key reference called '${foreignKey}' in type definition for entity ${fq}. But a property of that name is already defined explicitly. Consider renaming that property.`)
|
|
293
250
|
} else {
|
|
294
251
|
const kelement = Object.assign(Object.create(originalKeyElement), {
|
|
295
|
-
isRefNotNull: !!element.notNull || !!element.key
|
|
252
|
+
isRefNotNull: !!element.notNull || !!element.key,
|
|
253
|
+
key: element.key
|
|
296
254
|
})
|
|
297
255
|
this.visitElement({name: foreignKey, element: kelement, file, buffer, resolverOptions})
|
|
298
256
|
}
|
|
@@ -301,7 +259,7 @@ class Visitor {
|
|
|
301
259
|
}
|
|
302
260
|
|
|
303
261
|
// store inline enums for later handling, as they have to go into one common "static elements" wrapper
|
|
304
|
-
if (isInlineEnumType(element, this.csn
|
|
262
|
+
if (isInlineEnumType(element, this.csn)) {
|
|
305
263
|
enums.push(element)
|
|
306
264
|
}
|
|
307
265
|
}
|
|
@@ -314,7 +272,10 @@ class Visitor {
|
|
|
314
272
|
initialiser: propertyToInlineEnumName(clean, e.name),
|
|
315
273
|
isStatic: true,
|
|
316
274
|
}))
|
|
317
|
-
|
|
275
|
+
if (typeof e?.type !== 'string' && e?.type?.ref) {
|
|
276
|
+
e.resolvedType = /** @type {string} */(lookUpRefType(this.csn, e.type.ref)?.type)
|
|
277
|
+
}
|
|
278
|
+
file.addInlineEnum(clean, fq, e.name, csnToEnumPairs(e, {unwrapVals: true}), buffer, eDoc)
|
|
318
279
|
}
|
|
319
280
|
|
|
320
281
|
if ('kind' in entity) {
|
|
@@ -357,7 +318,7 @@ class Visitor {
|
|
|
357
318
|
*/
|
|
358
319
|
#printEntity(fq, entity) {
|
|
359
320
|
const info = this.entityRepository.getByFqOrThrow(fq)
|
|
360
|
-
const { namespace: ns, entityName: clean, inflection } = info
|
|
321
|
+
const { namespace: ns, entityName: clean, inflection, scope } = info
|
|
361
322
|
const file = this.fileRepository.getNamespaceFile(ns)
|
|
362
323
|
let { singular, plural } = inflection
|
|
363
324
|
|
|
@@ -373,7 +334,7 @@ class Visitor {
|
|
|
373
334
|
// as types are not inflected, their singular will always clash and there is also no plural for them anyway -> skip
|
|
374
335
|
// if the user defined their entities in singular form we would also have a false positive here -> skip
|
|
375
336
|
const namespacedSingular = `${ns.asNamespace()}.${singular}`
|
|
376
|
-
if (!isType(entity) && namespacedSingular !== fq && namespacedSingular in this.csn.
|
|
337
|
+
if (!isType(entity) && namespacedSingular !== fq && namespacedSingular in this.csn.definitions) {
|
|
377
338
|
LOG.error(
|
|
378
339
|
`Derived singular '${singular}' for your entity '${fq}', 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.`
|
|
379
340
|
)
|
|
@@ -386,20 +347,15 @@ class Visitor {
|
|
|
386
347
|
? file.getSubNamespace(this.resolver.trimNamespace(parent.name))
|
|
387
348
|
: file.classes
|
|
388
349
|
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
// in case of projections `entity` is empty -> retrieve from inferred csn where the actual properties are rolled out
|
|
398
|
-
const target = isProjection(entity) || isView(entity)
|
|
399
|
-
? this.csn.inferred.definitions[fq]
|
|
400
|
-
: entity
|
|
350
|
+
if (scope?.length > 0) {
|
|
351
|
+
/** @param {string} n - name of entity */
|
|
352
|
+
const scoped = n => [...scope, n].join('.')
|
|
353
|
+
file.addInflection(scoped(singular), scoped(plural), scoped(clean))
|
|
354
|
+
} else {
|
|
355
|
+
file.addInflection(singular, plural, clean)
|
|
356
|
+
}
|
|
401
357
|
|
|
402
|
-
this.#aspectify(fq,
|
|
358
|
+
this.#aspectify(fq, entity, buffer, { cleanName: singular })
|
|
403
359
|
|
|
404
360
|
buffer.add(overrideNameProperty(singular, entity.name))
|
|
405
361
|
buffer.add(`Object.defineProperty(${singular}, 'is_singular', { value: true })`)
|
|
@@ -516,7 +472,7 @@ class Visitor {
|
|
|
516
472
|
if (isEnum(type) && !isReferenceType(type) && this.resolver.builtinResolver.resolveBuiltin(type.type)) {
|
|
517
473
|
file.addEnum(fq, entityName, csnToEnumPairs(type), docify(type.doc))
|
|
518
474
|
} else {
|
|
519
|
-
const isEnumReference = typeof type.type === 'string' && isEnum(this.csn.
|
|
475
|
+
const isEnumReference = typeof type.type === 'string' && isEnum(this.csn.definitions[type?.type])
|
|
520
476
|
// alias
|
|
521
477
|
file.addType(fq, entityName, this.resolver.resolveAndRequire(type, file).typeName, isEnumReference)
|
|
522
478
|
}
|
|
@@ -579,13 +535,18 @@ class Visitor {
|
|
|
579
535
|
|
|
580
536
|
docify(service.doc).forEach(d => { buffer.add(d) })
|
|
581
537
|
// file.addImport(new Path(['cds'], '')) TODO make sap/cds import work
|
|
582
|
-
buffer.addIndentedBlock(
|
|
538
|
+
buffer.addIndentedBlock('export default class {', () => {
|
|
583
539
|
Object.entries(service.operations ?? {}).forEach(([name, {doc}]) => {
|
|
584
540
|
buffer.add(docify(doc))
|
|
585
|
-
buffer.add(
|
|
541
|
+
buffer.add(createMember({
|
|
542
|
+
name,
|
|
543
|
+
type: `typeof ${name}`,
|
|
544
|
+
isStatic: true,
|
|
545
|
+
isReadonly: true,
|
|
546
|
+
isDeclare: true,
|
|
547
|
+
}))
|
|
586
548
|
})
|
|
587
549
|
}, '}')
|
|
588
|
-
buffer.add(`export default ${serviceNameSimple}`)
|
|
589
550
|
buffer.blankLine()
|
|
590
551
|
file.addService(service.name)
|
|
591
552
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cap-js/cds-typer",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.32.1",
|
|
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",
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
"cds-typer": "./lib/cli.js"
|
|
43
43
|
},
|
|
44
44
|
"peerDependencies": {
|
|
45
|
-
"@cap-js/cds-types": ">=0.
|
|
45
|
+
"@cap-js/cds-types": ">=0.9",
|
|
46
46
|
"@sap/cds": ">=8"
|
|
47
47
|
},
|
|
48
48
|
"devDependencies": {
|