@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 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.28.0 - TBD
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
- - Fixa bug where cds-typer would produce redundant type declarations when the model contains an associations to another entity's property
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
+ }
@@ -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) doc.forEach(d => { buffer.add(d) })
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.add('')
63
+ buffer.blankLine()
64
64
  }
65
65
 
66
66
  /**
@@ -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
- into.structuredType[subname] = this.visitor.visitElement(subname, se, dummy)
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
- return type.typeInfo.isNotNull ? typeName : `${typeName} | null`
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
- sub.currentIndent = buffer.currentIndent
274
- buffer.add(this.flatten({fq, type, buffer: sub, modifiers, statementEnd}).join())
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
@@ -34,7 +34,8 @@ class Config {
34
34
  static #defaults = {
35
35
  propertiesOptional: true,
36
36
  useEntitiesProxy: false,
37
- inlineDeclarations: 'flat'
37
+ inlineDeclarations: 'flat',
38
+ outputDirectory: '.'
38
39
  }
39
40
 
40
41
  values = undefined
package/lib/csn.js CHANGED
@@ -1,4 +1,8 @@
1
- const annotation = '@odata.draft.enabled'
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 {EntityCSN} entity - the entity
47
+ * @param {string} fq - the fqn of an entity
44
48
  */
45
- const isDraftEnabled = entity => entity['@odata.draft.enabled'] === true
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 DraftUnroller {
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
- #entities = []
96
+ #draftRoots = []
97
+ /** @type {string[]} */
98
+ #serviceNames = []
99
99
  /** @type {CSN | undefined} */
100
100
  #csn
101
- set csn(c) {
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
- * @param {EntityCSN | string} entityOrFq - entity to set draftable annotation for.
117
- * @param {boolean} value - whether the entity is draftable.
104
+ * @returns {string[]}
118
105
  */
119
- #setDraftable(entityOrFq, value) {
120
- const entity = typeof entityOrFq === 'string'
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
- * @param {EntityCSN | string} entityOrFq - entity to look draftability up for.
135
- * @returns {boolean}
111
+ * @returns {EntityCSN[]}
136
112
  */
137
- #getDraftable(entityOrFq) {
138
- const entity = typeof entityOrFq === 'string'
139
- ? this.#getDefinition(entityOrFq)
140
- : entityOrFq
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
- * FIXME: could use EntityRepository here
149
- * @param {string} name - name of the entity.
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
- #propagateInheritance(entity) {
162
- if (!entity) return
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
- * Propagate draft-enablement through projections.
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
- #propagateProjections() {
173
- /**
174
- * @param {string} from - entity to propagate draftability from.
175
- * @param {string} to - entity to propagate draftability to.
176
- */
177
- const propagate = (from, to) => {
178
- do {
179
- this.#setDraftable(to, this.#getDraftable(to) || this.#getDraftable(from))
180
- from = to
181
- to = this.#projections[to]
182
- } while (to)
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
- for (let [projection, target] of Object.entries(this.#projections)) {
186
- propagate(projection, target)
187
- propagate(target, projection)
160
+ if (!this.#isDraftEnabled(draftEntity) && !draftEntities[draftEntityName]) {
161
+ this.#collectDraftEntitiesInto(draftEntity, draftEntityName, rootEntity, draftEntities)
162
+ }
188
163
  }
189
164
  }
190
165
 
191
166
  /**
192
- * If an entity E is draftable and contains any composition of entities,
193
- * then those entities also become draftable. Recursively.
194
- * @param {EntityCSN} entity - entity to propagate all compositions from.
167
+ * @param {EntityCSN} entity - entity to check
168
+ * @returns {boolean}
195
169
  */
196
- #propagateCompositions(entity) {
197
- if (!this.#getDraftable(entity)) return
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
- unroll(csn) {
211
- this.csn = csn
175
+ run(csn) {
176
+ if (!csn) return
212
177
 
213
- // inheritance
214
- for (const entity of this.#entities) {
215
- this.#propagateInheritance(entity)
216
- }
178
+ this.#csn = csn
179
+ this.#serviceNames = this.#getServiceNames()
180
+ this.#draftRoots = this.#collectDraftRoots()
217
181
 
218
- // transitivity through compositions
219
- // we have to do this in a second pass, as we only now know which entities are draft-enables themselves
220
- for (const entity of this.#entities) {
221
- this.#propagateCompositions(entity)
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
- this.#propagateProjections()
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 are unrolling the @odata.draft.enabled annotations into related entities manually.
231
- * This includes three scenarios:
204
+ * We collect all entities that are draft enabled.
205
+ * (@see `@sap/cds-compiler/lib/transform/draft/db.js#generateDraft`)
232
206
  *
233
- * (a) aspects via `A: B`, where `B` is draft enabled.
234
- * Note that when an entity extends two other entities of which one has drafts enabled and
235
- * one has not, then the one that is later in the list of mixins "wins":
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
- * ```ts
239
- * @odata.draft.enabled true
240
- * entity T {}
241
- * @odata.draft.enabled false
242
- * entity F {}
243
- * entity A: T,F {} // draft not enabled
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
- * (b) Draft enabled projections make the entity we project on draft enabled.
248
- * @example
249
- * ```ts
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
- * (c) Entities that are draft enabled propagate this property down through compositions:
256
- *
257
- * ```ts
258
- * @odata.draft.enabled: true
259
- * entity A {
260
- * b: Composition of B
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 unrollDraftability(csn) {
266
- new DraftUnroller().unroll(csn)
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
- amendCSN,
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(`// positional\n${docStr}(${paramTypesPositional}): ${returns}`) // docs shows up on action consumer side: `.action(...)`
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(`// named\n${docStr}(${parameterNames}: ${parameterTypeAsObject}): ${returns}`)
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 ? `, kind: '${kind}'` : ''
200
+ const kindDef = kind ? [`kind: '${kind}'`] : []
201
201
  const suffix = initialiser ? ` = ${initialiser}` : ''
202
- const lambda = `{\n${callableSignatures.join('\n')}, \n// metadata (do not use)\n__parameters: ${parameterTypeAsObject}, __returns: ${returns}${kindDef}}`
203
- return prefix + lambda + suffix
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
- this.operations.buffer.add(`export declare const ${SourceFile.stringifyLambda({name, parameters, returns, kind, doc, callStyles})};`)
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.add('') // empty line after imports
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
- this.parts.push(this.currentIndent + part)
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
  /**
@@ -61,6 +61,7 @@ class EntityInfo {
61
61
  /** @type {import('../typedefs').resolver.EntityCSN | undefined} */
62
62
  #csn
63
63
 
64
+ /** @returns the **inferred** csn for this entity. */
64
65
  get csn () {
65
66
  return this.#csn ??= this.#resolver.csn.definitions[this.fullyQualifiedName]
66
67
  }
@@ -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
- this.#resolveInlineDeclarationType(element.elements, result, file)
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 { amendCSN, isView, isUnresolved, propagateForeignKeys, isDraftEnabled, isType, isProjection, getMaxCardinality, isViewOrProjection, isEnum } = require('./csn')
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
- amendCSN(csn.xtended)
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
- actions.map(([aname, action]) => SourceFile.stringifyLambda({
143
- name: aname,
144
- parameters: this.#stringifyFunctionParams(action.params, file),
145
- returns: action.returns
146
- ? this.resolver.resolveAndRequire(action.returns, file).typeName
147
- : 'any',
148
- kind: action.kind,
149
- doc: docify(action.doc)
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(`declare static readonly actions: ${inherited}${empty}`)
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(`declare static readonly keys: ${createKeysOf(clean)}`)
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
- for (let [ename, element] of Object.entries(entity.elements ?? {})) {
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
- if ('kind' in entity) {
266
- buffer.addIndented([`static readonly kind: 'entity' | 'type' | 'aspect' = '${entity.kind}';`])
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
- buffer.addIndented(() => {
270
- for (const e of enums) {
271
- const eDoc = docify(e.doc)
272
- eDoc.forEach(d => { buffer.add(d) })
273
- buffer.add(`static ${e.name} = ${propertyToInlineEnumName(clean, e.name)}`)
274
- file.addInlineEnum(clean, fq, e.name, csnToEnumPairs(e, {unwrapVals: true}), eDoc)
275
- }
276
- this.#printStaticActions(entity, buffer, ancestorInfos, file)
277
- this.#printStaticKeys(buffer, clean)
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(clean, entity).join('\n')}}`)
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 {EntityCSN} entity - the entity to generate the static contents for
339
+ * @param {boolean} [isPlural] - `true` if passed entity is plural
293
340
  */
294
- #staticClassContents(clean, entity) {
295
- return isDraftEnabled(entity) ? [`static drafts: typeof ${clean}`] : []
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, entity)
414
+ const additionalProperties = this.#staticClassContents(fq, singular, true)
370
415
  additionalProperties.push('$count?: number')
371
- docify(entity.doc).forEach(d => { buffer.add(d) })
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.add('')
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
- docify(doc).forEach(d => { buffer.add(d) })
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.add('')
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 {string} name - name of the element
589
- * @param {EntityCSN} element - CSN data belonging to the the element.
590
- * @param {SourceFile} file - the namespace file the surrounding entity is being printed into.
591
- * @param {Buffer} [buffer] - buffer to add the definition to. If no buffer is passed, the passed file's class buffer is used instead.
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
  }
@@ -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.27.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
  }