@cap-js/cds-typer 0.11.0 → 0.12.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,12 +4,26 @@ 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.12.0 - TBD
7
+ ## Version 0.13.0 - TBD
8
+
9
+ ## Version 0.12.0 - 2023-11-23
10
+
11
+ ### Changed
12
+ - Generate `cds.LargeBinary` as string, buffer, _or readable_ in the case of media content
13
+
14
+ ### Added
15
+ - Added support for the `not null` modifier
16
+
17
+ ### Fixed
18
+ - Now using names of enum values in generated _index.js_ files if no explicit value is present
19
+
20
+ ## Version 0.11.1 - 2023-10-12
8
21
 
9
22
  ### Changed
10
23
 
11
24
  ### Added
12
25
  ### Fixed
26
+ - Fixed how service names are exported as default export
13
27
 
14
28
  ## Version 0.11.0 - 2023-10-10
15
29
 
package/lib/cli.js CHANGED
@@ -99,8 +99,4 @@ const main = async (args) => {
99
99
 
100
100
  if (require.main === module) {
101
101
  main(parseCommandlineArgs(process.argv.slice(2), flags))
102
- }
103
-
104
- function helpToCapire() {
105
-
106
102
  }
@@ -82,7 +82,7 @@ const propertyToAnonymousEnumName = (entity, property) => `${entity}_${property}
82
82
  */
83
83
  const isInlineEnumType = (element, csn) => element.enum && !(element.type in csn.definitions)
84
84
 
85
- const stringifyEnumImplementation = (name, enm) => `module.exports.${name} = Object.fromEntries(Object.entries(${enm}).map(([k,v]) => [k,v.val]))`
85
+ const stringifyEnumImplementation = (name, enm) => `module.exports.${name} = Object.fromEntries(Object.entries(${enm}).map(([k,v]) => [k,v.val??k]))`
86
86
 
87
87
  /**
88
88
  * @param {string} name
@@ -17,6 +17,7 @@ class InlineDeclarationResolver {
17
17
  * @protected
18
18
  * @abstract
19
19
  */
20
+ // eslint-disable-next-line no-unused-vars
20
21
  printInlineType(name, type, buffer, statementEnd) { /* abstract */ }
21
22
 
22
23
  /**
@@ -84,6 +85,17 @@ class InlineDeclarationResolver {
84
85
  return this.visitor.options.propertiesOptional ? '?:' : ':'
85
86
  }
86
87
 
88
+ /**
89
+ * It returns TypeScript datatype for provided TS property
90
+ * @param {{typeName: string, typeInfo: TypeResolveInfo & { inflection: Inflection } }} type
91
+ * @param {string} typeName name of the TypeScript property
92
+ * @return {string} the datatype to be presented on TypeScript layer
93
+ * @public
94
+ */
95
+ getPropertyDatatype(type, typeName = type.typeName) {
96
+ return type.typeInfo.isNotNull ? typeName : `${typeName} | null`
97
+ }
98
+
87
99
  /** @param {import('../visitor').Visitor} visitor */
88
100
  constructor(visitor) {
89
101
  this.visitor = visitor
@@ -111,6 +123,7 @@ class InlineDeclarationResolver {
111
123
  * @public
112
124
  * @abstract
113
125
  */
126
+ // eslint-disable-next-line no-unused-vars
114
127
  getTypeLookup(members) { /* abstract */ return '' }
115
128
  }
116
129
 
@@ -142,7 +155,7 @@ class FlatInlineDeclarationResolver extends InlineDeclarationResolver {
142
155
  flatten(prefix, type) {
143
156
  return type.typeInfo.structuredType
144
157
  ? Object.entries(type.typeInfo.structuredType).map(([k,v]) => this.flatten(`${this.prefix(prefix)}${k}`, v))
145
- : [`${prefix}${this.getPropertyTypeSeparator()} ${type.typeName}`]
158
+ : [`${prefix}${this.getPropertyTypeSeparator()} ${this.getPropertyDatatype(type)}`]
146
159
  }
147
160
 
148
161
  printInlineType(name, type, buffer) {
@@ -192,9 +205,9 @@ class StructuredInlineDeclarationResolver extends InlineDeclarationResolver {
192
205
  this.flatten(n, t, buffer)
193
206
  }
194
207
  buffer.outdent()
195
- buffer.add(`}${lineEnding}`)
208
+ buffer.add(`}${this.getPropertyDatatype(type, '')}${lineEnding}`)
196
209
  } else {
197
- buffer.add(`${name}${this.getPropertyTypeSeparator()} ${type.typeName}${lineEnding}`)
210
+ buffer.add(`${name}${this.getPropertyTypeSeparator()} ${this.getPropertyDatatype(type)}${lineEnding}`)
198
211
  }
199
212
  this.printDepth--
200
213
  return buffer
@@ -1,10 +1,11 @@
1
1
  'use strict'
2
2
 
3
3
  const util = require('../util')
4
+ // eslint-disable-next-line no-unused-vars
4
5
  const { Buffer, SourceFile, Path, Library, baseDefinitions } = require("../file")
5
6
  const { deepRequire, createToManyAssociation, createToOneAssociation, createArrayOf, createCompositionOfMany, createCompositionOfOne } = require('./wrappers')
6
7
  const { StructuredInlineDeclarationResolver } = require("./inline")
7
- const { isInlineEnumType, propertyToInlineEnumName, propertyToAnonymousEnumName } = require('./enum')
8
+ const { isInlineEnumType, propertyToAnonymousEnumName } = require('./enum')
8
9
 
9
10
  /** @typedef {{ cardinality?: { max?: '*' | number } }} EntityCSN */
10
11
  /** @typedef {{ definitions?: Object<string, EntityCSN> }} CSN */
@@ -22,6 +23,7 @@ const { isInlineEnumType, propertyToInlineEnumName, propertyToAnonymousEnumName
22
23
  * ```
23
24
  * @typedef {{
24
25
  * isBuiltin: boolean,
26
+ * isNotNull: boolean,
25
27
  * isInlineDeclaration: boolean,
26
28
  * isForeignKeyReference: boolean,
27
29
  * isArray: boolean,
@@ -41,7 +43,7 @@ const Builtins = {
41
43
  String: 'string',
42
44
  Binary: 'string',
43
45
  LargeString: 'string',
44
- LargeBinary: 'Buffer | string',
46
+ LargeBinary: 'Buffer | string | {value: import("stream").Readable, $mediaContentType: string, $mediaContentDispositionFilename?: string, $mediaContentDispositionType?: string}',
45
47
  Integer: 'number',
46
48
  UInt8: 'number',
47
49
  Int16: 'number',
@@ -231,6 +233,8 @@ class Resolver {
231
233
 
232
234
  if (toOne && toMany) {
233
235
  const target = element.items ?? (typeof element.target === 'string' ? { type: element.target } : element.target)
236
+ /** set `notNull = true` to avoid repeated `| not null` TS construction */
237
+ target.notNull = true
234
238
  const { singular, plural } = this.resolveAndRequire(target, file).typeInfo.inflection
235
239
  typeName =
236
240
  cardinality > 1 ? toMany(plural) : toOne(this.visitor.isSelfReference(target) ? 'this' : singular)
@@ -254,7 +258,7 @@ class Resolver {
254
258
  }
255
259
 
256
260
  if (element.type.ref?.length > 1) {
257
- const [entity, ...members] = element.type.ref
261
+ const [, ...members] = element.type.ref
258
262
  const lookup = this.visitor.inlineDeclarationResolver.getTypeLookup(members)
259
263
  typeName = deepRequire(typeInfo.inflection.singular, lookup)
260
264
  file.addImport(baseDefinitions.path)
@@ -348,11 +352,16 @@ class Resolver {
348
352
  return element
349
353
  }
350
354
 
355
+ const cardinality = this.getMaxCardinality(element)
356
+
351
357
  const result = {
352
358
  isBuiltin: false, // will be rectified in the corresponding handlers, if needed
353
359
  isInlineDeclaration: false,
354
360
  isForeignKeyReference: false,
355
361
  isArray: false,
362
+ isNotNull: element?.isRefNotNull !== undefined
363
+ ? element?.isRefNotNull
364
+ : element?.key || element?.notNull || cardinality > 1,
356
365
  }
357
366
 
358
367
  if (element?.type === undefined) {
@@ -377,6 +386,8 @@ class Resolver {
377
386
  // objects and arrays
378
387
  if (element?.items) {
379
388
  result.isArray = true
389
+ // TODO: re-implement this line once {element.notNull} will be provided for array-like elements
390
+ result.isNotNull = true
380
391
  result.isBuiltin = true
381
392
  this.resolveType(element.items, file)
382
393
  //delete element.items
package/lib/csn.js CHANGED
@@ -221,49 +221,6 @@ function propagateForeignKeys(csn) {
221
221
  }
222
222
  }
223
223
 
224
- /**
225
- * Entities inherit their ancestors annotations:
226
- * https://cap.cloud.sap/docs/cds/cdl#annotation-propagation
227
- * This is a problem if we annotate @singular/ @plural to an entity A,
228
- * as we don't want all descendents B, C, ... to share the ancestor's
229
- * annotated inflexion
230
- * -> remove all such annotations that appear in a parent as well.
231
- * BUT: we can't just delete the attributes. Imagine three classes
232
- * A <- B <- C
233
- * where A contains a @singular annotation.
234
- * If we erase the annotation from B, C will still contain it and
235
- * can not detect that its own annotation was inherited without
236
- * travelling up the entire inheritance chain up to A.
237
- * So instead, we monkey patch and maintain a dictionary "erased"
238
- * when removing an annotation which we also check.
239
- * @deprecated since we use the xtended flavour for CSN, we don't need to fix this anymore
240
- */
241
- function propagateInflectionAnnotations(csn) {
242
- const erase = (entity, parent, attr) => {
243
- if (attr in entity) {
244
- const ea = entity[attr]
245
- if (parent[attr] === ea || (parent.erased && parent.erased[attr] === ea)) {
246
- entity.erased ??= {}
247
- entity.erased[attr] = ea
248
- delete entity[attr]
249
- //this.logger.info(`Removing inherited attribute ${attr} from ${entity.name}.`)
250
- }
251
- }
252
- }
253
-
254
- for (const entity of Object.values(csn.definitions)) {
255
- let i = 0
256
- while (
257
- (getSingularAnnotation(entity) || getPluralAnnotation(entity)) &&
258
- i < (entity.includes ?? []).length
259
- ) {
260
- const parent = csn.definitions[entity.includes[i]]
261
- Object.values(annotations).flat().forEach(an => erase(entity, parent, an))
262
- i++
263
- }
264
- }
265
- }
266
-
267
224
  function amendCSN(csn) {
268
225
  unrollDraftability(csn)
269
226
  propagateForeignKeys(csn)
package/lib/file.js CHANGED
@@ -335,6 +335,7 @@ class SourceFile extends File {
335
335
  * @param {string} fq the fully qualified name of the service
336
336
  */
337
337
  addService(fq) {
338
+ // FIXME: warn the user when they're trying to add an entity/ type/ enum called "name", which will override our name export
338
339
  if (this.services.names.length) {
339
340
  throw new Error(`trying to add more than one service to file ${this.path.asDirectory()}. Existing service is ${this.services.names[0]}, trying to add ${fq}`)
340
341
  }
@@ -369,6 +370,7 @@ class SourceFile extends File {
369
370
  AUTO_GEN_NOTE,
370
371
  this.getImports().join(),
371
372
  this.preamble.join(),
373
+ this.services.buffer.join(), // must be the very first
372
374
  this.types.join(),
373
375
  this.enums.buffer.join(),
374
376
  this.inlineEnums.buffer.join(), // needs to be before classes
@@ -376,15 +378,16 @@ class SourceFile extends File {
376
378
  this.aspects.join(), // needs to be before classes
377
379
  this.classes.join(),
378
380
  this.events.buffer.join(),
379
- this.actions.buffer.join(),
380
- this.services.buffer.join() // should be at the end
381
+ this.actions.buffer.join()
381
382
  ].filter(Boolean).join('\n')
382
383
  }
383
384
 
384
385
  toJSExports() {
385
386
  return [AUTO_GEN_NOTE, "const cds = require('@sap/cds')", `const csn = cds.entities('${this.path.asNamespace()}')`] // boilerplate
386
387
  .concat(
387
- this.inflections
388
+ // FIXME: move stringification of service into own module
389
+ this.services.names.map(name => `module.exports = { name: '${name}' }`)) // there should be only one
390
+ .concat(this.inflections
388
391
  // sorting the entries based on the number of dots in their singular.
389
392
  // that makes sure we have defined all parent namespaces before adding subclasses to them e.g.:
390
393
  // "module.exports.Books" is defined before "module.exports.Books.text"
@@ -410,8 +413,6 @@ class SourceFile extends File {
410
413
  .concat(this.enums.fqs.map(({name, fq, property}) => property
411
414
  ? stringifyAnonymousEnum(name, fq, property)
412
415
  : stringifyNamedEnum(name, fq)))
413
- // FIXME: move stringification of service into own module
414
- .concat(this.services.names.map(name => `module.exports.default = { name: '${name}' }`)) // there should be only one
415
416
  .join('\n') + '\n'
416
417
  }
417
418
  }
package/lib/visitor.js CHANGED
@@ -3,6 +3,7 @@
3
3
  const util = require('./util')
4
4
 
5
5
  const { amendCSN } = require('./csn')
6
+ // eslint-disable-next-line no-unused-vars
6
7
  const { SourceFile, baseDefinitions, Buffer } = require('./file')
7
8
  const { FlatInlineDeclarationResolver, StructuredInlineDeclarationResolver } = require('./components/inline')
8
9
  const { Resolver } = require('./components/resolver')
@@ -116,7 +117,11 @@ class Visitor {
116
117
  // instead of using the definition from inferred CSN, we refer to the projected entity from xtended CSN instead.
117
118
  // The latter contains the CSN fixes (propagated foreign keys, etc) and none of the localised fields we don't handle yet.
118
119
  if (entity.projection) {
119
- this.visitEntity(name, this.csn.xtended.definitions[entity.projection.from.ref[0]])
120
+ const targetName = entity.projection.from.ref[0]
121
+ // FIXME: references to types of entity properties may be missing from xtendend flavour (see #103)
122
+ // this should be revisted once we settle on a single flavour.
123
+ const target = this.csn.xtended.definitions[targetName] ?? this.csn.inferred.definitions[targetName]
124
+ this.visitEntity(name, target)
120
125
  } else {
121
126
  this.logger.error(`Expecting an autoexposed projection within a service. Skipping ${name}`)
122
127
  }
@@ -160,7 +165,10 @@ class Visitor {
160
165
  // We don't really have to care for this case, as keys from such structs are _not_ propagated to
161
166
  // the containing entity.
162
167
  for (const [kname, kelement] of Object.entries(this.csn.xtended.definitions[element.target]?.keys ?? {})) {
163
- this.visitElement(`${ename}_${kname}`, kelement, file, buffer)
168
+ if (this.resolver.getMaxCardinality(element) === 1) {
169
+ kelement.isRefNotNull = !!element.notNull || !!element.key
170
+ this.visitElement(`${ename}_${kname}`, kelement, file, buffer)
171
+ }
164
172
  }
165
173
  }
166
174
 
@@ -299,7 +307,7 @@ class Visitor {
299
307
  .filter(([, type]) => type?.type !== '$self' && !(type.items?.type === '$self'))
300
308
  .map(([name, type]) => [
301
309
  name,
302
- this.resolver.resolveAndRequire(type, file).typeName,
310
+ this.resolver.visitor.inlineDeclarationResolver.getPropertyDatatype(this.resolver.resolveAndRequire(type, file)),
303
311
  ])
304
312
  : []
305
313
  }
@@ -310,7 +318,9 @@ class Visitor {
310
318
  const ns = this.resolver.resolveNamespace(name.split('.'))
311
319
  const file = this.getNamespaceFile(ns)
312
320
  const params = this.#stringifyFunctionParams(func.params, file)
313
- const returns = this.resolver.resolveAndRequire(func.returns, file).typeName
321
+ const returns = this.resolver.visitor.inlineDeclarationResolver.getPropertyDatatype(
322
+ this.resolver.resolveAndRequire(func.returns, file)
323
+ )
314
324
  file.addFunction(name.split('.').at(-1), params, returns)
315
325
  }
316
326
 
@@ -319,7 +329,9 @@ class Visitor {
319
329
  const ns = this.resolver.resolveNamespace(name.split('.'))
320
330
  const file = this.getNamespaceFile(ns)
321
331
  const params = this.#stringifyFunctionParams(action.params, file)
322
- const returns = this.resolver.resolveAndRequire(action.returns, file).typeName
332
+ const returns = this.resolver.visitor.inlineDeclarationResolver.getPropertyDatatype(
333
+ this.resolver.resolveAndRequire(action.returns, file)
334
+ )
323
335
  file.addAction(name.split('.').at(-1), params, returns)
324
336
  }
325
337
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cap-js/cds-typer",
3
- "version": "0.11.0",
3
+ "version": "0.12.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",
@@ -17,7 +17,7 @@
17
17
  "test:integration": "jest --projects test/int.jest.config.js",
18
18
  "test:all": "jest",
19
19
  "test": "npm run test:unit",
20
- "lint": "eslint",
20
+ "lint": "npx eslint .",
21
21
  "cli": "node lib/cli.js",
22
22
  "doc:clean": "rm -rf ./doc",
23
23
  "doc:prepare": "npm run doc:clean && mkdir -p doc/types",
@@ -40,10 +40,9 @@
40
40
  "@sap/cds": ">=6"
41
41
  },
42
42
  "devDependencies": {
43
+ "@babel/eslint-parser": "^7.23.3",
43
44
  "acorn": "^8.10.0",
44
45
  "eslint": "^8.15.0",
45
- "eslint-config-prettier": "^8.5.0",
46
- "eslint-plugin-prettier": "^4.0.0",
47
46
  "jest": "^29",
48
47
  "typescript": ">=4.6.4"
49
48
  },