@cap-js/cds-typer 0.27.0 → 0.28.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 +16 -2
- package/lib/components/basedefs.js +14 -0
- package/lib/components/class.js +35 -0
- package/lib/components/enum.js +2 -2
- package/lib/components/inline.js +13 -8
- package/lib/components/wrappers.js +33 -1
- package/lib/config.js +2 -1
- package/lib/csn.js +110 -147
- package/lib/file.js +32 -10
- package/lib/resolution/entity.js +1 -0
- package/lib/resolution/resolver.js +14 -9
- package/lib/typedefs.d.ts +33 -0
- package/lib/visitor.js +97 -49
- package/library/cds.hana.ts +2 -2
- package/package.json +70 -2
package/CHANGELOG.md
CHANGED
|
@@ -4,7 +4,20 @@ 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.29.0 - TBD
|
|
8
|
+
|
|
9
|
+
## Version 0.28.0 - 24-10-24
|
|
10
|
+
### Added
|
|
11
|
+
- Schema definition for `cds.typer` options in `package.json` and `.cdsrc-*.json` files
|
|
12
|
+
- Added a static `elements` property to all entities, which allows access to the `LinkedDefinitions` instance of an entity's elements
|
|
13
|
+
- Schema definition for `typescript` cds build task.
|
|
14
|
+
- `.drafts` property of any entity `E` is now of type `DraftOf<E>`, or `DraftsOf<E>` for plurals, respectively. This type exposes dditional properties that are available on drafts during runtime.
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
- Entity elements of named structured types are flattened when using the option `--inlineDeclarations flat`
|
|
18
|
+
- `override` modifier on `.kind` property is now only generated if the property is actually inherited, satisfying strict `tsconfig.json`s
|
|
19
|
+
- Properly support mandatory (`not null`) action parameters with `array of` types
|
|
20
|
+
- Static property `.drafts` is only create for entity classes that are actually draft enabled
|
|
8
21
|
|
|
9
22
|
## Version 0.27.0 - 2024-10-02
|
|
10
23
|
### Changed
|
|
@@ -13,7 +26,8 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/).
|
|
|
13
26
|
|
|
14
27
|
### Fixed
|
|
15
28
|
- Fix build task for projects with spaces
|
|
16
|
-
-
|
|
29
|
+
- Fix a bug where cds-typer would produce redundant type declarations when the model contains an associations to another entity's property
|
|
30
|
+
- Reintroduce default value `'.'` for `--outputDirectory`
|
|
17
31
|
|
|
18
32
|
## Version 0.26.0 - 2024-09-11
|
|
19
33
|
### Added
|
|
@@ -13,6 +13,10 @@ const timeRegex = '`${number}${number}:${number}${number}:${number}${number}`'
|
|
|
13
13
|
const baseDefinitions = new SourceFile('_')
|
|
14
14
|
// FIXME: this should be a library someday
|
|
15
15
|
baseDefinitions.addPreamble(`
|
|
16
|
+
import { type } from '@sap/cds'
|
|
17
|
+
|
|
18
|
+
export type ElementsOf<T> = {[name in keyof Required<T>]: type }
|
|
19
|
+
|
|
16
20
|
export namespace Association {
|
|
17
21
|
export type to <T> = T;
|
|
18
22
|
export namespace to {
|
|
@@ -38,6 +42,16 @@ export type EntitySet<T> = T[] & {
|
|
|
38
42
|
data (input:object) : T
|
|
39
43
|
};
|
|
40
44
|
|
|
45
|
+
export type DraftEntity<T> = T & {
|
|
46
|
+
IsActiveEntity?: boolean | null
|
|
47
|
+
HasActiveEntity?: boolean | null
|
|
48
|
+
HasDraftEntity?: boolean | null
|
|
49
|
+
DraftAdministrativeData_DraftUUID?: string | null
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export type DraftOf<T> = { new(...args: any[]): DraftEntity<T> }
|
|
53
|
+
export type DraftsOf<T> = typeof Array<DraftEntity<T>>
|
|
54
|
+
|
|
41
55
|
export type DeepRequired<T> = {
|
|
42
56
|
[K in keyof T]: DeepRequired<T[K]>
|
|
43
57
|
} & Exclude<Required<T>, null>;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Create a class member with given modifiers in the right order.
|
|
3
|
+
* @param {object} options - options
|
|
4
|
+
* @param {string} options.name - the name of the member
|
|
5
|
+
* @param {string} [options.type] - the type of the member
|
|
6
|
+
* @param {string} [options.initialiser] - the initialiser for the member
|
|
7
|
+
* @param {string} [options.statementEnd] - the closing character for the member
|
|
8
|
+
* @param {boolean} [options.isDeclare] - whether the member is declared
|
|
9
|
+
* @param {boolean} [options.isStatic] - whether the member is static
|
|
10
|
+
* @param {boolean} [options.isReadonly] - whether the member is readonly
|
|
11
|
+
* @param {boolean} [options.isOverride] - whether the member is an override
|
|
12
|
+
*/
|
|
13
|
+
function createMember ({name, type = undefined, initialiser = undefined, statementEnd = ';', isDeclare = false, isStatic = false, isReadonly = false, isOverride = false}) {
|
|
14
|
+
if (isDeclare && isOverride) throw new Error('member cannot have both declare and override modifiers')
|
|
15
|
+
|
|
16
|
+
const parts = []
|
|
17
|
+
|
|
18
|
+
if (isDeclare) parts.push('declare')
|
|
19
|
+
if (isStatic) parts.push('static')
|
|
20
|
+
if (isOverride) parts.push('override')
|
|
21
|
+
if (isReadonly) parts.push('readonly')
|
|
22
|
+
|
|
23
|
+
parts.push(type ? `${name}: ${type}` : name)
|
|
24
|
+
|
|
25
|
+
if (initialiser) parts.push(`= ${initialiser}`)
|
|
26
|
+
|
|
27
|
+
const member = parts.join(' ')
|
|
28
|
+
return statementEnd
|
|
29
|
+
? `${member}${statementEnd}`
|
|
30
|
+
: member
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
module.exports = {
|
|
34
|
+
createMember
|
|
35
|
+
}
|
package/lib/components/enum.js
CHANGED
|
@@ -55,12 +55,12 @@ const enumVal = (key, value, enumType) => enumType === 'cds.String' ? JSON.strin
|
|
|
55
55
|
function printEnum(buffer, name, kvs, options = {}, doc=[]) {
|
|
56
56
|
const opts = {...{export: true}, ...options}
|
|
57
57
|
buffer.add('// enum')
|
|
58
|
-
if (opts.export)
|
|
58
|
+
if (opts.export) buffer.add(doc)
|
|
59
59
|
buffer.addIndentedBlock(`${opts.export ? 'export ' : ''}const ${name} = {`, () =>
|
|
60
60
|
kvs.forEach(([k, v]) => { buffer.add(`${normalise(k)}: ${v},`) })
|
|
61
61
|
, '} as const;')
|
|
62
62
|
buffer.add(`${opts.export ? 'export ' : ''}type ${name} = ${stringifyEnumType(kvs)}`)
|
|
63
|
-
buffer.
|
|
63
|
+
buffer.blankLine()
|
|
64
64
|
}
|
|
65
65
|
|
|
66
66
|
/**
|
package/lib/components/inline.js
CHANGED
|
@@ -4,6 +4,7 @@ const { normalise } = require('./identifier')
|
|
|
4
4
|
const { docify } = require('./wrappers')
|
|
5
5
|
|
|
6
6
|
/** @typedef {import('../resolution/resolver').TypeResolveInfo} TypeResolveInfo */
|
|
7
|
+
/** @typedef {import('../resolution/resolver').TypeResolveOptions} TypeResolverOptions */
|
|
7
8
|
/** @typedef {import('../typedefs').visitor.Inflection} Inflection */
|
|
8
9
|
/** @typedef {import('../typedefs').resolver.PropertyModifier} PropertyModifier */
|
|
9
10
|
/** @typedef {import('../visitor').Visitor} Visitor */
|
|
@@ -34,9 +35,10 @@ class InlineDeclarationResolver {
|
|
|
34
35
|
* @param {any} items - properties of the declaration we are resolving
|
|
35
36
|
* @param {TypeResolveInfo} into - @see Visitor.resolveType
|
|
36
37
|
* @param {SourceFile} relativeTo - file to which the resolved type should be relative to
|
|
38
|
+
* @param {TypeResolverOptions} [options] - resolver options
|
|
37
39
|
* @public
|
|
38
40
|
*/
|
|
39
|
-
resolveInlineDeclaration(items, into, relativeTo) {
|
|
41
|
+
resolveInlineDeclaration(items, into, relativeTo, options) {
|
|
40
42
|
const dummy = new SourceFile(relativeTo.path.asDirectory())
|
|
41
43
|
dummy.classes.currentIndent = relativeTo.classes.currentIndent
|
|
42
44
|
dummy.classes.add('{')
|
|
@@ -51,7 +53,9 @@ class InlineDeclarationResolver {
|
|
|
51
53
|
const se = (typeof subelement === 'string')
|
|
52
54
|
? this.visitor.resolver.resolveTypeName(subelement)
|
|
53
55
|
: subelement
|
|
54
|
-
|
|
56
|
+
// resolver options need to be passed through, otherwise deep expand of struct types to flat
|
|
57
|
+
// does not work
|
|
58
|
+
into.structuredType[subname] = this.visitor.visitElement({name: subname, element: se, file: dummy, resolverOptions: options})
|
|
55
59
|
}
|
|
56
60
|
dummy.classes.outdent()
|
|
57
61
|
dummy.classes.add('}')
|
|
@@ -73,14 +77,15 @@ class InlineDeclarationResolver {
|
|
|
73
77
|
* @param {SourceFile} options.file - the namespace file the surrounding entity is being printed into.
|
|
74
78
|
* @param {Buffer} options.buffer - buffer to add the definition to. If no buffer is passed, the passed file's class buffer is used instead.
|
|
75
79
|
* @param {PropertyModifier[]} options.modifiers - modifiers to add to each generated property
|
|
80
|
+
* @param {TypeResolverOptions} [options.resolverOptions] - resolver options
|
|
76
81
|
* @public
|
|
77
82
|
*/
|
|
78
|
-
visitElement({name, element, file, buffer = file.classes, modifiers = []}) {
|
|
83
|
+
visitElement({name, element, file, buffer = file.classes, modifiers = [], resolverOptions}) {
|
|
79
84
|
this.depth++
|
|
80
85
|
for (const d of docify(element.doc)) {
|
|
81
86
|
buffer.add(d)
|
|
82
87
|
}
|
|
83
|
-
const type = this.visitor.resolver.resolveAndRequire(element, file)
|
|
88
|
+
const type = this.visitor.resolver.resolveAndRequire(element, file, resolverOptions)
|
|
84
89
|
this.depth--
|
|
85
90
|
if (this.depth === 0) {
|
|
86
91
|
this.printInlineType({fq: name, type, buffer, modifiers})
|
|
@@ -106,7 +111,8 @@ class InlineDeclarationResolver {
|
|
|
106
111
|
* @public
|
|
107
112
|
*/
|
|
108
113
|
getPropertyDatatype(type, typeName = type.typeName) {
|
|
109
|
-
|
|
114
|
+
// do not append null if already added to type
|
|
115
|
+
return type.typeInfo.isNotNull ? typeName : typeName.endsWith('| null') ? typeName : `${typeName} | null`
|
|
110
116
|
}
|
|
111
117
|
|
|
112
118
|
/**
|
|
@@ -268,10 +274,9 @@ class StructuredInlineDeclarationResolver extends InlineDeclarationResolver {
|
|
|
268
274
|
* @type {InlineDeclarationResolver['printInlineType']}
|
|
269
275
|
*/
|
|
270
276
|
printInlineType({fq, type, buffer, modifiers, statementEnd}) {
|
|
271
|
-
// FIXME: indent not quite right
|
|
272
277
|
const sub = new Buffer()
|
|
273
|
-
|
|
274
|
-
buffer.add(
|
|
278
|
+
this.flatten({fq, type, buffer: sub, modifiers, statementEnd})
|
|
279
|
+
buffer.add(sub.parts)
|
|
275
280
|
}
|
|
276
281
|
|
|
277
282
|
/**
|
|
@@ -17,6 +17,27 @@ const createKey = t => `${base}.Key<${t}>`
|
|
|
17
17
|
*/
|
|
18
18
|
const createKeysOf = t => `${base}.KeysOf<${t}>`
|
|
19
19
|
|
|
20
|
+
/**
|
|
21
|
+
* Wraps type into DraftOf type.
|
|
22
|
+
* @param {string} t - the type name.
|
|
23
|
+
* @returns {string}
|
|
24
|
+
*/
|
|
25
|
+
const createDraftOf = t => `${base}.DraftOf<${t}>`
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Wraps type into DraftsOf type.
|
|
29
|
+
* @param {string} t - the type name.
|
|
30
|
+
* @returns {string}
|
|
31
|
+
*/
|
|
32
|
+
const createDraftsOf = t => `${base}.DraftsOf<${t}>`
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Wraps type into ElementsOf type.
|
|
36
|
+
* @param {string} t - the type name.
|
|
37
|
+
* @returns {string}
|
|
38
|
+
*/
|
|
39
|
+
const createElementsOf = t => `${base}.ElementsOf<${t}>`
|
|
40
|
+
|
|
20
41
|
/**
|
|
21
42
|
* Wraps type into association to scalar.
|
|
22
43
|
* @param {string} t - the singular type name.
|
|
@@ -98,10 +119,20 @@ const docify = doc => {
|
|
|
98
119
|
return ['/**'].concat(lines.map(line => `* ${line}`)).concat(['*/'])
|
|
99
120
|
}
|
|
100
121
|
|
|
122
|
+
/**
|
|
123
|
+
* Wraps a string in single quotes. No escaping is done, so use with caution.
|
|
124
|
+
* @param {string} s - the string to wrap.
|
|
125
|
+
* @returns {string}
|
|
126
|
+
*/
|
|
127
|
+
const stringIdent = s => `'${s}'`
|
|
128
|
+
|
|
101
129
|
module.exports = {
|
|
102
130
|
createArrayOf,
|
|
131
|
+
createDraftOf,
|
|
132
|
+
createDraftsOf,
|
|
103
133
|
createKey,
|
|
104
134
|
createKeysOf,
|
|
135
|
+
createElementsOf,
|
|
105
136
|
createObjectOf,
|
|
106
137
|
createPromiseOf,
|
|
107
138
|
createUnionOf,
|
|
@@ -110,5 +141,6 @@ module.exports = {
|
|
|
110
141
|
createCompositionOfOne,
|
|
111
142
|
createCompositionOfMany,
|
|
112
143
|
deepRequire,
|
|
113
|
-
docify
|
|
144
|
+
docify,
|
|
145
|
+
stringIdent
|
|
114
146
|
}
|
package/lib/config.js
CHANGED
package/lib/csn.js
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
const
|
|
1
|
+
const { LOG } = require('./logging')
|
|
2
|
+
|
|
3
|
+
const DRAFT_ENABLED_ANNO = '@odata.draft.enabled'
|
|
4
|
+
/** @type {string[]} */
|
|
5
|
+
const draftEnabledEntities = []
|
|
2
6
|
|
|
3
7
|
/** @typedef {import('./typedefs').resolver.CSN} CSN */
|
|
4
8
|
/** @typedef {import('./typedefs').resolver.EntityCSN} EntityCSN */
|
|
@@ -40,9 +44,9 @@ const isUnresolved = entity => entity._unresolved === true
|
|
|
40
44
|
const isCsnAny = entity => entity?.constructor?.name === 'any'
|
|
41
45
|
|
|
42
46
|
/**
|
|
43
|
-
* @param {
|
|
47
|
+
* @param {string} fq - the fqn of an entity
|
|
44
48
|
*/
|
|
45
|
-
const isDraftEnabled =
|
|
49
|
+
const isDraftEnabled = fq => draftEnabledEntities.includes(fq)
|
|
46
50
|
|
|
47
51
|
/**
|
|
48
52
|
* @param {EntityCSN} entity - the entity
|
|
@@ -87,183 +91,151 @@ const getProjectionTarget = entity => isProjection(entity)
|
|
|
87
91
|
? entity.projection?.from?.ref?.[0]
|
|
88
92
|
: undefined
|
|
89
93
|
|
|
90
|
-
class
|
|
91
|
-
/** @type {Set<string>} */
|
|
92
|
-
#positives = new Set()
|
|
93
|
-
/** @type {{[key: string]: boolean}} */
|
|
94
|
-
#draftable = {}
|
|
95
|
-
/** @type {{[key: string]: string}} */
|
|
96
|
-
#projections = {}
|
|
94
|
+
class DraftEnabledEntityCollector {
|
|
97
95
|
/** @type {EntityCSN[]} */
|
|
98
|
-
#
|
|
96
|
+
#draftRoots = []
|
|
97
|
+
/** @type {string[]} */
|
|
98
|
+
#serviceNames = []
|
|
99
99
|
/** @type {CSN | undefined} */
|
|
100
100
|
#csn
|
|
101
|
-
|
|
102
|
-
this.#csn = c
|
|
103
|
-
if (c === undefined) return
|
|
104
|
-
this.#entities = Object.values(c.definitions)
|
|
105
|
-
this.#projections = this.#entities.reduce((pjs, entity) => {
|
|
106
|
-
if (isProjection(entity)) {
|
|
107
|
-
// @ts-ignore - we know that entity is a projection here
|
|
108
|
-
pjs[entity.name] = getProjectionTarget(entity)
|
|
109
|
-
}
|
|
110
|
-
return pjs
|
|
111
|
-
}, {})
|
|
112
|
-
}
|
|
113
|
-
get csn() { return this.#csn }
|
|
101
|
+
#compileError = false
|
|
114
102
|
|
|
115
103
|
/**
|
|
116
|
-
* @
|
|
117
|
-
* @param {boolean} value - whether the entity is draftable.
|
|
104
|
+
* @returns {string[]}
|
|
118
105
|
*/
|
|
119
|
-
#
|
|
120
|
-
|
|
121
|
-
? this.#getDefinition(entityOrFq)
|
|
122
|
-
: entityOrFq
|
|
123
|
-
if (!entity) return // inline definition -- not found in definitions
|
|
124
|
-
entity[annotation] = value
|
|
125
|
-
this.#draftable[entity.name] = value
|
|
126
|
-
if (value) {
|
|
127
|
-
this.#positives.add(entity.name)
|
|
128
|
-
} else {
|
|
129
|
-
this.#positives.delete(entity.name)
|
|
130
|
-
}
|
|
106
|
+
#getServiceNames() {
|
|
107
|
+
return Object.values(this.#csn?.definitions ?? {}).filter(d => d.kind === 'service').map(d => d.name)
|
|
131
108
|
}
|
|
132
109
|
|
|
133
110
|
/**
|
|
134
|
-
* @
|
|
135
|
-
* @returns {boolean}
|
|
111
|
+
* @returns {EntityCSN[]}
|
|
136
112
|
*/
|
|
137
|
-
#
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
// assert(typeof entity !== 'string')
|
|
142
|
-
const name = entity?.name ?? entityOrFq
|
|
143
|
-
// @ts-expect-error - .name not being present means entityOrFq is a string, so name is always a string and therefore a valid index
|
|
144
|
-
return this.#draftable[name] ??= this.#propagateInheritance(entity)
|
|
113
|
+
#collectDraftRoots() {
|
|
114
|
+
return Object.values(this.#csn?.definitions ?? {}).filter(
|
|
115
|
+
d => isEntity(d) && this.#isDraftEnabled(d) && this.#isPartOfAnyService(d.name)
|
|
116
|
+
)
|
|
145
117
|
}
|
|
146
118
|
|
|
147
119
|
/**
|
|
148
|
-
*
|
|
149
|
-
* @
|
|
150
|
-
* @returns {EntityCSN}
|
|
151
|
-
*/
|
|
152
|
-
// @ts-expect-error - poor man's #getDefinitionOrThrow. We are always sure name is a valid key
|
|
153
|
-
#getDefinition(name) { return this.csn?.definitions[name] }
|
|
154
|
-
|
|
155
|
-
/**
|
|
156
|
-
* Propagate draft annotations through inheritance (includes).
|
|
157
|
-
* The latest annotation through the inheritance chain "wins".
|
|
158
|
-
* Annotations on the entity itself are always queued last, so they will always be decisive over ancestors.
|
|
159
|
-
* @param {EntityCSN | undefined} entity - entity to pull draftability from its parents.
|
|
120
|
+
* @param {string} entityName - entity to check
|
|
121
|
+
* @returns {boolean} `true` if entity is part an service
|
|
160
122
|
*/
|
|
161
|
-
#
|
|
162
|
-
|
|
163
|
-
/** @type {(boolean | undefined)[]} */
|
|
164
|
-
const annotations = (entity.includes ?? []).map(parent => this.#getDraftable(parent))
|
|
165
|
-
annotations.push(entity[annotation])
|
|
166
|
-
this.#setDraftable(entity, annotations.filter(a => a !== undefined).at(-1) ?? false)
|
|
123
|
+
#isPartOfAnyService(entityName) {
|
|
124
|
+
return this.#serviceNames.some(s => entityName.startsWith(s))
|
|
167
125
|
}
|
|
168
126
|
|
|
169
127
|
/**
|
|
170
|
-
*
|
|
128
|
+
* Collect all entities that are transitively reachable via compositions from `entity` into `draftNodes`.
|
|
129
|
+
* Check that no entity other than the root node has `@odata.draft.enabled`
|
|
130
|
+
* @param {EntityCSN} entity -
|
|
131
|
+
* @param {string} entityName -
|
|
132
|
+
* @param {EntityCSN} rootEntity - root entity where composition traversal started.
|
|
133
|
+
* @param {Record<string,EntityCSN>} draftEntities - Dictionary of entitys
|
|
171
134
|
*/
|
|
172
|
-
#
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
135
|
+
#collectDraftEntitiesInto(entity, entityName, rootEntity, draftEntities) {
|
|
136
|
+
draftEntities[entityName] = entity
|
|
137
|
+
|
|
138
|
+
for (const elem of Object.values(entity.elements ?? {})) {
|
|
139
|
+
if (!elem.target || elem.type !== 'cds.Composition') continue
|
|
140
|
+
|
|
141
|
+
const draftEntity = this.#csn?.definitions[elem.target]
|
|
142
|
+
const draftEntityName = elem.target
|
|
143
|
+
|
|
144
|
+
if (!draftEntity) {
|
|
145
|
+
throw new Error(`Expecting target to be resolved: ${JSON.stringify(elem, null, 2)}`)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (!this.#isPartOfAnyService(draftEntityName)) {
|
|
149
|
+
LOG.warn(`Ignoring draft entity for composition target ${draftEntityName} because it is not part of a service`)
|
|
150
|
+
continue
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (draftEntity !== rootEntity && this.#isDraftEnabled(draftEntity)) {
|
|
154
|
+
this.#compileError = true
|
|
155
|
+
LOG.error(`Composition in draft-enabled entity can't lead to another entity with "@odata.draft.enabled" (in entity: "${entityName}"/element: ${elem.name})!`)
|
|
156
|
+
delete draftEntities[draftEntityName]
|
|
157
|
+
continue
|
|
158
|
+
}
|
|
184
159
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
160
|
+
if (!this.#isDraftEnabled(draftEntity) && !draftEntities[draftEntityName]) {
|
|
161
|
+
this.#collectDraftEntitiesInto(draftEntity, draftEntityName, rootEntity, draftEntities)
|
|
162
|
+
}
|
|
188
163
|
}
|
|
189
164
|
}
|
|
190
165
|
|
|
191
166
|
/**
|
|
192
|
-
*
|
|
193
|
-
*
|
|
194
|
-
* @param {EntityCSN} entity - entity to propagate all compositions from.
|
|
167
|
+
* @param {EntityCSN} entity - entity to check
|
|
168
|
+
* @returns {boolean}
|
|
195
169
|
*/
|
|
196
|
-
#
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
for (const comp of Object.values(entity.compositions ?? {})) {
|
|
200
|
-
const target = this.#getDefinition(comp.target)
|
|
201
|
-
const current = this.#getDraftable(target)
|
|
202
|
-
if (!current) {
|
|
203
|
-
this.#setDraftable(target, true)
|
|
204
|
-
this.#propagateCompositions(target)
|
|
205
|
-
}
|
|
206
|
-
}
|
|
170
|
+
#isDraftEnabled(entity) {
|
|
171
|
+
return entity[DRAFT_ENABLED_ANNO] === true
|
|
207
172
|
}
|
|
208
173
|
|
|
209
174
|
/** @param {CSN} csn - the full csn */
|
|
210
|
-
|
|
211
|
-
|
|
175
|
+
run(csn) {
|
|
176
|
+
if (!csn) return
|
|
212
177
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
}
|
|
178
|
+
this.#csn = csn
|
|
179
|
+
this.#serviceNames = this.#getServiceNames()
|
|
180
|
+
this.#draftRoots = this.#collectDraftRoots()
|
|
217
181
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
this.#
|
|
222
|
-
}
|
|
182
|
+
for (const draftRoot of this.#draftRoots) {
|
|
183
|
+
/** @type {Record<string,EntityCSN>} */
|
|
184
|
+
const draftEntities = {}
|
|
185
|
+
this.#collectDraftEntitiesInto(draftRoot, draftRoot.name, draftRoot, draftEntities)
|
|
223
186
|
|
|
224
|
-
|
|
187
|
+
for (const draftNode of Object.values(draftEntities)) {
|
|
188
|
+
draftEnabledEntities.push(draftNode.name)
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* If an unreconcilable draft model error occurred, the whole type generation
|
|
193
|
+
* will be cancelled. This aligns with the behavior of commands like e.g.
|
|
194
|
+
* - cds compile srv -4 odata
|
|
195
|
+
* - cds compile srv -4 sql
|
|
196
|
+
* - cds watch
|
|
197
|
+
*/
|
|
198
|
+
if (this.#compileError) throw new Error('Compilation of model failed')
|
|
225
199
|
}
|
|
226
200
|
}
|
|
227
201
|
|
|
228
202
|
// note to self: following doc uses @ homoglyph instead of @, as the latter apparently has special semantics in code listings
|
|
229
203
|
/**
|
|
230
|
-
* We
|
|
231
|
-
*
|
|
204
|
+
* We collect all entities that are draft enabled.
|
|
205
|
+
* (@see `@sap/cds-compiler/lib/transform/draft/db.js#generateDraft`)
|
|
232
206
|
*
|
|
233
|
-
*
|
|
234
|
-
*
|
|
235
|
-
*
|
|
207
|
+
* This includes thwo scenarios:
|
|
208
|
+
* - (a) Entities that are part of a service and have the annotation @odata.draft.enabled
|
|
209
|
+
* - (b) Entities that are draft enabled propagate this property down through compositions.
|
|
210
|
+
* NOTE: The compositions themselves must not be draft enabled, otherwise no draft entity will be generated for them
|
|
236
211
|
* @param {any} csn - the entity
|
|
237
212
|
* @example
|
|
238
|
-
*
|
|
239
|
-
*
|
|
240
|
-
*
|
|
241
|
-
*
|
|
242
|
-
*
|
|
243
|
-
*
|
|
244
|
-
* entity B: F,T {} // draft enabled
|
|
245
|
-
* ```
|
|
213
|
+
* (a)
|
|
214
|
+
* ```cds
|
|
215
|
+
* // service.cds
|
|
216
|
+
* service MyService {
|
|
217
|
+
* @odata.draft.enabled true
|
|
218
|
+
* entity A {}
|
|
246
219
|
*
|
|
247
|
-
*
|
|
248
|
-
*
|
|
249
|
-
*
|
|
250
|
-
* @odata.draft.enabled: true
|
|
251
|
-
* entity A as projection on B {}
|
|
252
|
-
* entity B {} // draft enabled
|
|
220
|
+
* @odata.draft.enabled true
|
|
221
|
+
* entity B {}
|
|
222
|
+
* }
|
|
253
223
|
* ```
|
|
254
|
-
*
|
|
255
|
-
* (
|
|
256
|
-
*
|
|
257
|
-
*
|
|
258
|
-
*
|
|
259
|
-
*
|
|
260
|
-
*
|
|
224
|
+
* @example
|
|
225
|
+
* (b)
|
|
226
|
+
* ```cds
|
|
227
|
+
* // service.cds
|
|
228
|
+
* service MyService {
|
|
229
|
+
* @odata.draft.enabled: true
|
|
230
|
+
* entity A {
|
|
231
|
+
* b: Composition of B
|
|
232
|
+
* }
|
|
233
|
+
* entity B {} // draft enabled
|
|
261
234
|
* }
|
|
262
|
-
* entity B {} // draft enabled
|
|
263
235
|
* ```
|
|
264
236
|
*/
|
|
265
|
-
function
|
|
266
|
-
new
|
|
237
|
+
function collectDraftEnabledEntities(csn) {
|
|
238
|
+
new DraftEnabledEntityCollector().run(csn)
|
|
267
239
|
}
|
|
268
240
|
|
|
269
241
|
/**
|
|
@@ -320,15 +292,6 @@ function propagateForeignKeys(csn) {
|
|
|
320
292
|
}
|
|
321
293
|
}
|
|
322
294
|
|
|
323
|
-
/**
|
|
324
|
-
*
|
|
325
|
-
* @param {any} csn - complete csn
|
|
326
|
-
*/
|
|
327
|
-
function amendCSN(csn) {
|
|
328
|
-
unrollDraftability(csn)
|
|
329
|
-
propagateForeignKeys(csn)
|
|
330
|
-
}
|
|
331
|
-
|
|
332
295
|
/**
|
|
333
296
|
* @param {EntityCSN} entity - the entity
|
|
334
297
|
*/
|
|
@@ -349,7 +312,7 @@ const getProjectionAliases = entity => {
|
|
|
349
312
|
}
|
|
350
313
|
|
|
351
314
|
module.exports = {
|
|
352
|
-
|
|
315
|
+
collectDraftEnabledEntities,
|
|
353
316
|
isView,
|
|
354
317
|
isProjection,
|
|
355
318
|
isViewOrProjection,
|
package/lib/file.js
CHANGED
|
@@ -157,7 +157,7 @@ class SourceFile extends File {
|
|
|
157
157
|
* @param {boolean} [options.isStatic] - whether the lambda is static
|
|
158
158
|
* @param {{positional?: boolean, named?: boolean}} [options.callStyles] - whether to generate positional and/or named call styles
|
|
159
159
|
* @param {string[]?} [options.doc] - documentation for the operation
|
|
160
|
-
* @returns {string} the stringified lambda
|
|
160
|
+
* @returns {[string,string[],string]} the stringified lambda parts
|
|
161
161
|
* @example
|
|
162
162
|
* ```js
|
|
163
163
|
* // note: these samples are actually simplified! See below.
|
|
@@ -186,21 +186,25 @@ class SourceFile extends File {
|
|
|
186
186
|
const callableSignatures = []
|
|
187
187
|
if (callStyles.positional) {
|
|
188
188
|
const paramTypesPositional = parameters.map(({name, type, doc}) => `${doc?'\n'+doc:''}${normalise(name)}: ${type}`).join(', ') // must not include ? modifiers
|
|
189
|
-
callableSignatures.push(
|
|
189
|
+
callableSignatures.push('// positional',`${docStr}(${paramTypesPositional}): ${returns}`) // docs shows up on action consumer side: `.action(...)`
|
|
190
190
|
}
|
|
191
191
|
if (callStyles.named) {
|
|
192
192
|
const parameterNames = createObjectOf(parameters.map(({name}) => normalise(name)).join(', '))
|
|
193
|
-
callableSignatures.push(
|
|
193
|
+
callableSignatures.push('// named',`${docStr}(${parameterNames}: ${parameterTypeAsObject}): ${returns}`)
|
|
194
194
|
}
|
|
195
195
|
if (callableSignatures.length === 0) throw new Error('At least one call style must be specified')
|
|
196
196
|
let prefix = name ? `${normalise(name)}: `: ''
|
|
197
197
|
if (prefix && isStatic) {
|
|
198
198
|
prefix = `static ${prefix}`
|
|
199
199
|
}
|
|
200
|
-
const kindDef = kind ?
|
|
200
|
+
const kindDef = kind ? [`kind: '${kind}'`] : []
|
|
201
201
|
const suffix = initialiser ? ` = ${initialiser}` : ''
|
|
202
|
-
|
|
203
|
-
return
|
|
202
|
+
|
|
203
|
+
return [
|
|
204
|
+
`${prefix} {`,
|
|
205
|
+
[...callableSignatures, '// metadata (do not use)', `__parameters: ${parameterTypeAsObject}, __returns: ${returns}`, ...kindDef],
|
|
206
|
+
`}${suffix}`,
|
|
207
|
+
]
|
|
204
208
|
}
|
|
205
209
|
|
|
206
210
|
/**
|
|
@@ -228,7 +232,8 @@ class SourceFile extends File {
|
|
|
228
232
|
addOperation(name, parameters, returns, kind, doc, callStyles) {
|
|
229
233
|
// this.operations.buffer.add(`// ${kind}`)
|
|
230
234
|
if (doc) this.operations.buffer.add(doc.join('\n')) // docs shows up on action provider side: `.on(action,...)`
|
|
231
|
-
|
|
235
|
+
const [opener, content, closer] = SourceFile.stringifyLambda({name, parameters, returns, kind, doc, callStyles})
|
|
236
|
+
this.operations.buffer.addIndentedBlock(`export declare const ${opener}`, content, closer)
|
|
232
237
|
this.operations.names.push(name)
|
|
233
238
|
}
|
|
234
239
|
|
|
@@ -395,7 +400,7 @@ class SourceFile extends File {
|
|
|
395
400
|
buffer.add(`import * as ${imp.asIdentifier()} from '${imp.asDirectory({relative: this.path.asDirectory()})}';`)
|
|
396
401
|
}
|
|
397
402
|
}
|
|
398
|
-
buffer.
|
|
403
|
+
buffer.blankLine()
|
|
399
404
|
return buffer
|
|
400
405
|
}
|
|
401
406
|
|
|
@@ -585,10 +590,27 @@ class Buffer {
|
|
|
585
590
|
|
|
586
591
|
/**
|
|
587
592
|
* Adds an element to the buffer with the current indentation level.
|
|
588
|
-
* @param {string} part - what to attach to the buffer
|
|
593
|
+
* @param {string | (() => string) | ((() => string) | string)[]} part - what to attach to the buffer
|
|
589
594
|
*/
|
|
590
595
|
add(part) {
|
|
591
|
-
|
|
596
|
+
if (typeof part === 'string') {
|
|
597
|
+
this.parts.push(this.currentIndent + part)
|
|
598
|
+
} else if (Array.isArray(part)) {
|
|
599
|
+
for (const p of part) {
|
|
600
|
+
this.add(p) // recurse to have proper indentation
|
|
601
|
+
}
|
|
602
|
+
} else if (typeof part === 'function') {
|
|
603
|
+
this.parts.push(part())
|
|
604
|
+
} else {
|
|
605
|
+
throw new Error(`trying to add something of type ${typeof part} to a Buffer`)
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
/**
|
|
610
|
+
* Adds a blank line to the buffer.
|
|
611
|
+
*/
|
|
612
|
+
blankLine() {
|
|
613
|
+
this.add('')
|
|
592
614
|
}
|
|
593
615
|
|
|
594
616
|
/**
|
package/lib/resolution/entity.js
CHANGED
|
@@ -19,6 +19,7 @@ const { configuration } = require('../config')
|
|
|
19
19
|
/** @typedef {import('../typedefs').resolver.CSN} CSN */
|
|
20
20
|
/** @typedef {import('../typedefs').resolver.EntityCSN} EntityCSN */
|
|
21
21
|
/** @typedef {import('../typedefs').resolver.TypeResolveInfo} TypeResolveInfo */
|
|
22
|
+
/** @typedef {import('../typedefs').resolver.TypeResolveOptions} TypeResolveOptions */
|
|
22
23
|
/** @typedef {import('../typedefs').visitor.Inflection} Inflection */
|
|
23
24
|
/** @typedef {{typeName: string, typeInfo: TypeResolveInfo & { inflection: Inflection }}} ResolveAndRequireInfo */
|
|
24
25
|
|
|
@@ -64,7 +65,7 @@ class Resolver {
|
|
|
64
65
|
* @returns {boolean} whether the type is configured to be optional
|
|
65
66
|
*/
|
|
66
67
|
isOptional(type) {
|
|
67
|
-
return !type.notNull
|
|
68
|
+
return type.items ? !type.items.notNull : !type.notNull
|
|
68
69
|
}
|
|
69
70
|
|
|
70
71
|
/**
|
|
@@ -233,7 +234,7 @@ class Resolver {
|
|
|
233
234
|
statementEnd: '',
|
|
234
235
|
modifiers: getPropertyModifiers(typeInfo.csn)
|
|
235
236
|
})
|
|
236
|
-
typeName = into.join(
|
|
237
|
+
typeName = into.join()
|
|
237
238
|
singular = typeName
|
|
238
239
|
plural = createArrayOf(typeName)
|
|
239
240
|
} else {
|
|
@@ -276,10 +277,11 @@ class Resolver {
|
|
|
276
277
|
* 3. return a properly prefixed name to use within model2.d.ts, e.g. "m1.Foo"
|
|
277
278
|
* @param {import('../visitor').EntityCSN} element - the CSN element to resolve the type for.
|
|
278
279
|
* @param {SourceFile} file - source file for context.
|
|
280
|
+
* @param {TypeResolveOptions} [options] - resolver options
|
|
279
281
|
* @returns {ResolveAndRequireInfo} info about the resolved type
|
|
280
282
|
*/
|
|
281
|
-
resolveAndRequire(element, file) {
|
|
282
|
-
const typeInfo = this.resolveType(element, file)
|
|
283
|
+
resolveAndRequire(element, file, options) {
|
|
284
|
+
const typeInfo = this.resolveType(element, file, options)
|
|
283
285
|
const cardinality = getMaxCardinality(element)
|
|
284
286
|
|
|
285
287
|
let typeName = typeInfo.plainName ?? typeInfo.type
|
|
@@ -434,9 +436,10 @@ class Resolver {
|
|
|
434
436
|
* Enriched with additional information for improved printout (see return type).
|
|
435
437
|
* @param {import('../typedefs').resolver.EntityCSN | TypeResolveInfo} element - the CSN element to resolve the type for.
|
|
436
438
|
* @param {SourceFile} file - source file for context.
|
|
439
|
+
* @param {TypeResolveOptions} [options] - resolver options
|
|
437
440
|
* @returns {TypeResolveInfo} description of the resolved type
|
|
438
441
|
*/
|
|
439
|
-
resolveType(element, file) {
|
|
442
|
+
resolveType(element, file, options) {
|
|
440
443
|
// while resolving inline declarations, it can happen that we land here
|
|
441
444
|
// with an already resolved type. In that case, just return the type we have.
|
|
442
445
|
// type guard check purely to satisfy return statement
|
|
@@ -505,9 +508,10 @@ class Resolver {
|
|
|
505
508
|
result.isBuiltin = true
|
|
506
509
|
this.resolveType(element.items, file)
|
|
507
510
|
//delete element.items
|
|
508
|
-
} else if (element?.elements && !element?.type) {
|
|
511
|
+
} else if (element?.elements && (options?.forceInlineStructs || !element?.type)) {
|
|
509
512
|
// explicitly skip named type definitions, which have elements too, but should not be considered inline declarations
|
|
510
|
-
|
|
513
|
+
// if the resolver option `forceInlineStructs` is `true`, named types in elements will be converted to inline
|
|
514
|
+
this.#resolveInlineDeclarationType(element.elements, result, file, options)
|
|
511
515
|
}
|
|
512
516
|
|
|
513
517
|
if (result.isBuiltin === false && result.isInlineDeclaration === false && !result.plainName) {
|
|
@@ -529,11 +533,12 @@ class Resolver {
|
|
|
529
533
|
* @param {{ [key: string]: EntityCSN }} items - the properties of the inline declaration.
|
|
530
534
|
* @param {TypeResolveInfo} into - @see resolveType()
|
|
531
535
|
* @param {SourceFile} relativeTo - the sourcefile in which we have found the reference to the type.
|
|
536
|
+
* @param {TypeResolveOptions} [options] - resolver options
|
|
532
537
|
* This is important to correctly detect when a field in the inline declaration is referencing
|
|
533
538
|
* types from the CWD. In that case, we will not add an import for that type and not add a namespace-prefix.
|
|
534
539
|
*/
|
|
535
|
-
#resolveInlineDeclarationType(items, into, relativeTo) {
|
|
536
|
-
return this.visitor.inlineDeclarationResolver.resolveInlineDeclaration(items, into, relativeTo)
|
|
540
|
+
#resolveInlineDeclarationType(items, into, relativeTo, options) {
|
|
541
|
+
return this.visitor.inlineDeclarationResolver.resolveInlineDeclaration(items, into, relativeTo, options)
|
|
537
542
|
}
|
|
538
543
|
|
|
539
544
|
/**
|
package/lib/typedefs.d.ts
CHANGED
|
@@ -82,6 +82,39 @@ export module resolver {
|
|
|
82
82
|
inflection?: visitor.Inflection
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
+
/**
|
|
86
|
+
* Custom options to be used during type resolvement
|
|
87
|
+
*/
|
|
88
|
+
export type TypeResolveOptions = {
|
|
89
|
+
/**
|
|
90
|
+
* Entity elements that have a custom type are not available when entity is accessed using CQL.
|
|
91
|
+
*
|
|
92
|
+
* They only exist in the original defined form in the CSN and LinkedCSN but not in the compiled
|
|
93
|
+
* OData or SQL models (i.e. `cds.compile(..).for.odata()`).
|
|
94
|
+
*
|
|
95
|
+
* Therefore they need to be flattened down like inline structs.
|
|
96
|
+
*
|
|
97
|
+
* ```cds
|
|
98
|
+
* // model.cds
|
|
99
|
+
* type Adress {
|
|
100
|
+
* street: String;
|
|
101
|
+
* zipCode: String;
|
|
102
|
+
* }
|
|
103
|
+
* entity Persons {
|
|
104
|
+
* title: String
|
|
105
|
+
* address: Adress
|
|
106
|
+
* }
|
|
107
|
+
* ```
|
|
108
|
+
*
|
|
109
|
+
* // service.js
|
|
110
|
+
* ```js
|
|
111
|
+
* const {title, address_street, address_zipCode} = await SELECT.from(Persons);
|
|
112
|
+
* ```
|
|
113
|
+
*
|
|
114
|
+
*/
|
|
115
|
+
forceInlineStructs?: boolean
|
|
116
|
+
}
|
|
117
|
+
|
|
85
118
|
export type EntityInfo = Exclude<ReturnType<import('../lib/resolution/entity').EntityRepository['getByFq']>, null>
|
|
86
119
|
|
|
87
120
|
// TODO: this will be completely replaced by EntityInfo
|
package/lib/visitor.js
CHANGED
|
@@ -2,13 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
const util = require('./util')
|
|
4
4
|
|
|
5
|
-
const {
|
|
5
|
+
const { isView, isUnresolved, propagateForeignKeys, collectDraftEnabledEntities, isDraftEnabled, isType, isProjection, getMaxCardinality, isViewOrProjection, isEnum, isEntity } = require('./csn')
|
|
6
6
|
// eslint-disable-next-line no-unused-vars
|
|
7
7
|
const { SourceFile, FileRepository, Buffer, Path } = require('./file')
|
|
8
8
|
const { FlatInlineDeclarationResolver, StructuredInlineDeclarationResolver } = require('./components/inline')
|
|
9
9
|
const { Resolver } = require('./resolution/resolver')
|
|
10
10
|
const { LOG } = require('./logging')
|
|
11
|
-
const { docify, createPromiseOf, createUnionOf, createKeysOf } = require('./components/wrappers')
|
|
11
|
+
const { docify, createPromiseOf, createUnionOf, createKeysOf, createElementsOf, stringIdent, createDraftsOf, createDraftOf } = require('./components/wrappers')
|
|
12
12
|
const { csnToEnumPairs, propertyToInlineEnumName, isInlineEnumType, stringifyEnumType } = require('./components/enum')
|
|
13
13
|
const { isReferenceType } = require('./components/reference')
|
|
14
14
|
const { empty } = require('./components/typescript')
|
|
@@ -17,11 +17,13 @@ const { EntityRepository, asIdentifier } = require('./resolution/entity')
|
|
|
17
17
|
const { last } = require('./components/identifier')
|
|
18
18
|
const { getPropertyModifiers } = require('./components/property')
|
|
19
19
|
const { configuration } = require('./config')
|
|
20
|
+
const { createMember } = require('./components/class')
|
|
20
21
|
|
|
21
22
|
/** @typedef {import('./file').File} File */
|
|
22
23
|
/** @typedef {import('./typedefs').visitor.Context} Context */
|
|
23
24
|
/** @typedef {import('./typedefs').visitor.Inflection} Inflection */
|
|
24
25
|
/** @typedef {import('./typedefs').resolver.CSN} CSN */
|
|
26
|
+
/** @typedef {import('./typedefs').resolver.TypeResolveOptions} TypeResolveOptions */
|
|
25
27
|
/** @typedef {import('./typedefs').resolver.EntityCSN} EntityCSN */
|
|
26
28
|
/** @typedef {import('./typedefs').resolver.EnumCSN} EnumCSN */
|
|
27
29
|
|
|
@@ -40,8 +42,10 @@ class Visitor {
|
|
|
40
42
|
* @param {{xtended: CSN, inferred: CSN}} csn - root CSN
|
|
41
43
|
*/
|
|
42
44
|
constructor(csn) {
|
|
43
|
-
|
|
45
|
+
propagateForeignKeys(csn.xtended)
|
|
44
46
|
propagateForeignKeys(csn.inferred)
|
|
47
|
+
// has to be executed on the inferred model as autoexposed entities are not included in the xtended csn
|
|
48
|
+
collectDraftEnabledEntities(csn.inferred)
|
|
45
49
|
this.csn = csn
|
|
46
50
|
|
|
47
51
|
/** @type {Context[]} **/
|
|
@@ -139,18 +143,27 @@ class Visitor {
|
|
|
139
143
|
: ''
|
|
140
144
|
if (actions.length) {
|
|
141
145
|
buffer.addIndentedBlock(`declare static readonly actions: ${inherited}{`,
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
146
|
+
() => {
|
|
147
|
+
for (const [aname, action] of actions) {
|
|
148
|
+
const [opener, content, closer] = SourceFile.stringifyLambda({
|
|
149
|
+
name: aname,
|
|
150
|
+
parameters: this.#stringifyFunctionParams(action.params, file),
|
|
151
|
+
returns: action.returns
|
|
152
|
+
? this.resolver.resolveAndRequire(action.returns, file).typeName
|
|
153
|
+
: 'any',
|
|
154
|
+
kind: action.kind,
|
|
155
|
+
doc: docify(action.doc)})
|
|
156
|
+
buffer.addIndentedBlock(opener, content, closer)
|
|
157
|
+
}
|
|
158
|
+
}, '}'
|
|
151
159
|
) // end of actions
|
|
152
160
|
} else {
|
|
153
|
-
buffer.add(
|
|
161
|
+
buffer.add(createMember({
|
|
162
|
+
name: 'actions',
|
|
163
|
+
type: `${inherited}${empty}`,
|
|
164
|
+
isStatic: true,
|
|
165
|
+
isReadonly: true
|
|
166
|
+
}))
|
|
154
167
|
}
|
|
155
168
|
}
|
|
156
169
|
|
|
@@ -159,7 +172,27 @@ class Visitor {
|
|
|
159
172
|
* @param {string} clean - the clean name of the entity
|
|
160
173
|
*/
|
|
161
174
|
#printStaticKeys(buffer, clean) {
|
|
162
|
-
buffer.add(
|
|
175
|
+
buffer.add(createMember({
|
|
176
|
+
name: 'keys',
|
|
177
|
+
type: createKeysOf(clean),
|
|
178
|
+
isDeclare: true,
|
|
179
|
+
isStatic: true,
|
|
180
|
+
isReadonly: true,
|
|
181
|
+
}))
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* @param {Buffer} buffer - the buffer to write the elements into
|
|
186
|
+
* @param {string} clean - the clean name of the entity
|
|
187
|
+
*/
|
|
188
|
+
#printStaticElements(buffer, clean) {
|
|
189
|
+
buffer.add(createMember({
|
|
190
|
+
name: 'elements',
|
|
191
|
+
type: createElementsOf(clean),
|
|
192
|
+
isDeclare: true,
|
|
193
|
+
isStatic: true,
|
|
194
|
+
isReadonly: true
|
|
195
|
+
}))
|
|
163
196
|
}
|
|
164
197
|
|
|
165
198
|
/**
|
|
@@ -229,12 +262,15 @@ class Visitor {
|
|
|
229
262
|
buffer.addIndentedBlock(`return class ${clean} extends ${ancestorsAspects} {`, () => {
|
|
230
263
|
/** @type {import('./typedefs').resolver.EnumCSN[]} */
|
|
231
264
|
const enums = []
|
|
232
|
-
|
|
265
|
+
/** @type {TypeResolveOptions} */
|
|
266
|
+
const resolverOptions = { forceInlineStructs: isEntity(entity) && configuration.inlineDeclarations === 'flat'}
|
|
267
|
+
|
|
268
|
+
for (let [ename, element] of Object.entries(entity.elements ?? [])) {
|
|
233
269
|
if (element.target && /\.texts?/.test(element.target)) {
|
|
234
270
|
LOG.warn(`referring to .texts property in ${fq}. This is currently not supported and will be ignored.`)
|
|
235
271
|
continue
|
|
236
272
|
}
|
|
237
|
-
this.visitElement(ename, element, file, buffer)
|
|
273
|
+
this.visitElement({name: ename, element, file, buffer, resolverOptions})
|
|
238
274
|
|
|
239
275
|
// make foreign keys explicit
|
|
240
276
|
if (element.target) {
|
|
@@ -250,7 +286,7 @@ class Visitor {
|
|
|
250
286
|
const kelement = Object.assign(Object.create(originalKeyElement), {
|
|
251
287
|
isRefNotNull: !!element.notNull || !!element.key
|
|
252
288
|
})
|
|
253
|
-
this.visitElement(foreignKey, kelement, file, buffer)
|
|
289
|
+
this.visitElement({name: foreignKey, element: kelement, file, buffer, resolverOptions})
|
|
254
290
|
}
|
|
255
291
|
}
|
|
256
292
|
}
|
|
@@ -262,37 +298,49 @@ class Visitor {
|
|
|
262
298
|
}
|
|
263
299
|
}
|
|
264
300
|
|
|
265
|
-
|
|
266
|
-
|
|
301
|
+
for (const e of enums) {
|
|
302
|
+
const eDoc = docify(e.doc)
|
|
303
|
+
buffer.add(eDoc)
|
|
304
|
+
buffer.add(createMember({
|
|
305
|
+
name: e.name,
|
|
306
|
+
initialiser: propertyToInlineEnumName(clean, e.name),
|
|
307
|
+
isStatic: true,
|
|
308
|
+
}))
|
|
309
|
+
file.addInlineEnum(clean, fq, e.name, csnToEnumPairs(e, {unwrapVals: true}), eDoc)
|
|
267
310
|
}
|
|
268
311
|
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
}
|
|
312
|
+
if ('kind' in entity) {
|
|
313
|
+
buffer.add(createMember({
|
|
314
|
+
name: 'kind',
|
|
315
|
+
type: '"entity" | "type" | "aspect"',
|
|
316
|
+
isStatic: true,
|
|
317
|
+
isReadonly: true,
|
|
318
|
+
isDeclare: false,
|
|
319
|
+
isOverride: ancestorInfos.some(ancestor => ancestor.csn.kind),
|
|
320
|
+
initialiser: stringIdent(entity.kind)
|
|
321
|
+
}))
|
|
322
|
+
}
|
|
323
|
+
this.#printStaticKeys(buffer, clean)
|
|
324
|
+
this.#printStaticElements(buffer, clean)
|
|
325
|
+
this.#printStaticActions(entity, buffer, ancestorInfos, file)
|
|
280
326
|
}, '};') // end of generated class
|
|
281
327
|
}, '}') // end of aspect
|
|
282
328
|
|
|
283
329
|
// CLASS WITH ADDED ASPECTS
|
|
284
330
|
file.addImport(baseDefinitions.path)
|
|
285
331
|
docify(entity.doc).forEach(d => { buffer.add(d) })
|
|
286
|
-
buffer.add(`export class ${identSingular(clean)} extends ${identAspect(toLocalIdent({clean, fq}))}(${baseDefinitions.path.asIdentifier()}.Entity) {${this.#staticClassContents(
|
|
332
|
+
buffer.add(`export class ${identSingular(clean)} extends ${identAspect(toLocalIdent({clean, fq}))}(${baseDefinitions.path.asIdentifier()}.Entity) {${this.#staticClassContents(fq, clean).join('\n')}}`)
|
|
287
333
|
this.contexts.pop()
|
|
288
334
|
}
|
|
289
335
|
|
|
290
336
|
/**
|
|
337
|
+
* @param {string} fq - fully qualified name of the entity
|
|
291
338
|
* @param {string} clean - the clean name of the entity
|
|
292
|
-
* @param {
|
|
339
|
+
* @param {boolean} [isPlural] - `true` if passed entity is plural
|
|
293
340
|
*/
|
|
294
|
-
#staticClassContents(clean,
|
|
295
|
-
|
|
341
|
+
#staticClassContents(fq, clean, isPlural = false) {
|
|
342
|
+
if (!isDraftEnabled(fq)) return []
|
|
343
|
+
return [`static drafts: ${isPlural ? createDraftsOf(clean) : createDraftOf(clean)}`]
|
|
296
344
|
}
|
|
297
345
|
|
|
298
346
|
/**
|
|
@@ -348,9 +396,6 @@ class Visitor {
|
|
|
348
396
|
? this.csn.inferred.definitions[fq]
|
|
349
397
|
: entity
|
|
350
398
|
|
|
351
|
-
// draft enablement is stored in csn.xtended. Iff we took the entity from csn.inferred, we have to carry the draft-enablement over at this point
|
|
352
|
-
target['@odata.draft.enabled'] = isDraftEnabled(entity)
|
|
353
|
-
|
|
354
399
|
this.#aspectify(fq, target, buffer, { cleanName: singular })
|
|
355
400
|
|
|
356
401
|
buffer.add(overrideNameProperty(singular, entity.name))
|
|
@@ -366,13 +411,13 @@ class Visitor {
|
|
|
366
411
|
}
|
|
367
412
|
// plural can not be a type alias to $singular[] but needs to be a proper class instead,
|
|
368
413
|
// so it can get passed as value to CQL functions.
|
|
369
|
-
const additionalProperties = this.#staticClassContents(singular,
|
|
414
|
+
const additionalProperties = this.#staticClassContents(fq, singular, true)
|
|
370
415
|
additionalProperties.push('$count?: number')
|
|
371
|
-
docify(entity.doc)
|
|
416
|
+
buffer.add(docify(entity.doc))
|
|
372
417
|
buffer.add(`export class ${plural} extends Array<${singular}> {${additionalProperties.join('\n')}}`)
|
|
373
418
|
buffer.add(overrideNameProperty(plural, entity.name))
|
|
374
419
|
}
|
|
375
|
-
buffer.
|
|
420
|
+
buffer.blankLine()
|
|
376
421
|
}
|
|
377
422
|
|
|
378
423
|
/**
|
|
@@ -496,7 +541,7 @@ class Visitor {
|
|
|
496
541
|
// FIXME: shouldn't need to change config here! Idea: init Visitor with .options fed from config, then manipulate that
|
|
497
542
|
configuration.propertiesOptional = false
|
|
498
543
|
for (const [ename, element] of Object.entries(event.elements ?? {})) {
|
|
499
|
-
this.visitElement(ename, element, file, buffer)
|
|
544
|
+
this.visitElement({name: ename, element, file, buffer})
|
|
500
545
|
}
|
|
501
546
|
configuration.propertiesOptional = propOpt
|
|
502
547
|
}, '}')
|
|
@@ -517,12 +562,12 @@ class Visitor {
|
|
|
517
562
|
// file.addImport(new Path(['cds'], '')) TODO make sap/cds import work
|
|
518
563
|
buffer.addIndentedBlock(`export class ${serviceNameSimple} extends cds.Service {`, () => {
|
|
519
564
|
Object.entries(service.operations ?? {}).forEach(([name, {doc}]) => {
|
|
520
|
-
|
|
565
|
+
buffer.add(docify(doc))
|
|
521
566
|
buffer.add(`declare ${name}: typeof ${name}`)
|
|
522
567
|
})
|
|
523
568
|
}, '}')
|
|
524
569
|
buffer.add(`export default ${serviceNameSimple}`)
|
|
525
|
-
buffer.
|
|
570
|
+
buffer.blankLine()
|
|
526
571
|
file.addService(service.name)
|
|
527
572
|
}
|
|
528
573
|
|
|
@@ -585,13 +630,15 @@ class Visitor {
|
|
|
585
630
|
|
|
586
631
|
/**
|
|
587
632
|
* Visits a single element in an entity.
|
|
588
|
-
* @param {
|
|
589
|
-
* @param {
|
|
590
|
-
* @param {
|
|
591
|
-
* @param {
|
|
633
|
+
* @param {object} options - options
|
|
634
|
+
* @param {string} options.name - name of the element
|
|
635
|
+
* @param {EntityCSN} options.element - CSN data belonging to the the element.
|
|
636
|
+
* @param {SourceFile} options.file - the namespace file the surrounding entity is being printed into.
|
|
637
|
+
* @param {Buffer} [options.buffer] - buffer to add the definition to. If no buffer is passed, the passed file's class buffer is used instead.
|
|
638
|
+
* @param {TypeResolveOptions} [options.resolverOptions] - custom type resolver options
|
|
592
639
|
* @returns @see InlineDeclarationResolver.visitElement
|
|
593
640
|
*/
|
|
594
|
-
visitElement(name, element, file, buffer = file.classes) {
|
|
641
|
+
visitElement({name, element, file, buffer = file.classes, resolverOptions}) {
|
|
595
642
|
return this.inlineDeclarationResolver.visitElement({
|
|
596
643
|
name,
|
|
597
644
|
element,
|
|
@@ -599,7 +646,8 @@ class Visitor {
|
|
|
599
646
|
buffer,
|
|
600
647
|
// we explicitly pass the "declare" modifier here to avoid problems with noImplicitOverride and useDefineForClassFields in strict tsconfigs
|
|
601
648
|
// but not inside type defs (e.g. parameter types) where this would be a syntax error
|
|
602
|
-
modifiers: getPropertyModifiers(element)
|
|
649
|
+
modifiers: getPropertyModifiers(element),
|
|
650
|
+
resolverOptions
|
|
603
651
|
})
|
|
604
652
|
}
|
|
605
653
|
}
|
package/library/cds.hana.ts
CHANGED
|
@@ -9,7 +9,7 @@ export class VARCHAR extends String {};
|
|
|
9
9
|
export class CLOB extends String {};
|
|
10
10
|
export class BINARY extends String {}
|
|
11
11
|
export class ST_POINT {
|
|
12
|
-
public x: number;
|
|
13
|
-
public y: number;
|
|
12
|
+
declare public x: number;
|
|
13
|
+
declare public y: number;
|
|
14
14
|
}
|
|
15
15
|
export class ST_GEOMETRY { /* FIXME */ }
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cap-js/cds-typer",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.28.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",
|
|
@@ -25,7 +25,8 @@
|
|
|
25
25
|
"doc:prepare": "npm run doc:clean && mkdir -p doc/types",
|
|
26
26
|
"doc:typegen": "./node_modules/.bin/tsc ./lib/*.js --skipLibCheck --declaration --allowJs --emitDeclarationOnly --outDir doc/types && cd doc/types && tsc --init",
|
|
27
27
|
"doc:cli": "npm run cli -- --help > ./doc/cli.txt",
|
|
28
|
-
"jsdoc:check": "tsc --noEmit --project jsconfig.json"
|
|
28
|
+
"jsdoc:check": "tsc --noEmit --project jsconfig.json",
|
|
29
|
+
"write:cds-typer-shema": "node scripts/write-cds-typer-schema.js"
|
|
29
30
|
},
|
|
30
31
|
"files": [
|
|
31
32
|
"lib/",
|
|
@@ -60,5 +61,72 @@
|
|
|
60
61
|
"test/smoke.jest.config.js",
|
|
61
62
|
"test/unit.jest.config.js"
|
|
62
63
|
]
|
|
64
|
+
},
|
|
65
|
+
"cds": {
|
|
66
|
+
"schema": {
|
|
67
|
+
"buildTaskType": {
|
|
68
|
+
"name": "typescript",
|
|
69
|
+
"description": "TypeScript build plugin. For use after the nodejs build task."
|
|
70
|
+
},
|
|
71
|
+
"cds": {
|
|
72
|
+
"typer": {
|
|
73
|
+
"type": "object",
|
|
74
|
+
"description": "Configuration for CDS Typer",
|
|
75
|
+
"properties": {
|
|
76
|
+
"output_directory": {
|
|
77
|
+
"type": "string",
|
|
78
|
+
"description": "Root directory to write the generated files to.",
|
|
79
|
+
"default": "@cds-models"
|
|
80
|
+
},
|
|
81
|
+
"log_level": {
|
|
82
|
+
"type": "string",
|
|
83
|
+
"description": "Minimum log level that is printed.\nThe default is only used if no explicit value is passed\nand there is no configuration passed via cds.env either.",
|
|
84
|
+
"enum": [
|
|
85
|
+
"SILENT",
|
|
86
|
+
"ERROR",
|
|
87
|
+
"WARN",
|
|
88
|
+
"INFO",
|
|
89
|
+
"DEBUG",
|
|
90
|
+
"TRACE",
|
|
91
|
+
"SILLY",
|
|
92
|
+
"VERBOSE",
|
|
93
|
+
"WARNING",
|
|
94
|
+
"CRITICAL",
|
|
95
|
+
"NONE"
|
|
96
|
+
],
|
|
97
|
+
"default": "ERROR"
|
|
98
|
+
},
|
|
99
|
+
"js_config_path": {
|
|
100
|
+
"type": "string",
|
|
101
|
+
"description": "Path to where the jsconfig.json should be written.\nIf specified, cds-typer will create a jsconfig.json file and\nset it up to restrict property usage in types entities to\nexisting properties only."
|
|
102
|
+
},
|
|
103
|
+
"use_entities_proxy": {
|
|
104
|
+
"type": "boolean",
|
|
105
|
+
"description": "If set to true the 'cds.entities' exports in the generated 'index.js'\nfiles will be wrapped in 'Proxy' objects\nso static import/require calls can be used everywhere.\n\nWARNING: entity properties can still only be accessed after\n'cds.entities' has been loaded",
|
|
106
|
+
"default": false
|
|
107
|
+
},
|
|
108
|
+
"inline_declarations": {
|
|
109
|
+
"type": "string",
|
|
110
|
+
"description": "Whether to resolve inline type declarations\nflat: (x_a, x_b, ...)\nor structured: (x: {a, b}).",
|
|
111
|
+
"enum": [
|
|
112
|
+
"flat",
|
|
113
|
+
"structured"
|
|
114
|
+
],
|
|
115
|
+
"default": "structured"
|
|
116
|
+
},
|
|
117
|
+
"properties_optional": {
|
|
118
|
+
"type": "boolean",
|
|
119
|
+
"description": "If set to true, properties in entities are\nalways generated as optional (a?: T).",
|
|
120
|
+
"default": true
|
|
121
|
+
},
|
|
122
|
+
"ieee754compatible": {
|
|
123
|
+
"type": "boolean",
|
|
124
|
+
"description": "If set to true, floating point properties are generated\nas IEEE754 compatible '(number | string)' instead of 'number'.",
|
|
125
|
+
"default": false
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
63
131
|
}
|
|
64
132
|
}
|