@cap-js/cds-typer 0.19.0 → 0.20.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/csn.js CHANGED
@@ -1,5 +1,33 @@
1
1
  const annotation = '@odata.draft.enabled'
2
2
 
3
+ /**
4
+ * FIXME: this is pretty handwavey: we are looking for view-entities,
5
+ * i.e. ones that have a query, but are not a cds level projection.
6
+ * Those are still not expanded and we have to retrieve their definition
7
+ * with all properties from the inferred model.
8
+ */
9
+ const isView = entity => entity.query && !entity.projection
10
+
11
+ const isProjection = entity => entity.projection
12
+
13
+ const getViewTarget = entity => entity.query?.SELECT?.from?.ref?.[0]
14
+
15
+ const getProjectionTarget = entity => entity.projection?.from?.ref?.[0]
16
+
17
+ const isDraftEnabled = entity => entity['@odata.draft.enabled'] === true
18
+
19
+ const isType = entity => entity?.kind === 'type'
20
+
21
+ const isEntity = entity => entity?.kind === 'entity'
22
+
23
+ /**
24
+ * @see isView
25
+ * Unresolved entities have to be looked up from inferred csn.
26
+ */
27
+ const isUnresolved = entity => entity._unresolved === true
28
+
29
+ const isCsnAny = entity => entity?.constructor?.name === 'any'
30
+
3
31
  class DraftUnroller {
4
32
  /** @type {Set<string>} */
5
33
  #positives = new Set()
@@ -226,19 +254,6 @@ function amendCSN(csn) {
226
254
  propagateForeignKeys(csn)
227
255
  }
228
256
 
229
- /**
230
- * FIXME: this is pretty handwavey: we are looking for view-entities,
231
- * i.e. ones that have a query, but are not a cds level projection.
232
- * Those are still not expanded and we have to retrieve their definition
233
- * with all properties from the inferred model.
234
- */
235
- const isView = entity => entity.query && !entity.projection
236
-
237
- const isProjection = entity => entity.projection
238
-
239
- const getViewTarget = entity => entity.query?.SELECT?.from?.ref?.[0]
240
-
241
- const getProjectionTarget = entity => entity.projection?.from?.ref?.[0]
242
257
 
243
258
  const getProjectionAliases = entity => {
244
259
  const aliases = {}
@@ -255,25 +270,17 @@ const getProjectionAliases = entity => {
255
270
  return { aliases, all }
256
271
  }
257
272
 
258
- const isDraftEnabled = entity => entity['@odata.draft.enabled'] === true
259
-
260
- const isType = entity => entity?.kind === 'type'
261
-
262
- /**
263
- * @see isView
264
- * Unresolved entities have to be looked up from inferred csn.
265
- */
266
- const isUnresolved = entity => entity._unresolved === true
267
-
268
273
  module.exports = {
269
274
  amendCSN,
270
275
  isView,
271
276
  isProjection,
272
277
  isDraftEnabled,
278
+ isEntity,
273
279
  isUnresolved,
274
280
  isType,
275
281
  getProjectionTarget,
276
282
  getProjectionAliases,
277
283
  getViewTarget,
278
- propagateForeignKeys
284
+ propagateForeignKeys,
285
+ isCsnAny
279
286
  }
package/lib/file.js CHANGED
@@ -1,12 +1,14 @@
1
1
  'use strict'
2
2
 
3
3
  const fs = require('fs').promises
4
+ const path = require('path')
4
5
  const { readFileSync } = require('fs')
5
6
  const { printEnum, propertyToInlineEnumName, stringifyEnumImplementation } = require('./components/enum')
6
7
  const { normalise } = require('./components/identifier')
7
- const path = require('path')
8
+ const { empty } = require('./components/typescript')
9
+ const { createObjectOf } = require('./components/wrappers')
8
10
 
9
- const AUTO_GEN_NOTE = "// This is an automatically generated file. Please do not change its contents manually!"
11
+ const AUTO_GEN_NOTE = '// This is an automatically generated file. Please do not change its contents manually!'
10
12
 
11
13
  /** @typedef {Object<string, Buffer>} Namespace */
12
14
 
@@ -115,7 +117,7 @@ class SourceFile extends File {
115
117
  /** @type {Buffer} */
116
118
  this.classes = new Buffer()
117
119
  /** @type {{ buffer: Buffer, names: string[]}} */
118
- this.actions = { buffer: new Buffer(), names: [] }
120
+ this.operations = { buffer: new Buffer(), names: [] }
119
121
  /** @type {Buffer} */
120
122
  this.aspects = new Buffer()
121
123
  /** @type {Namespace} */
@@ -154,15 +156,19 @@ class SourceFile extends File {
154
156
  * stringifyLambda({name: 'f', parameters: [['p','T']], returns: 'number'}) // { (p: T): number, __parameters: { p: T } }
155
157
  * ```
156
158
  */
157
- static stringifyLambda({name, parameters=[], returns='any', initialiser, isStatic=false}) {
159
+ static stringifyLambda({name, parameters=[], returns='any', initialiser, isStatic=false, kind}) {
158
160
  const parameterTypes = parameters.map(([n, t]) => `${normalise(n)}: ${t}`).join(', ')
161
+ const parameterTypeAsObject = parameterTypes.length
162
+ ? createObjectOf(parameterTypes)
163
+ : empty
159
164
  const callableSignature = `(${parameterTypes}): ${returns}`
160
165
  let prefix = name ? `${normalise(name)}: `: ''
161
166
  if (prefix && isStatic) {
162
167
  prefix = `static ${prefix}`
163
168
  }
169
+ const kindDef = kind ? `, kind: '${kind}'` : ''
164
170
  const suffix = initialiser ? ` = ${initialiser}` : ''
165
- const lambda = `{ ${callableSignature}, __parameters: {${parameterTypes}}, __returns: ${returns} }`
171
+ const lambda = `{ ${callableSignature}, __parameters: ${parameterTypeAsObject}, __returns: ${returns}${kindDef}}`
166
172
  return prefix + lambda + suffix
167
173
  }
168
174
 
@@ -183,25 +189,13 @@ class SourceFile extends File {
183
189
  * Adds a function definition in form of a arrow function to the file.
184
190
  * @param {string} name name of the function
185
191
  * @param {{relative: string | undefined, local: boolean, posix: boolean}} parameters list of parameters, passed as [name, type] pairs
192
+ * @param {'function' | 'action'} kind
186
193
  * @param returns the return type of the function
187
194
  */
188
- addFunction(name, parameters, returns) {
189
- // FIXME: use different buffers for buffers and actions, or at least rename buffer to the more general category "functions"?
190
- this.actions.buffer.add("// function")
191
- this.actions.buffer.add(`export declare const ${SourceFile.stringifyLambda({name, parameters, returns})};`)
192
- this.actions.names.push(name)
193
- }
194
-
195
- /**
196
- * Adds an action definition in form of a arrow function to the file.
197
- * @param {string} name name of the action
198
- * @param {{relative: string | undefined, local: boolean, posix: boolean}} parameters list of parameters, passed as [name, type] pairs
199
- * @param returns the return type of the action
200
- */
201
- addAction(name, parameters, returns) {
202
- this.actions.buffer.add("// action")
203
- this.actions.buffer.add(`export declare const ${SourceFile.stringifyLambda({name, parameters, returns})};`)
204
- this.actions.names.push(name)
195
+ addOperation(name, parameters, returns, kind) {
196
+ //this.operations.buffer.add("// operation")
197
+ this.operations.buffer.add(`export declare const ${SourceFile.stringifyLambda({name, parameters, returns, kind})};`)
198
+ this.operations.names.push(name)
205
199
  }
206
200
 
207
201
  /**
@@ -382,38 +376,38 @@ class SourceFile extends File {
382
376
  this.aspects.join(), // needs to be before classes
383
377
  this.classes.join(),
384
378
  this.events.buffer.join(),
385
- this.actions.buffer.join(),
379
+ this.operations.buffer.join(),
386
380
  namespaces.join() // needs to be after classes for possible declaration merging
387
381
  ].filter(Boolean).join('\n')
388
382
  }
389
383
 
390
384
  toJSExports() {
391
- return [AUTO_GEN_NOTE, "const cds = require('@sap/cds')", `const csn = cds.entities('${this.path.asNamespace()}')`] // boilerplate
385
+ return [AUTO_GEN_NOTE, 'const cds = require(\'@sap/cds\')', `const csn = cds.entities('${this.path.asNamespace()}')`] // boilerplate
392
386
  .concat(
393
387
  // FIXME: move stringification of service into own module
394
388
  this.services.names.map(name => `module.exports = { name: '${name}' }`)) // there should be only one
395
- .concat(this.inflections
396
- // sorting the entries based on the number of dots in their singular.
397
- // that makes sure we have defined all parent namespaces before adding subclasses to them e.g.:
398
- // "module.exports.Books" is defined before "module.exports.Books.text"
399
- .sort(([a], [b]) => a.split('.').length - b.split('.').length)
400
- // by using a temporary Set we make sure to catch cases where
401
- // (1) plural is the same as original (default case) and
402
- // (2) plural differs from original, i.e. when a @plural annotation is present
403
- // or when plural4 produced weird inflection.
404
- .flatMap(([singular, plural, original]) => Array.from(new Set([
405
- `module.exports.${singular} = csn.${original}`,
406
- /Array<.*>/.test(plural) ? undefined : `module.exports.${plural} = csn.${original}`,
407
- // FIXME: we currently produce at most 3 entries.
408
- // This could be an issue when the user re-used the original name in a @singular/@plural annotation.
409
- // Seems unlikely, but we have to eliminate the original entry if users start running into this.
410
- `module.exports.${original} = csn.${original}`
411
- ].filter(Boolean)))) // FIXME: this is a hack to support CDS types that will produce "Array<MyType>" as plural, which we do not want as export in the index.js files
389
+ .concat(this.inflections
390
+ // sorting the entries based on the number of dots in their singular.
391
+ // that makes sure we have defined all parent namespaces before adding subclasses to them e.g.:
392
+ // "module.exports.Books" is defined before "module.exports.Books.text"
393
+ .sort(([a], [b]) => a.split('.').length - b.split('.').length)
394
+ // by using a temporary Set we make sure to catch cases where
395
+ // (1) plural is the same as original (default case) and
396
+ // (2) plural differs from original, i.e. when a @plural annotation is present
397
+ // or when plural4 produced weird inflection.
398
+ .flatMap(([singular, plural, original]) => Array.from(new Set([
399
+ `module.exports.${singular} = csn.${original}`,
400
+ /Array<.*>/.test(plural) ? undefined : `module.exports.${plural} = csn.${original}`,
401
+ // FIXME: we currently produce at most 3 entries.
402
+ // This could be an issue when the user re-used the original name in a @singular/@plural annotation.
403
+ // Seems unlikely, but we have to eliminate the original entry if users start running into this.
404
+ `module.exports.${original} = csn.${original}`
405
+ ].filter(Boolean)))) // FIXME: this is a hack to support CDS types that will produce "Array<MyType>" as plural, which we do not want as export in the index.js files
412
406
  ) // singular -> plural aliases
413
407
  .concat(['// events'])
414
408
  .concat(this.events.fqs.map(({fq, name}) => `module.exports.${name} = '${fq}'`))
415
409
  .concat(['// actions'])
416
- .concat(this.actions.names.map(name => `module.exports.${name} = '${name}'`))
410
+ .concat(this.operations.names.map(name => `module.exports.${name} = '${name}'`))
417
411
  .concat(['// enums'])
418
412
  .concat(this.enums.data.map(({name, kvs}) => stringifyEnumImplementation(name, kvs)))
419
413
  .join('\n') + '\n'
@@ -494,7 +488,7 @@ class Buffer {
494
488
  if (typeof part === 'function') {
495
489
  part()
496
490
  } else if (Array.isArray(part)) {
497
- part.forEach(p => this.add(p))
491
+ part.forEach(p => { this.add(p) })
498
492
  }
499
493
  this.outdent()
500
494
  }
@@ -610,44 +604,6 @@ class FileRepository {
610
604
  }
611
605
  }
612
606
 
613
- /**
614
- * Base definitions used throughout the typing process,
615
- * such as Associations and Compositions.
616
- * @type {SourceFile}
617
- */
618
- const baseDefinitions = new SourceFile('_')
619
- // FIXME: this should be a library someday
620
- baseDefinitions.addPreamble(`
621
- export namespace Association {
622
- export type to <T> = T;
623
- export namespace to {
624
- export type many <T extends readonly any[]> = T;
625
- }
626
- }
627
-
628
- export namespace Composition {
629
- export type of <T> = T;
630
- export namespace of {
631
- export type many <T extends readonly any[]> = T;
632
- }
633
- }
634
-
635
- export class Entity {
636
- static data<T extends Entity> (this:T, _input:Object) : T {
637
- return {} as T // mock
638
- }
639
- }
640
-
641
- export type EntitySet<T> = T[] & {
642
- data (input:object[]) : T[]
643
- data (input:object) : T
644
- };
645
-
646
- export type DeepRequired<T> = {
647
- [K in keyof T]: DeepRequired<T[K]>
648
- } & Exclude<Required<T>, null>;
649
- `)
650
-
651
607
  /**
652
608
  * Writes the files to disk. For each source, a index.d.ts holding the type definitions
653
609
  * and a index.js holding implementation stubs is generated at the appropriate directory.
@@ -658,18 +614,19 @@ export type DeepRequired<T> = {
658
614
  */
659
615
  const writeout = async (root, sources) =>
660
616
  Promise.all(
661
- sources.map(async (source) => {
617
+ sources.map(async source => {
662
618
  const dir = path.join(root, source.path.asDirectory({local: false, posix: false}))
663
619
  try {
664
620
  await fs.mkdir(dir, { recursive: true })
665
621
  await Promise.all([
666
- fs.writeFile(path.join(dir, 'index.ts'), source.toTypeDefs()),
667
- fs.writeFile(path.join(dir, 'index.js'), source.toJSExports()),
668
- ])
622
+ fs.writeFile(path.join(dir, 'index.ts'), source.toTypeDefs()),
623
+ fs.writeFile(path.join(dir, 'index.js'), source.toJSExports()),
624
+ ])
669
625
 
670
626
  } catch (err) {
671
627
  // eslint-disable-next-line no-console
672
628
  console.error(`Could not create parent directory ${dir}: ${err}.`)
629
+ // eslint-disable-next-line no-console
673
630
  console.error(err.stack)
674
631
  }
675
632
  return dir
@@ -683,6 +640,5 @@ module.exports = {
683
640
  FileRepository,
684
641
  SourceFile,
685
642
  Path,
686
- writeout,
687
- baseDefinitions,
643
+ writeout
688
644
  }
package/lib/util.js CHANGED
@@ -1,5 +1,3 @@
1
- /* eslint-disable indent */
2
-
3
1
  /**
4
2
  * @typedef { {name?: string, '@singular'?: string, '@plural'?: string,} } Annotations
5
3
  * @typedef { {desc: string, default?: any} } CommandlineFlag
@@ -30,7 +28,7 @@ const annotations = {
30
28
  * @param {Object} csn the CSN of an entity to check
31
29
  * @returns {string | undefined} the singular annotation or undefined
32
30
  */
33
- const getSingularAnnotation = (csn) => csn[annotations.singular.find(a => Object.hasOwn(csn, a))]
31
+ const getSingularAnnotation = csn => csn[annotations.singular.find(a => Object.hasOwn(csn, a))]
34
32
 
35
33
  /**
36
34
  * Tries to retrieve an annotation that specifies the plural name
@@ -40,9 +38,9 @@ const getSingularAnnotation = (csn) => csn[annotations.singular.find(a => Object
40
38
  * @param {Object} csn the CSN of an entity to check
41
39
  * @returns {string | undefined} the plural annotation or undefined
42
40
  */
43
- const getPluralAnnotation = (csn) => csn[annotations.plural.find(a => Object.hasOwn(csn, a))]
41
+ const getPluralAnnotation = csn => csn[annotations.plural.find(a => Object.hasOwn(csn, a))]
44
42
 
45
- /**
43
+ /**
46
44
  * Users can specify that they want to refer to localisation
47
45
  * using the syntax {i18n>Foo}, where Foo is the name of the
48
46
  * entity as found in the .cds file
@@ -53,9 +51,9 @@ const getPluralAnnotation = (csn) => csn[annotations.plural.find(a => Object.has
53
51
  * @returns {string} the name without localisation syntax or untouched.
54
52
  * @deprecated we have dropped this feature altogether, users specify custom names via @singular/@plural now
55
53
  */
56
- const unlocalize = (name) => {
57
- const match = name.match(/\{i18n>(.*)\}/)
58
- return match ? match[1] : name
54
+ const unlocalize = name => {
55
+ const match = name.match(/\{i18n>(.*)\}/)
56
+ return match ? match[1] : name
59
57
  }
60
58
 
61
59
  /**
@@ -74,19 +72,19 @@ const singular4 = (dn, stripped = false) => {
74
72
  (/.*species|news$/i.test(n)
75
73
  ? n
76
74
  : /.*ess$/.test(n)
77
- ? n // Address
78
- : /.*ees$/.test(n)
79
- ? n.slice(0, -1) // Employees --> Employee
80
- : /.*[sz]es$/.test(n)
81
- ? n.slice(0, -2)
82
- : /.*[^aeiou]ies$/.test(n)
83
- ? n.slice(0, -3) + 'y' // Deliveries --> Delivery
84
- : /.*s$/.test(n)
85
- ? n.slice(0, -1)
86
- : /.*_$/.test(n) // special cdstyper case where we revert the _ suffix for when a plural can not be determined
87
- ? n.slice(0, -1)
88
- : n
89
- )
75
+ ? n // Address
76
+ : /.*ees$/.test(n)
77
+ ? n.slice(0, -1) // Employees --> Employee
78
+ : /.*[sz]es$/.test(n)
79
+ ? n.slice(0, -2)
80
+ : /.*[^aeiou]ies$/.test(n)
81
+ ? n.slice(0, -3) + 'y' // Deliveries --> Delivery
82
+ : /.*s$/.test(n)
83
+ ? n.slice(0, -1)
84
+ : /.*_$/.test(n) // special cdstyper case where we revert the _ suffix for when a plural can not be determined
85
+ ? n.slice(0, -1)
86
+ : n
87
+ )
90
88
  )
91
89
  }
92
90
 
@@ -106,10 +104,10 @@ const plural4 = (dn, stripped) => {
106
104
  (/.*analysis|status|species|news$/i.test(n)
107
105
  ? n
108
106
  : /.*[^aeiou]y$/.test(n)
109
- ? n.slice(0, -1) + 'ies'
110
- : /.*(s|x|z|ch|sh)$/.test(n)
111
- ? n + 'es'
112
- : n + 's')
107
+ ? n.slice(0, -1) + 'ies'
108
+ : /.*(s|x|z|ch|sh)$/.test(n)
109
+ ? n + 'es'
110
+ : n + 's')
113
111
  )
114
112
  }
115
113
 
@@ -121,8 +119,8 @@ const plural4 = (dn, stripped) => {
121
119
  */
122
120
  const deepMerge = (target, source) => {
123
121
  Object.keys(target)
124
- .filter((k) => k in source)
125
- .forEach((k) => deepMerge(target[k], source[k]))
122
+ .filter(k => k in source)
123
+ .forEach(k => { deepMerge(target[k], source[k]) })
126
124
  Object.assign(target, source)
127
125
  }
128
126
 
@@ -141,7 +139,7 @@ const deepMerge = (target, source) => {
141
139
  * @returns {ParsedFlags}
142
140
  */
143
141
  const parseCommandlineArgs = (argv, validFlags) => {
144
- const isFlag = (arg) => arg.startsWith('--')
142
+ const isFlag = arg => arg.startsWith('--')
145
143
  const positional = []
146
144
  const named = {}
147
145
 
@@ -173,7 +171,7 @@ const parseCommandlineArgs = (argv, validFlags) => {
173
171
  }
174
172
 
175
173
  const defaults = Object.entries(validFlags)
176
- .filter((e) => !!e[1].default)
174
+ .filter(e => !!e[1].default)
177
175
  .reduce((dict, [k, v]) => {
178
176
  dict[k] = v.default
179
177
  return dict