@cap-js/cds-typer 0.26.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.
@@ -3,6 +3,41 @@
3
3
  // this was derived from baseDefinitions before, but caused a circular dependency
4
4
  const base = '__'
5
5
 
6
+ /**
7
+ * Wraps type into the Key type.
8
+ * @param {string} t - the type name.
9
+ * @returns {string}
10
+ */
11
+ const createKey = t => `${base}.Key<${t}>`
12
+
13
+ /**
14
+ * Wraps type into KeysOf type.
15
+ * @param {string} t - the type name.
16
+ * @returns {string}
17
+ */
18
+ const createKeysOf = t => `${base}.KeysOf<${t}>`
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
+
6
41
  /**
7
42
  * Wraps type into association to scalar.
8
43
  * @param {string} t - the singular type name.
@@ -84,8 +119,20 @@ const docify = doc => {
84
119
  return ['/**'].concat(lines.map(line => `* ${line}`)).concat(['*/'])
85
120
  }
86
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
+
87
129
  module.exports = {
88
130
  createArrayOf,
131
+ createDraftOf,
132
+ createDraftsOf,
133
+ createKey,
134
+ createKeysOf,
135
+ createElementsOf,
89
136
  createObjectOf,
90
137
  createPromiseOf,
91
138
  createUnionOf,
@@ -94,5 +141,6 @@ module.exports = {
94
141
  createCompositionOfOne,
95
142
  createCompositionOfMany,
96
143
  deepRequire,
97
- docify
144
+ docify,
145
+ stringIdent
98
146
  }
package/lib/config.js ADDED
@@ -0,0 +1,118 @@
1
+ const cds = require('@sap/cds')
2
+ const { camelToSnake } = require('./util')
3
+
4
+ /**
5
+ * Makes properties of an object accessible in both camelCase and snake_case.
6
+ * Snake_case gets precedence over camelCase.
7
+ * @template T
8
+ * @param {T} target - The object to proxy.
9
+ * @returns {T} - The proxied object.
10
+ */
11
+ const camelSnakeHybrid = target => {
12
+ // @ts-expect-error - expecting target to be of type {}, which is not T (same for following)
13
+ const proxy = new Proxy(target, {
14
+ get(target, prop) {
15
+ // @ts-expect-error
16
+ return target[camelToSnake(prop)] ?? target[prop]
17
+ },
18
+ set(target, p, v) {
19
+ // @ts-expect-error
20
+ target[camelToSnake(p)] = v
21
+ return true
22
+ }
23
+ })
24
+ // need to make sure all properties are initially available in snake_case
25
+ // @ts-expect-error
26
+ for (const [k,v] of Object.entries(target)) {
27
+ // @ts-expect-error
28
+ proxy[k] = v
29
+ }
30
+ // @ts-expect-error
31
+ return proxy
32
+ }
33
+ class Config {
34
+ static #defaults = {
35
+ propertiesOptional: true,
36
+ useEntitiesProxy: false,
37
+ inlineDeclarations: 'flat',
38
+ outputDirectory: '.'
39
+ }
40
+
41
+ values = undefined
42
+ proxy = undefined
43
+
44
+ init () {
45
+ this.values = {...Config.#defaults, ...(cds.env.typer ?? {})}
46
+ this.proxy = camelSnakeHybrid(this.values)
47
+ }
48
+
49
+ constructor() {
50
+ // proxy around config still allows arbitrary property access:
51
+ // require('config').configuration.logLevel = 'warn' will work
52
+ // eslint-disable-next-line no-constructor-return
53
+ return new Proxy(this, {
54
+ get(target, prop) {
55
+ // lazy loading of cds.env
56
+ // if we don't do this, configuration will load cds.env whenever it is
57
+ // first imported anywhere (even by proxy from, say, cli.js).
58
+ // So we don't get to modify cds.env before that, which is important
59
+ // in cds-build.js.
60
+ // FIXME: revisit. This is horrible.
61
+ if (target.values === undefined) target.init()
62
+ return target[prop] ?? target.proxy[prop]
63
+ },
64
+ set(target, p, v) {
65
+ if (target.values === undefined) target.init()
66
+
67
+ // this.value, this.proxy etc should not be forwarded to the wrapped values
68
+ if (target[p]) {
69
+ target[p] = v
70
+ } else {
71
+ target.proxy[p] = v
72
+ }
73
+ return true
74
+ }
75
+ })
76
+ }
77
+
78
+ /**
79
+ * @param {string} key - The key to set.
80
+ * @param {any} value - The value to set
81
+ */
82
+ setOne (key, value) {
83
+ this.proxy[key] = value
84
+ }
85
+
86
+ /**
87
+ * @param {object} props - The properties to set.
88
+ */
89
+ setMany (props) {
90
+ for (const [k,v] of Object.entries(props)) {
91
+ this.proxy[k] = v
92
+ }
93
+ }
94
+
95
+ /**
96
+ * Resets the config value and sets all its values from another passed
97
+ * config object. This allows to keep the reference to the same object.
98
+ * @param {Config} config - Another config object to set all config entries from.
99
+ */
100
+ setFrom (config) {
101
+ this.values = camelSnakeHybrid({})
102
+ this.setMany(config.values)
103
+ }
104
+
105
+ clone () {
106
+ const res = new Config()
107
+ res.init()
108
+ res.setMany(this.values)
109
+ return res
110
+ }
111
+ }
112
+
113
+ module.exports = {
114
+ camelSnakeHybrid,
115
+ /** @type {import('./typedefs').config.Configuration} */
116
+ // @ts-ignore
117
+ configuration: new Config()
118
+ }
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
@@ -8,11 +8,11 @@ const { normalise } = require('./components/identifier')
8
8
  const { empty } = require('./components/typescript')
9
9
  const { proxyAccessFunction } = require('./components/javascript')
10
10
  const { createObjectOf } = require('./components/wrappers')
11
+ const { configuration } = require('./config')
11
12
 
12
13
  const AUTO_GEN_NOTE = '// This is an automatically generated file. Please do not change its contents manually!'
13
14
 
14
15
  /** @typedef {import('./typedefs').file.Namespace} Namespace */
15
- /** @typedef {import('./typedefs').file.FileOptions} FileOptions */
16
16
 
17
17
  class File {
18
18
  /**
@@ -109,11 +109,9 @@ class Library extends File {
109
109
  class SourceFile extends File {
110
110
  /**
111
111
  * @param {string | Path} path - path to the file
112
- * @param {FileOptions} [options] - options for file output
113
112
  */
114
- constructor(path, options) {
113
+ constructor(path) {
115
114
  super()
116
- this.options = options ?? { useEntitiesProxy: false }
117
115
  /** @type {Path} */
118
116
  this.path = path instanceof Path ? path : new Path(path.split('.'))
119
117
  /** @type {{[key:string]: any}} */
@@ -159,7 +157,7 @@ class SourceFile extends File {
159
157
  * @param {boolean} [options.isStatic] - whether the lambda is static
160
158
  * @param {{positional?: boolean, named?: boolean}} [options.callStyles] - whether to generate positional and/or named call styles
161
159
  * @param {string[]?} [options.doc] - documentation for the operation
162
- * @returns {string} the stringified lambda
160
+ * @returns {[string,string[],string]} the stringified lambda parts
163
161
  * @example
164
162
  * ```js
165
163
  * // note: these samples are actually simplified! See below.
@@ -188,21 +186,25 @@ class SourceFile extends File {
188
186
  const callableSignatures = []
189
187
  if (callStyles.positional) {
190
188
  const paramTypesPositional = parameters.map(({name, type, doc}) => `${doc?'\n'+doc:''}${normalise(name)}: ${type}`).join(', ') // must not include ? modifiers
191
- 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(...)`
192
190
  }
193
191
  if (callStyles.named) {
194
192
  const parameterNames = createObjectOf(parameters.map(({name}) => normalise(name)).join(', '))
195
- callableSignatures.push(`// named\n${docStr}(${parameterNames}: ${parameterTypeAsObject}): ${returns}`)
193
+ callableSignatures.push('// named',`${docStr}(${parameterNames}: ${parameterTypeAsObject}): ${returns}`)
196
194
  }
197
195
  if (callableSignatures.length === 0) throw new Error('At least one call style must be specified')
198
196
  let prefix = name ? `${normalise(name)}: `: ''
199
197
  if (prefix && isStatic) {
200
198
  prefix = `static ${prefix}`
201
199
  }
202
- const kindDef = kind ? `, kind: '${kind}'` : ''
200
+ const kindDef = kind ? [`kind: '${kind}'`] : []
203
201
  const suffix = initialiser ? ` = ${initialiser}` : ''
204
- const lambda = `{\n${callableSignatures.join('\n')}, \n// metadata (do not use)\n__parameters: ${parameterTypeAsObject}, __returns: ${returns}${kindDef}}`
205
- return prefix + lambda + suffix
202
+
203
+ return [
204
+ `${prefix} {`,
205
+ [...callableSignatures, '// metadata (do not use)', `__parameters: ${parameterTypeAsObject}, __returns: ${returns}`, ...kindDef],
206
+ `}${suffix}`,
207
+ ]
206
208
  }
207
209
 
208
210
  /**
@@ -230,7 +232,8 @@ class SourceFile extends File {
230
232
  addOperation(name, parameters, returns, kind, doc, callStyles) {
231
233
  // this.operations.buffer.add(`// ${kind}`)
232
234
  if (doc) this.operations.buffer.add(doc.join('\n')) // docs shows up on action provider side: `.on(action,...)`
233
- 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)
234
237
  this.operations.names.push(name)
235
238
  }
236
239
 
@@ -397,7 +400,7 @@ class SourceFile extends File {
397
400
  buffer.add(`import * as ${imp.asIdentifier()} from '${imp.asDirectory({relative: this.path.asDirectory()})}';`)
398
401
  }
399
402
  }
400
- buffer.add('') // empty line after imports
403
+ buffer.blankLine()
401
404
  return buffer
402
405
  }
403
406
 
@@ -437,7 +440,7 @@ class SourceFile extends File {
437
440
  const namespace = this.path.asNamespace()
438
441
 
439
442
  const boilerplate = [AUTO_GEN_NOTE]
440
- if (this.options.useEntitiesProxy) {
443
+ if (configuration.useEntitiesProxy) {
441
444
  if (namespace === '_') {
442
445
  boilerplate.push('const cds = require(\'@sap/cds\')', this.#getEntityProxyFunctionExport())
443
446
  } else {
@@ -460,7 +463,7 @@ class SourceFile extends File {
460
463
  * @returns {{singularRhs: string, pluralRhs: string}}
461
464
  */
462
465
  #getEntityExportsRhs(singular, original) {
463
- if (this.options.useEntitiesProxy) {
466
+ if (configuration.useEntitiesProxy) {
464
467
  const namespace = this.path.asNamespace()
465
468
  // determine the custom properties for the proxy function call
466
469
  const customProps = this.entityProxies[singular] ?? []
@@ -587,10 +590,27 @@ class Buffer {
587
590
 
588
591
  /**
589
592
  * Adds an element to the buffer with the current indentation level.
590
- * @param {string} part - what to attach to the buffer
593
+ * @param {string | (() => string) | ((() => string) | string)[]} part - what to attach to the buffer
591
594
  */
592
595
  add(part) {
593
- 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('')
594
614
  }
595
615
 
596
616
  /**
@@ -697,13 +717,6 @@ class FileRepository {
697
717
  /** @type {{[key:string]: SourceFile}} */
698
718
  #files = {}
699
719
 
700
- /**
701
- * @param {FileOptions} options - options to control file
702
- */
703
- constructor(options) {
704
- this.options = options
705
- }
706
-
707
720
  /**
708
721
  * @param {string} name - file name
709
722
  * @param {SourceFile} file - the file
@@ -720,7 +733,7 @@ class FileRepository {
720
733
  */
721
734
  getNamespaceFile(path) {
722
735
  const key = path instanceof Path ? path.asNamespace() : path
723
- return (this.#files[key] ??= new SourceFile(path, this.options))
736
+ return (this.#files[key] ??= new SourceFile(path))
724
737
  }
725
738
 
726
739
  /**