@cap-js/cds-typer 0.5.0 → 0.6.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,24 @@ 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.5.1 - TBD
7
+ ## Version 0.6.1 - TBD
8
+
9
+ ### Changed
10
+
11
+ ### Added
12
+
13
+ ### Fixed
14
+
15
+
16
+ ## Version 0.6.0 - 2023-08-07
17
+
18
+ ### Added
19
+ - Support for `event` syntax
20
+
21
+ ### Fixed
22
+ - Initialise bound actions with stubs to support `"strict":true` in _tsconfig.json_
23
+ - Add leading underscore to appease `noUnusedParameters` in strict tsconfigs
24
+ - No longer inflect `type` definitions when they are referenced within entities or other type definitions
8
25
 
9
26
  ## Version 0.5.0 - 2023-07-25
10
27
 
@@ -14,7 +31,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/).
14
31
  ### Added
15
32
  - Support for `array of` syntax
16
33
 
17
- ### Fixes
34
+ ### Fixed
18
35
  - Generate `string` type for date-related types in CDS definitions
19
36
  - Generate `Buffer | string` type for the CDS type `LargeBinary`
20
37
 
@@ -142,7 +142,7 @@ class FlatInlineDeclarationResolver extends InlineDeclarationResolver {
142
142
  flatten(prefix, type) {
143
143
  return type.typeInfo.structuredType
144
144
  ? Object.entries(type.typeInfo.structuredType).map(([k,v]) => this.flatten(`${this.prefix(prefix)}${k}`, v))
145
- : [`${prefix} ${this.getPropertyTypeSeparator()} ${type.typeName}`]
145
+ : [`${prefix}${this.getPropertyTypeSeparator()} ${type.typeName}`]
146
146
  }
147
147
 
148
148
  printInlineType(name, type, buffer) {
@@ -185,7 +185,7 @@ class StructuredInlineDeclarationResolver extends InlineDeclarationResolver {
185
185
  this.printDepth++
186
186
  const lineEnding = this.printDepth > 1 ? ',' : statementEnd
187
187
  if (type.typeInfo.structuredType) {
188
- const prefix = name ? `${name} ${this.getPropertyTypeSeparator()}`: ''
188
+ const prefix = name ? `${name}${this.getPropertyTypeSeparator()}`: ''
189
189
  buffer.add(`${prefix} {`)
190
190
  buffer.indent()
191
191
  for (const [n, t] of Object.entries(type.typeInfo.structuredType)) {
@@ -194,7 +194,7 @@ class StructuredInlineDeclarationResolver extends InlineDeclarationResolver {
194
194
  buffer.outdent()
195
195
  buffer.add(`}${lineEnding}`)
196
196
  } else {
197
- buffer.add(`${name} ${this.getPropertyTypeSeparator()} ${type.typeName}${lineEnding}`)
197
+ buffer.add(`${name}${this.getPropertyTypeSeparator()} ${type.typeName}${lineEnding}`)
198
198
  }
199
199
  this.printDepth--
200
200
  return buffer
@@ -116,7 +116,7 @@ class Resolver {
116
116
  let qualifier = parts.join('.')
117
117
  while (
118
118
  this.csn.definitions[qualifier] &&
119
- ['entity', 'type', 'aspect'].includes(this.csn.definitions[qualifier].kind)
119
+ ['entity', 'type', 'aspect', 'event'].includes(this.csn.definitions[qualifier].kind)
120
120
  ) {
121
121
  parts.pop()
122
122
  qualifier = parts.join('.')
@@ -131,6 +131,7 @@ class Resolver {
131
131
  * - explicit annotation by the user in the CSN
132
132
  * - implicitly derived inflection based on simple grammar rules
133
133
  * - collisions between singular and plural name (resolved by appending a '_' suffix)
134
+ * - type definitions, which are not inflected
134
135
  * - inline type definitions, which don't really have a linguistic plural,
135
136
  * but need to expressed as array type to be consumable by the likes of Composition.of.many<T>
136
137
  * @param {import('./resolver').TypeResolveInfo} typeInfo information about the type gathered so far.
@@ -138,6 +139,16 @@ class Resolver {
138
139
  * @returns {Inflection}
139
140
  */
140
141
  inflect(typeInfo, namespace) {
142
+ // TODO: handle builtins here as well?
143
+ // guard: types don't get inflected
144
+ if (typeInfo.csn?.kind === 'type') {
145
+ return {
146
+ singular: typeInfo.plainName,
147
+ plural: typeInfo.plainName,
148
+ typeName: typeInfo.plainName,
149
+ }
150
+ }
151
+
141
152
  let typeName
142
153
  let singular
143
154
  let plural
package/lib/file.js CHANGED
@@ -99,6 +99,8 @@ class SourceFile extends File {
99
99
  this.imports = {}
100
100
  /** @type {Buffer} */
101
101
  this.preamble = new Buffer()
102
+ /** @type {{ buffer: Buffer, fqs: {name: string, fq: string}[]}} */
103
+ this.events = { buffer: new Buffer(), fqs: []}
102
104
  /** @type {Buffer} */
103
105
  this.types = new Buffer()
104
106
  /** @type {{ buffer: Buffer, fqs: {name: string, fq: string}[]}} */
@@ -119,8 +121,24 @@ class SourceFile extends File {
119
121
  this.inflections = []
120
122
  }
121
123
 
122
- static stringifyLambda(name, parameters = [], returns = 'any') {
123
- return `${name}: (${parameters.map(([n, t]) => `${n}: ${t}`).join(', ')}) => ${returns}`
124
+ /**
125
+ * Stringifies a lambda expression.
126
+ * @param {{name: string, parameters: [string, string][], returns: string, initialiser: string}} - name, parameters, return type, and initialiser expression
127
+ * @returns {string} - the stringified lambda
128
+ * @example
129
+ * ```js
130
+ * stringifyLambda({parameters: [['p','T']]} // (p: T) => any
131
+ * stringifyLambda({name: 'f', parameters: [['p','T']]} // f: (p: T) => any
132
+ * stringifyLambda({name: 'f', parameters: [['p','T']], returns: 'number'} // f: (p: T) => number
133
+ * stringifyLambda({name: 'f', parameters: [['p','T']], returns: 'number', initialiser: '_ => 42'} // f: (p: T) => string = _ => 42
134
+ *
135
+ * ```
136
+ */
137
+ static stringifyLambda({name, parameters=[], returns='any', initialiser}) {
138
+ const signature = `(${parameters.map(([n, t]) => `${n}: ${t}`).join(', ')}) => ${returns}`
139
+ const prefix = name ? `${name}: `: ''
140
+ const suffix = initialiser ? ` = ${initialiser}` : ''
141
+ return prefix + signature + suffix
124
142
  }
125
143
 
126
144
  /**
@@ -139,27 +157,25 @@ class SourceFile extends File {
139
157
  /**
140
158
  * Adds a function definition in form of a arrow function to the file.
141
159
  * @param {string} name name of the function
142
- * @param {{relative: string | undefined, local: boolean, posix: boolean}} params list of parameters, passed as [name, type] pairs
160
+ * @param {{relative: string | undefined, local: boolean, posix: boolean}} parameters list of parameters, passed as [name, type] pairs
143
161
  * @param returns the return type of the function
144
162
  */
145
- addFunction(name, params, returns) {
163
+ addFunction(name, parameters, returns) {
146
164
  // FIXME: use different buffers for buffers and actions, or at least rename buffer to the more general category "functions"?
147
165
  this.actions.buffer.add("// function")
148
- this.actions.buffer.add(`export declare const ${SourceFile.stringifyLambda(name, params, returns)};`)
166
+ this.actions.buffer.add(`export declare const ${SourceFile.stringifyLambda({name, parameters, returns})};`)
149
167
  this.actions.names.push(name)
150
168
  }
151
169
 
152
170
  /**
153
171
  * Adds an action definition in form of a arrow function to the file.
154
172
  * @param {string} name name of the action
155
- * @param {{relative: string | undefined, local: boolean, posix: boolean}} params list of parameters, passed as [name, type] pairs
173
+ * @param {{relative: string | undefined, local: boolean, posix: boolean}} parameters list of parameters, passed as [name, type] pairs
156
174
  * @param returns the return type of the action
157
175
  */
158
- addAction(name, params, returns) {
159
- //const ps = params.map(([n, t]) => `${n}: ${t}`).join(', ')
176
+ addAction(name, parameters, returns) {
160
177
  this.actions.buffer.add("// action")
161
- //this.actions.buffer.add(`export declare const ${name}: ( args: { ${ps} }) => ${returns};`)
162
- this.actions.buffer.add(`export declare const ${SourceFile.stringifyLambda(name, params, returns)};`)
178
+ this.actions.buffer.add(`export declare const ${SourceFile.stringifyLambda({name, parameters, returns})};`)
163
179
  this.actions.names.push(name)
164
180
  }
165
181
 
@@ -196,6 +212,7 @@ class SourceFile extends File {
196
212
  // and a type containing all disctinct values.
197
213
  // We can get away with this as TS doesn't feature nominal typing, so the structure
198
214
  // is all we care about.
215
+ // FIXME: this really should be in visitor, as File should not contain logic of this kind
199
216
  this.enums.fqs.push({ name, fq })
200
217
  const bu = this.enums.buffer
201
218
  bu.add('// enum')
@@ -210,7 +227,6 @@ class SourceFile extends File {
210
227
  bu.add('}')
211
228
  bu.add(`export type ${name} = ${[...vals].join(' | ')}`)
212
229
  bu.add('')
213
-
214
230
  }
215
231
 
216
232
  /**
@@ -225,6 +241,16 @@ class SourceFile extends File {
225
241
  this.classNames[clean] = fq
226
242
  }
227
243
 
244
+ /**
245
+ * Adds an event to this file.
246
+ * are supposed to be present in this file.
247
+ * @param {string} name cleaned name of the event
248
+ * @param {string} fq fully qualified name, including the namespace
249
+ */
250
+ addEvent(name, fq) {
251
+ this.events.fqs.push({ name, fq })
252
+ }
253
+
228
254
  /**
229
255
  * Adds an import if it does not exist yet.
230
256
  * @param {Path} imp qualifier for the namespace to import.
@@ -289,6 +315,7 @@ class SourceFile extends File {
289
315
  namespaces.join(),
290
316
  this.aspects.join(), // needs to be before classes
291
317
  this.classes.join(),
318
+ this.events.buffer.join(),
292
319
  this.actions.buffer.join(),
293
320
  ].filter(Boolean).join('\n')
294
321
  }
@@ -311,6 +338,8 @@ class SourceFile extends File {
311
338
  `module.exports.${original} = csn.${original}`
312
339
  ])))
313
340
  ) // singular -> plural aliases
341
+ .concat(['// events'])
342
+ .concat(this.events.fqs.map(({fq, name}) => `module.exports.${name} = '${fq}'`))
314
343
  .concat(['// actions'])
315
344
  .concat(this.actions.names.map(name => `module.exports.${name} = '${name}'`))
316
345
  .concat(['// enums'])
@@ -473,7 +502,7 @@ export namespace Composition {
473
502
  }
474
503
 
475
504
  export class Entity {
476
- static data<T extends Entity> (this:T, input:Object) : T {
505
+ static data<T extends Entity> (this:T, _input:Object) : T {
477
506
  return {} as T // mock
478
507
  }
479
508
  }
package/lib/visitor.js CHANGED
@@ -132,17 +132,18 @@ class Visitor {
132
132
  this.visitElement(ename, element, file, buffer)
133
133
  }
134
134
  for (const [aname, action] of Object.entries(entity.actions ?? {})) {
135
+ const lambdaString =
135
136
  buffer.add(
136
- SourceFile.stringifyLambda(
137
- aname,
138
- Object.entries(action.params ?? {}).map(([n, t]) => [
137
+ SourceFile.stringifyLambda({
138
+ name: aname,
139
+ parameters: Object.entries(action.params ?? {}).map(([n, t]) => [
139
140
  n,
140
141
  this.resolver.resolveAndRequire(t, file).typeName,
141
142
  ]),
142
- action.returns ? this.resolver.resolveAndRequire(action.returns, file).typeName : 'any'
143
- )
143
+ returns: action.returns ? this.resolver.resolveAndRequire(action.returns, file).typeName : 'any',
144
+ initialiser: `undefined as unknown as this['${aname}']`
145
+ })
144
146
  )
145
- //this.visitEntity(aname, action, file, buffer)
146
147
  }
147
148
  buffer.outdent()
148
149
  buffer.add('};')
@@ -287,6 +288,26 @@ class Visitor {
287
288
  this._aspectify(name, aspect, file.aspects, clean)
288
289
  }
289
290
 
291
+ #printEvent(name, event) {
292
+ this.logger.debug(`Printing event ${name}`)
293
+ const clean = this.resolver.trimNamespace(name)
294
+ const ns = this.resolver.resolveNamespace(name.split('.'))
295
+ const file = this.getNamespaceFile(ns)
296
+ file.addEvent(clean, name)
297
+ const buffer = file.events.buffer
298
+ buffer.add('// event')
299
+ buffer.add(`export class ${clean} {`)
300
+ buffer.indent()
301
+ const propOpt = this.options.propertiesOptional
302
+ this.options.propertiesOptional = false
303
+ for (const [ename, element] of Object.entries(event.elements ?? {})) {
304
+ this.visitElement(ename, element, file, buffer)
305
+ }
306
+ this.options.propertiesOptional = propOpt
307
+ buffer.outdent()
308
+ buffer.add('}')
309
+ }
310
+
290
311
  /**
291
312
  * Visits a single entity from the CSN's definition field.
292
313
  * Will call #printEntity or #printAction based on the entity's kind.
@@ -310,6 +331,9 @@ class Visitor {
310
331
  case 'aspect':
311
332
  this.#printAspect(name, entity)
312
333
  break
334
+ case 'event':
335
+ this.#printEvent(name, entity)
336
+ break
313
337
  default:
314
338
  this.logger.error(`Unhandled entity kind '${entity.kind}'.`)
315
339
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cap-js/cds-typer",
3
- "version": "0.5.0",
3
+ "version": "0.6.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",