@cap-js/cds-typer
Advanced tools
Comparing version 0.14.0 to 0.15.0
@@ -7,4 +7,15 @@ # Change Log | ||
## Version 0.15.0 - TBD | ||
## Version 0.16.0 - TBD | ||
## Version 0.15.0 - 2023-12-21 | ||
### Added | ||
- Support for [scoped entities](https://cap.cloud.sap/docs/cds/cdl#scoped-names) | ||
- Support for [delimited identifiers](https://cap.cloud.sap/docs/cds/cdl#delimited-identifiers) | ||
### Fixed | ||
- Inline enums are now available during runtime as well | ||
- Inline enums can now be used as action parameter types as well. These enums will not have a runtime representation, but will only assert type safety! | ||
- Arrays of inline enum values can now be used as action parameters too. But they will only be represented by their enclosing type for now, i.e. `string`, `number`, etc. | ||
- Foreign keys of projection entities are now propagated as well | ||
## Version 0.14.0 - 2023-12-13 | ||
@@ -15,3 +26,3 @@ ### Added | ||
## Version 0.13.0 - 2023-12-06 | ||
### Changes | ||
### Changed | ||
- Enums are now generated ecplicitly in the respective _index.js_ files and don't have to extract their values from the model at runtime anymore | ||
@@ -18,0 +29,0 @@ |
@@ -0,1 +1,3 @@ | ||
const { normalise } = require('./identifier') | ||
/** | ||
@@ -32,13 +34,29 @@ * Prints an enum to a buffer. To be precise, it prints | ||
buffer.indent() | ||
const vals = new Set() | ||
for (const [k, v] of kvs) { | ||
buffer.add(`${k}: ${v},`) | ||
vals.add(v?.val ?? v) // in case of wrapped vals we need to unwrap here for the type | ||
buffer.add(`${normalise(k)}: ${v},`) | ||
} | ||
buffer.outdent() | ||
buffer.add('} as const;') | ||
buffer.add(`${opts.export ? 'export ' : ''}type ${name} = ${[...vals].join(' | ')}`) | ||
buffer.add(`${opts.export ? 'export ' : ''}type ${name} = ${stringifyEnumType(kvs)}`) | ||
buffer.add('') | ||
} | ||
/** | ||
* Stringifies a list of enum key-value pairs into the righthand side of a TS type. | ||
* @param {[string, string][]} kvs list of key-value pairs | ||
* @returns {string} a stringified type | ||
* @example | ||
* ```js | ||
* ['A', 'B', 'A'] // -> '"A" | "B"' | ||
* ``` | ||
*/ | ||
const stringifyEnumType = kvs => [...uniqueValues(kvs)].join(' | ') | ||
/** | ||
* Extracts all unique values from a list of enum key-value pairs. | ||
* If the value is an object, then the `.val` property is used. | ||
* @param {[string, any | {val: any}][]} kvs | ||
*/ | ||
const uniqueValues = kvs => new Set(kvs.map(([,v]) => v?.val ?? v)) // in case of wrapped vals we need to unwrap here for the type | ||
// in case of strings, wrap in quotes and fallback to key to make sure values are attached for every key | ||
@@ -108,3 +126,4 @@ const enumVal = (key, value, enumType) => enumType === 'cds.String' ? JSON.stringify(`${value ?? key}`) : value | ||
*/ | ||
const stringifyEnumImplementation = (name, kvs) => `module.exports.${name} = { ${kvs.map(([k,v]) => `${k}: ${v}`).join(', ')} }` | ||
// ??= for inline enums. If there is some static property of that name, we don't want to override it (for example: ".actions" | ||
const stringifyEnumImplementation = (name, kvs) => `module.exports.${name} ??= { ${kvs.map(([k,v]) => `${normalise(k)}: ${v}`).join(', ')} }` | ||
@@ -117,3 +136,4 @@ | ||
isInlineEnumType, | ||
stringifyEnumImplementation | ||
stringifyEnumImplementation, | ||
stringifyEnumType | ||
} |
const { SourceFile, Buffer } = require('../file') | ||
const { normalise } = require('./identifier') | ||
const { docify } = require('./wrappers') | ||
@@ -11,3 +12,2 @@ | ||
class InlineDeclarationResolver { | ||
/** | ||
@@ -159,3 +159,3 @@ * @param {string} name | ||
? Object.entries(type.typeInfo.structuredType).map(([k,v]) => this.flatten(`${this.prefix(prefix)}${k}`, v)) | ||
: [`${prefix}${this.getPropertyTypeSeparator()} ${this.getPropertyDatatype(type)}`] | ||
: [`${normalise(prefix)}${this.getPropertyTypeSeparator()} ${this.getPropertyDatatype(type)}`] | ||
} | ||
@@ -203,3 +203,3 @@ | ||
if (type.typeInfo.structuredType) { | ||
const prefix = name ? `${name}${this.getPropertyTypeSeparator()}`: '' | ||
const prefix = name ? `${normalise(name)}${this.getPropertyTypeSeparator()}`: '' | ||
buffer.add(`${prefix} {`) | ||
@@ -213,3 +213,3 @@ buffer.indent() | ||
} else { | ||
buffer.add(`${name}${this.getPropertyTypeSeparator()} ${this.getPropertyDatatype(type)}${lineEnding}`) | ||
buffer.add(`${normalise(name)}${this.getPropertyTypeSeparator()} ${this.getPropertyDatatype(type)}${lineEnding}`) | ||
} | ||
@@ -216,0 +216,0 @@ this.printDepth-- |
@@ -374,9 +374,22 @@ 'use strict' | ||
if (isInlineEnumType(element, this.csn)) { | ||
// we use the singular as the initial declaration of these enums takes place | ||
// while defining the singular class. Which therefore uses the singular over the plural name. | ||
const cleanEntityName = util.singular4(element.parent, true) | ||
const enumName = propertyToInlineEnumName(cleanEntityName, element.name) | ||
result.type = enumName | ||
result.plainName = enumName | ||
result.isInlineDeclaration = true | ||
// element.parent is only set if the enum is attached to an entity's property. | ||
// If it is missing then we are dealing with an inline parameter type of an action. | ||
if (element.parent) { | ||
result.isInlineDeclaration = true | ||
// we use the singular as the initial declaration of these enums takes place | ||
// while defining the singular class. Which therefore uses the singular over the plural name. | ||
const cleanEntityName = util.singular4(element.parent, true) | ||
const enumName = propertyToInlineEnumName(cleanEntityName, element.name) | ||
result.type = enumName | ||
result.plainName = enumName | ||
} else { | ||
// FIXME: this is the case where users have arrays of enums as action parameter type. | ||
// Instead of building the proper type (e.g. `'A' | 'B' | ...`, we are instead building | ||
// the encasing type (e.g. `string` here) | ||
// We should instead aim for a proper type, i.e. | ||
// this.#resolveInlineDeclarationType(element.enum, result, file) | ||
// or | ||
// stringifyEnumType(csnToEnumPairs(element)) | ||
this.#resolveTypeName(element.type, result) | ||
} | ||
} else { | ||
@@ -383,0 +396,0 @@ this.resolvePotentialReferenceType(element.type, result, file) |
@@ -243,2 +243,2 @@ const annotation = '@odata.draft.enabled' | ||
module.exports = { amendCSN, isView, isUnresolved } | ||
module.exports = { amendCSN, isView, isUnresolved, propagateForeignKeys } |
@@ -6,2 +6,3 @@ 'use strict' | ||
const { printEnum, propertyToInlineEnumName, stringifyEnumImplementation } = require('./components/enum') | ||
const { normalise } = require('./components/identifier') | ||
const path = require('path') | ||
@@ -155,5 +156,5 @@ | ||
static stringifyLambda({name, parameters=[], returns='any', initialiser, isStatic=false}) { | ||
const parameterTypes = parameters.map(([n, t]) => `${n}: ${t}`).join(', ') | ||
const parameterTypes = parameters.map(([n, t]) => `${normalise(n)}: ${t}`).join(', ') | ||
const callableSignature = `(${parameterTypes}): ${returns}` | ||
let prefix = name ? `${name}: `: '' | ||
let prefix = name ? `${normalise(name)}: `: '' | ||
if (prefix && isStatic) { | ||
@@ -273,3 +274,3 @@ prefix = `static ${prefix}` | ||
this.enums.data.push({ | ||
name: entityFqName, | ||
name: `${entityCleanName}.${propertyName}`, | ||
property: propertyName, | ||
@@ -381,7 +382,7 @@ kvs, | ||
this.inlineEnums.buffer.join(), // needs to be before classes | ||
namespaces.join(), | ||
this.aspects.join(), // needs to be before classes | ||
this.classes.join(), | ||
this.events.buffer.join(), | ||
this.actions.buffer.join() | ||
this.actions.buffer.join(), | ||
namespaces.join() // needs to be after classes for possible declaration merging | ||
].filter(Boolean).join('\n') | ||
@@ -388,0 +389,0 @@ } |
@@ -54,2 +54,3 @@ /* eslint-disable indent */ | ||
* @returns {string} the name without localisation syntax or untouched. | ||
* @deprecated we have dropped this feature altogether, users specify custom names via @singular/@plural now | ||
*/ | ||
@@ -56,0 +57,0 @@ const unlocalize = (name) => { |
@@ -5,3 +5,3 @@ 'use strict' | ||
const { amendCSN, isView, isUnresolved } = require('./csn') | ||
const { amendCSN, isView, isUnresolved, propagateForeignKeys } = require('./csn') | ||
// eslint-disable-next-line no-unused-vars | ||
@@ -13,3 +13,3 @@ const { SourceFile, baseDefinitions, Buffer } = require('./file') | ||
const { docify } = require('./components/wrappers') | ||
const { csnToEnumPairs, propertyToInlineEnumName, isInlineEnumType } = require('./components/enum') | ||
const { csnToEnumPairs, propertyToInlineEnumName, isInlineEnumType, stringifyEnumType } = require('./components/enum') | ||
@@ -68,2 +68,3 @@ /** @typedef {import('./file').File} File */ | ||
amendCSN(csn.xtended) | ||
propagateForeignKeys(csn.inferred) | ||
this.options = { ...defaults, ...options } | ||
@@ -137,2 +138,20 @@ this.logger = logger | ||
/** | ||
* Retrieves all the keys from an entity. | ||
* That is: all keys that are present in both inferred, as well as xtended flavour. | ||
* @returns {[string, object][]} array of key name and key element pairs | ||
*/ | ||
#keys(name) { | ||
// FIXME: this is actually pretty bad, as not only have to propagate keys through | ||
// both flavours of CSN (see constructor), but we are now also collecting them from | ||
// both flavours and deduplicating them. | ||
// xtended contains keys that have been inherited from parents | ||
// inferred contains keys from queried entities (thing `entity Foo as select from Bar`, where Bar has keys) | ||
// So we currently need them both. | ||
return Object.entries({ | ||
...this.csn.inferred.definitions[name]?.keys ?? {}, | ||
...this.csn.xtended.definitions[name]?.keys ?? {} | ||
}) | ||
} | ||
/** | ||
* Transforms an entity or CDS aspect into a JS aspect (aka mixin). | ||
@@ -175,3 +194,3 @@ * That is, for an element A we get: | ||
// the containing entity. | ||
for (const [kname, kelement] of Object.entries(this.csn.xtended.definitions[element.target]?.keys ?? {})) { | ||
for (const [kname, kelement] of this.#keys(element.target)) { | ||
if (this.resolver.getMaxCardinality(element) === 1) { | ||
@@ -255,7 +274,9 @@ kelement.isRefNotNull = !!element.notNull || !!element.key | ||
// If the user decides to pass a @plural annotation, that gets precedence over the regular name. | ||
let plural = util.unlocalize( | ||
this.resolver.trimNamespace(util.getPluralAnnotation(entity) ? util.plural4(entity, false) : name) | ||
) | ||
const singular = util.unlocalize(util.singular4(entity, true)) | ||
if (singular === plural) { | ||
let plural = this.resolver.trimNamespace(util.getPluralAnnotation(entity) ? util.plural4(entity, false) : name) | ||
const singular = this.resolver.trimNamespace(util.singular4(entity, true)) | ||
// trimNamespace does not properly detect scoped entities, like A.B where both A and B are | ||
// entities. So to see if we would run into a naming collision, we forcefully take the last | ||
// part of the name, so "A.B" and "A.Bs" just become "B" and "Bs" to be compared. | ||
// FIXME: put this in a util function | ||
if (singular.split('.').at(-1) === plural.split('.').at(-1)) { | ||
plural += '_' | ||
@@ -290,3 +311,3 @@ this.logger.warning( | ||
this.#aspectify(name, entity, file.classes, singular) | ||
this.#aspectify(name, entity, buffer, singular) | ||
@@ -321,3 +342,3 @@ // PLURAL | ||
name, | ||
this.resolver.visitor.inlineDeclarationResolver.getPropertyDatatype(this.resolver.resolveAndRequire(type, file)), | ||
this.#stringifyFunctionParamType(type, file) | ||
]) | ||
@@ -327,2 +348,8 @@ : [] | ||
#stringifyFunctionParamType(type, file) { | ||
return type.enum | ||
? stringifyEnumType(csnToEnumPairs(type)) | ||
: this.inlineDeclarationResolver.getPropertyDatatype(this.resolver.resolveAndRequire(type, file)) | ||
} | ||
#printFunction(name, func) { | ||
@@ -329,0 +356,0 @@ // FIXME: mostly duplicate of printAction -> reuse |
{ | ||
"name": "@cap-js/cds-typer", | ||
"version": "0.14.0", | ||
"version": "0.15.0", | ||
"description": "Generates .ts files for a CDS model to receive code completion in VS Code", | ||
@@ -5,0 +5,0 @@ "main": "index.js", |
127588
18
2561