@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.
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 } = 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')
@@ -16,23 +16,17 @@ const { baseDefinitions } = require('./components/basedefs')
16
16
  const { EntityRepository, asIdentifier } = require('./resolution/entity')
17
17
  const { last } = require('./components/identifier')
18
18
  const { getPropertyModifiers } = require('./components/property')
19
+ const { configuration } = require('./config')
20
+ const { createMember } = require('./components/class')
19
21
 
20
22
  /** @typedef {import('./file').File} File */
21
23
  /** @typedef {import('./typedefs').visitor.Context} Context */
22
- /** @typedef {import('./typedefs').visitor.CompileParameters} CompileParameters */
23
- /** @typedef {import('./typedefs').visitor.VisitorOptions} VisitorOptions */
24
24
  /** @typedef {import('./typedefs').visitor.Inflection} Inflection */
25
25
  /** @typedef {import('./typedefs').resolver.CSN} CSN */
26
+ /** @typedef {import('./typedefs').resolver.TypeResolveOptions} TypeResolveOptions */
26
27
  /** @typedef {import('./typedefs').resolver.EntityCSN} EntityCSN */
27
28
  /** @typedef {import('./typedefs').resolver.EnumCSN} EnumCSN */
28
29
 
29
- const defaults = {
30
- // FIXME: add defaults for remaining parameters
31
- propertiesOptional: true,
32
- useEntitiesProxy: false,
33
- inlineDeclarations: 'flat'
34
- }
35
-
36
30
  class Visitor {
37
31
  /**
38
32
  * Gathers all files that are supposed to be written to
@@ -46,12 +40,12 @@ class Visitor {
46
40
 
47
41
  /**
48
42
  * @param {{xtended: CSN, inferred: CSN}} csn - root CSN
49
- * @param {VisitorOptions | {}} options - the options
50
43
  */
51
- constructor(csn, options = {}) {
52
- amendCSN(csn.xtended)
44
+ constructor(csn) {
45
+ propagateForeignKeys(csn.xtended)
53
46
  propagateForeignKeys(csn.inferred)
54
- this.options = { ...defaults, ...options }
47
+ // has to be executed on the inferred model as autoexposed entities are not included in the xtended csn
48
+ collectDraftEnabledEntities(csn.inferred)
55
49
  this.csn = csn
56
50
 
57
51
  /** @type {Context[]} **/
@@ -64,12 +58,10 @@ class Visitor {
64
58
  this.entityRepository = new EntityRepository(this.resolver)
65
59
 
66
60
  /** @type {FileRepository} */
67
- this.fileRepository = new FileRepository(this.options)
68
- // REVISIT: better way to pass options to base source file ???
69
- baseDefinitions.options = this.options
61
+ this.fileRepository = new FileRepository()
70
62
  this.fileRepository.add(baseDefinitions.path.asNamespace(), baseDefinitions)
71
63
  this.inlineDeclarationResolver =
72
- this.options.inlineDeclarations === 'structured'
64
+ configuration.inlineDeclarations === 'structured'
73
65
  ? new StructuredInlineDeclarationResolver(this)
74
66
  : new FlatInlineDeclarationResolver(this)
75
67
 
@@ -105,7 +97,13 @@ class Visitor {
105
97
  // FIXME: references to types of entity properties may be missing from xtendend flavour (see #103)
106
98
  // this should be revisted once we settle on a single flavour.
107
99
  const target = this.csn.xtended.definitions[targetName] ?? this.csn.inferred.definitions[targetName]
108
- this.visitEntity(name, target)
100
+ if (target.kind !== 'type') {
101
+ // skip if the target is a property, like in:
102
+ // books: Association to many Author.books ...
103
+ // as this would result in a type definition that
104
+ // name-clashes with the actual declaration of Author
105
+ this.visitEntity(name, target)
106
+ }
109
107
  } else {
110
108
  LOG.error(`Expecting an autoexposed projection within a service. Skipping ${name}`)
111
109
  }
@@ -145,21 +143,58 @@ class Visitor {
145
143
  : ''
146
144
  if (actions.length) {
147
145
  buffer.addIndentedBlock(`declare static readonly actions: ${inherited}{`,
148
- actions.map(([aname, action]) => 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
- })), '}'
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
+ }, '}'
157
159
  ) // end of actions
158
160
  } else {
159
- 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
+ }))
160
167
  }
161
168
  }
162
169
 
170
+ /**
171
+ * @param {Buffer} buffer - the buffer to write the keys into
172
+ * @param {string} clean - the clean name of the entity
173
+ */
174
+ #printStaticKeys(buffer, 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
+ }))
196
+ }
197
+
163
198
  /**
164
199
  * Transforms an entity or CDS aspect into a JS aspect (aka mixin).
165
200
  * That is, for an element A we get:
@@ -227,12 +262,15 @@ class Visitor {
227
262
  buffer.addIndentedBlock(`return class ${clean} extends ${ancestorsAspects} {`, () => {
228
263
  /** @type {import('./typedefs').resolver.EnumCSN[]} */
229
264
  const enums = []
230
- 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 ?? [])) {
231
269
  if (element.target && /\.texts?/.test(element.target)) {
232
270
  LOG.warn(`referring to .texts property in ${fq}. This is currently not supported and will be ignored.`)
233
271
  continue
234
272
  }
235
- this.visitElement(ename, element, file, buffer)
273
+ this.visitElement({name: ename, element, file, buffer, resolverOptions})
236
274
 
237
275
  // make foreign keys explicit
238
276
  if (element.target) {
@@ -248,7 +286,7 @@ class Visitor {
248
286
  const kelement = Object.assign(Object.create(originalKeyElement), {
249
287
  isRefNotNull: !!element.notNull || !!element.key
250
288
  })
251
- this.visitElement(foreignKey, kelement, file, buffer)
289
+ this.visitElement({name: foreignKey, element: kelement, file, buffer, resolverOptions})
252
290
  }
253
291
  }
254
292
  }
@@ -260,35 +298,49 @@ class Visitor {
260
298
  }
261
299
  }
262
300
 
263
- if ('kind' in entity) {
264
- 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)
265
310
  }
266
311
 
267
- buffer.addIndented(() => {
268
- for (const e of enums) {
269
- const eDoc = docify(e.doc)
270
- eDoc.forEach(d => { buffer.add(d) })
271
- buffer.add(`static ${e.name} = ${propertyToInlineEnumName(clean, e.name)}`)
272
- file.addInlineEnum(clean, fq, e.name, csnToEnumPairs(e, {unwrapVals: true}), eDoc)
273
- }
274
- this.#printStaticActions(entity, buffer, ancestorInfos, file)
275
- })
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)
276
326
  }, '};') // end of generated class
277
327
  }, '}') // end of aspect
278
328
 
279
329
  // CLASS WITH ADDED ASPECTS
280
330
  file.addImport(baseDefinitions.path)
281
331
  docify(entity.doc).forEach(d => { buffer.add(d) })
282
- 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')}}`)
283
333
  this.contexts.pop()
284
334
  }
285
335
 
286
336
  /**
337
+ * @param {string} fq - fully qualified name of the entity
287
338
  * @param {string} clean - the clean name of the entity
288
- * @param {EntityCSN} entity - the entity to generate the static contents for
339
+ * @param {boolean} [isPlural] - `true` if passed entity is plural
289
340
  */
290
- #staticClassContents(clean, entity) {
291
- 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)}`]
292
344
  }
293
345
 
294
346
  /**
@@ -344,9 +396,6 @@ class Visitor {
344
396
  ? this.csn.inferred.definitions[fq]
345
397
  : entity
346
398
 
347
- // 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
348
- target['@odata.draft.enabled'] = isDraftEnabled(entity)
349
-
350
399
  this.#aspectify(fq, target, buffer, { cleanName: singular })
351
400
 
352
401
  buffer.add(overrideNameProperty(singular, entity.name))
@@ -362,13 +411,13 @@ class Visitor {
362
411
  }
363
412
  // plural can not be a type alias to $singular[] but needs to be a proper class instead,
364
413
  // so it can get passed as value to CQL functions.
365
- const additionalProperties = this.#staticClassContents(singular, entity)
414
+ const additionalProperties = this.#staticClassContents(fq, singular, true)
366
415
  additionalProperties.push('$count?: number')
367
- docify(entity.doc).forEach(d => { buffer.add(d) })
416
+ buffer.add(docify(entity.doc))
368
417
  buffer.add(`export class ${plural} extends Array<${singular}> {${additionalProperties.join('\n')}}`)
369
418
  buffer.add(overrideNameProperty(plural, entity.name))
370
419
  }
371
- buffer.add('')
420
+ buffer.blankLine()
372
421
  }
373
422
 
374
423
  /**
@@ -488,12 +537,13 @@ class Visitor {
488
537
  buffer.add('// event')
489
538
  // only declare classes, as their properties are not optional, so we don't have to do awkward initialisation thereof.
490
539
  buffer.addIndentedBlock(`export declare class ${entityName} {`, () => {
491
- const propOpt = this.options.propertiesOptional
492
- this.options.propertiesOptional = false
540
+ const propOpt = configuration.propertiesOptional
541
+ // FIXME: shouldn't need to change config here! Idea: init Visitor with .options fed from config, then manipulate that
542
+ configuration.propertiesOptional = false
493
543
  for (const [ename, element] of Object.entries(event.elements ?? {})) {
494
- this.visitElement(ename, element, file, buffer)
544
+ this.visitElement({name: ename, element, file, buffer})
495
545
  }
496
- this.options.propertiesOptional = propOpt
546
+ configuration.propertiesOptional = propOpt
497
547
  }, '}')
498
548
  }
499
549
 
@@ -512,12 +562,12 @@ class Visitor {
512
562
  // file.addImport(new Path(['cds'], '')) TODO make sap/cds import work
513
563
  buffer.addIndentedBlock(`export class ${serviceNameSimple} extends cds.Service {`, () => {
514
564
  Object.entries(service.operations ?? {}).forEach(([name, {doc}]) => {
515
- docify(doc).forEach(d => { buffer.add(d) })
565
+ buffer.add(docify(doc))
516
566
  buffer.add(`declare ${name}: typeof ${name}`)
517
567
  })
518
568
  }, '}')
519
569
  buffer.add(`export default ${serviceNameSimple}`)
520
- buffer.add('')
570
+ buffer.blankLine()
521
571
  file.addService(service.name)
522
572
  }
523
573
 
@@ -580,13 +630,15 @@ class Visitor {
580
630
 
581
631
  /**
582
632
  * Visits a single element in an entity.
583
- * @param {string} name - name of the element
584
- * @param {EntityCSN} element - CSN data belonging to the the element.
585
- * @param {SourceFile} file - the namespace file the surrounding entity is being printed into.
586
- * @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
587
639
  * @returns @see InlineDeclarationResolver.visitElement
588
640
  */
589
- visitElement(name, element, file, buffer = file.classes) {
641
+ visitElement({name, element, file, buffer = file.classes, resolverOptions}) {
590
642
  return this.inlineDeclarationResolver.visitElement({
591
643
  name,
592
644
  element,
@@ -594,7 +646,8 @@ class Visitor {
594
646
  buffer,
595
647
  // we explicitly pass the "declare" modifier here to avoid problems with noImplicitOverride and useDefineForClassFields in strict tsconfigs
596
648
  // but not inside type defs (e.g. parameter types) where this would be a syntax error
597
- modifiers: getPropertyModifiers(element)
649
+ modifiers: getPropertyModifiers(element),
650
+ resolverOptions
598
651
  })
599
652
  }
600
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.26.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
  }