diff --git a/CHANGELOG.md b/CHANGELOG.md index b98c730b..7fbfd909 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,9 @@ This project adheres to [Semantic Versioning](http://semver.org/). The format is based on [Keep a Changelog](http://keepachangelog.com/). ## Version 0.15.0 - TBD +### Added +- Support for [scoped entities](https://cap.cloud.sap/docs/cds/cdl#scoped-names) + ### Fixed - Inline enums are now available during runtime as well diff --git a/lib/util.js b/lib/util.js index f3522cea..078910b7 100644 --- a/lib/util.js +++ b/lib/util.js @@ -51,6 +51,7 @@ const getPluralAnnotation = (csn) => csn[annotations.plural.find(a => Object.has * unlocalize("{i18n>Foo}") -> "Foo" * @param {string} name the entity name (singular or plural). * @returns {string} the name without localisation syntax or untouched. + * @deprecated we have dropped this feature altogether, users specify custom names via @singular/@plural now */ const unlocalize = (name) => { const match = name.match(/\{i18n>(.*)\}/) diff --git a/lib/visitor.js b/lib/visitor.js index 4f7dea78..61fe2397 100644 --- a/lib/visitor.js +++ b/lib/visitor.js @@ -247,11 +247,13 @@ class Visitor { const file = this.getNamespaceFile(ns) // entities are expected to be in plural anyway, so we would favour the regular name. // 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 += '_' this.logger.warning( `Derived singular and plural forms for '${singular}' are the same. This usually happens when your CDS entities are named following singular flexion. Consider naming your entities in plural or providing '@singular:'/ '@plural:' annotations to have a clear distinction between the two. Plural form will be renamed to '${plural}' to avoid compilation errors within the output.` @@ -281,7 +283,7 @@ class Visitor { docify(entity.doc).forEach((d) => buffer.add(d)) } - this.#aspectify(name, entity, file.classes, singular) + this.#aspectify(name, entity, buffer, singular) // PLURAL if (plural.includes('.')) { diff --git a/test/ast.js b/test/ast.js index 89f23228..2e75015e 100644 --- a/test/ast.js +++ b/test/ast.js @@ -20,7 +20,8 @@ const kinds = { PropertyDeclaration: 'propertyDeclaration', Keyword: 'keyword', VariableStatement: 'variableStatement', - TypeAliasDeclaration: 'typeAliasDeclaration' + TypeAliasDeclaration: 'typeAliasDeclaration', + ModuleDeclaration: 'moduleDeclaration' } /* @@ -56,6 +57,7 @@ const visitors = [ // order in some cases important. For example, // ts.isStatement will be true for ImportDeclarations etc. // so it has to be added after more specific checks. + [ts.isModuleDeclaration, visitModuleDeclaration], [ts.isObjectLiteralExpression, visitObjectLiteralExpression], [ts.isClassDeclaration, visitClassDeclaration], [ts.isFunctionDeclaration, visitFunctionDeclaration], @@ -78,6 +80,19 @@ const visitors = [ [() => true, node => console.error(`unhandled node type: ${JSON.stringify(node, null, 2)}`)] ] +/** + * @typedef {{name: string, body: any[]}} ModuleDeclaration + * @param node {ts.ModuleDeclaration} + * @returns {ModuleDeclaration} + */ +function visitModuleDeclaration(node) { + return { + nodeType: kinds.ModuleDeclaration, + name: visit(node.name), + body: node.body.statements.map(visit) + } +} + /** * @typedef {{name: string, type: any[]}} TypeAliasDeclaration * @param node {ts.TypeAliasDeclaration} @@ -297,6 +312,17 @@ class ASTWrapper { .filter(n => n.nodeType === kinds.TypeAliasDeclaration) } + /** @returns {ModuleDeclaration[]} */ + getModuleDeclarations() { + return this.tree + .filter(n => n.nodeType === kinds.ModuleDeclaration) + } + + /** @returns {ModuleDeclaration | undefined} */ + getModuleDeclaration(name) { + return this.getModuleDeclarations().find(m => m.name === name) + } + // /** @returns {ClassDeclaration[]} */ // getSingularClassDeclarations() { // return this.getTopLevelClassDeclarations() diff --git a/test/unit/files/scoped/model.cds b/test/unit/files/scoped/model.cds new file mode 100644 index 00000000..2317389e --- /dev/null +++ b/test/unit/files/scoped/model.cds @@ -0,0 +1,14 @@ +namespace scoped; + +entity Name.Something { + key something: String; +} + +entity Name.SomethingElse { + key something: String; +} + +entity Name { + key something: String; + somethingElse: Association to Name.SomethingElse +} \ No newline at end of file diff --git a/test/unit/scoped.test.js b/test/unit/scoped.test.js new file mode 100644 index 00000000..2f7dedb4 --- /dev/null +++ b/test/unit/scoped.test.js @@ -0,0 +1,30 @@ +'use strict' + +const fs = require('fs').promises +const path = require('path') +const cds2ts = require('../../lib/compile') +const { ASTWrapper } = require('../ast') +const { locations } = require('../util') + +const dir = locations.testOutput('scoped_test') + +describe('Scoped Entities', () => { + let astw + + beforeEach(async () => await fs.unlink(dir).catch(() => {})) + beforeAll(async () => { + const paths = await cds2ts + .compileFromFile(locations.unit.files('scoped/model.cds'), { outputDirectory: dir, inlineDeclarations: 'structured' }) + astw = new ASTWrapper(path.join(paths[1], 'index.ts')) + }) + + test('Namespace Exists', () => expect(astw.getModuleDeclaration('Name')).toBeTruthy()) + test('Namespace Entity Exists', () => expect(astw.getAspect('_NameAspect')).toBeTruthy()) + + test('Entities Present Within Namespace', () => { + const namespace = astw.getModuleDeclaration('Name') + expect(namespace).toBeTruthy() + expect(namespace.body.find(e => e.name === 'Something')).toBeTruthy() + expect(namespace.body.find(e => e.name === 'Something_')).toBeTruthy() + }) +})