From 382a80617c18d14cd6b56e4ab9131009386c9614 Mon Sep 17 00:00:00 2001 From: Daniel O'Grady <103028279+daogrady@users.noreply.github.com> Date: Thu, 23 Nov 2023 23:53:22 +0100 Subject: [PATCH] Feat/nullable cds fields (#111) * Add "null" to compiled TS definitions * Improve getPropertyDatatype * Implement "not null" logic for CDS Asso/Compo * Refactor getPropertyDatatype * Fix nullable parameters/returns for CDS functions/actions * Fix repeated `| not null` for CDS Asso/Compo * Reset tests and adjust to not-null * Add test suite for not null * Add changelog entry --------- Co-authored-by: Siarhei Murkou Co-authored-by: Siarhei Murkou --- .gitignore | 1 + CHANGELOG.md | 2 + lib/components/inline.js | 17 +++++-- lib/components/resolver.js | 10 ++++ lib/visitor.js | 15 ++++-- test/ast.js | 7 ++- test/unit/actions.test.js | 38 ++++++++++------ test/unit/enum.test.js | 8 ++-- test/unit/events.test.js | 6 +-- test/unit/files/arrayof/model.cds | 2 +- test/unit/files/enums/model.cds | 2 +- test/unit/files/notnull/model.cds | 19 ++++++++ test/unit/files/typeof/model.cds | 2 +- test/unit/hana.test.js | 28 ++++++------ test/unit/inline.test.js | 40 ++++++++-------- test/unit/notnull.test.js | 76 +++++++++++++++++++++++++++++++ test/unit/output.test.js | 68 +++++++++++++-------------- test/unit/references.test.js | 54 ++++++++++------------ test/unit/typeof.test.js | 14 +++--- 19 files changed, 271 insertions(+), 138 deletions(-) create mode 100644 test/unit/files/notnull/model.cds create mode 100644 test/unit/notnull.test.js diff --git a/.gitignore b/.gitignore index 3314c81f..ee30dffc 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ coverage test/**/output test/**/cloud-cap-samples .DS_Store +.idea/ # Logs logs diff --git a/CHANGELOG.md b/CHANGELOG.md index f4088df1..0e85d3d0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,8 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/). - Generate `cds.LargeBinary` as string, buffer, _or readable_ in the case of media content ### Added +- Added support for the `not null` modifier + ### Fixed - Now using names of enum values in generated _index.js_ files if no explicit value is present diff --git a/lib/components/inline.js b/lib/components/inline.js index 571e69be..546410a7 100644 --- a/lib/components/inline.js +++ b/lib/components/inline.js @@ -85,6 +85,17 @@ class InlineDeclarationResolver { return this.visitor.options.propertiesOptional ? '?:' : ':' } + /** + * It returns TypeScript datatype for provided TS property + * @param {{typeName: string, typeInfo: TypeResolveInfo & { inflection: Inflection } }} type + * @param {string} typeName name of the TypeScript property + * @return {string} the datatype to be presented on TypeScript layer + * @public + */ + getPropertyDatatype(type, typeName = type.typeName) { + return type.typeInfo.isNotNull ? typeName : `${typeName} | null` + } + /** @param {import('../visitor').Visitor} visitor */ constructor(visitor) { this.visitor = visitor @@ -144,7 +155,7 @@ class FlatInlineDeclarationResolver extends InlineDeclarationResolver { flatten(prefix, type) { return type.typeInfo.structuredType ? Object.entries(type.typeInfo.structuredType).map(([k,v]) => this.flatten(`${this.prefix(prefix)}${k}`, v)) - : [`${prefix}${this.getPropertyTypeSeparator()} ${type.typeName}`] + : [`${prefix}${this.getPropertyTypeSeparator()} ${this.getPropertyDatatype(type)}`] } printInlineType(name, type, buffer) { @@ -194,9 +205,9 @@ class StructuredInlineDeclarationResolver extends InlineDeclarationResolver { this.flatten(n, t, buffer) } buffer.outdent() - buffer.add(`}${lineEnding}`) + buffer.add(`}${this.getPropertyDatatype(type, '')}${lineEnding}`) } else { - buffer.add(`${name}${this.getPropertyTypeSeparator()} ${type.typeName}${lineEnding}`) + buffer.add(`${name}${this.getPropertyTypeSeparator()} ${this.getPropertyDatatype(type)}${lineEnding}`) } this.printDepth-- return buffer diff --git a/lib/components/resolver.js b/lib/components/resolver.js index 880490ee..b0f12ef1 100644 --- a/lib/components/resolver.js +++ b/lib/components/resolver.js @@ -23,6 +23,7 @@ const { isInlineEnumType, propertyToAnonymousEnumName } = require('./enum') * ``` * @typedef {{ * isBuiltin: boolean, + * isNotNull: boolean, * isInlineDeclaration: boolean, * isForeignKeyReference: boolean, * isArray: boolean, @@ -232,6 +233,8 @@ class Resolver { if (toOne && toMany) { const target = element.items ?? (typeof element.target === 'string' ? { type: element.target } : element.target) + /** set `notNull = true` to avoid repeated `| not null` TS construction */ + target.notNull = true const { singular, plural } = this.resolveAndRequire(target, file).typeInfo.inflection typeName = cardinality > 1 ? toMany(plural) : toOne(this.visitor.isSelfReference(target) ? 'this' : singular) @@ -349,11 +352,16 @@ class Resolver { return element } + const cardinality = this.getMaxCardinality(element) + const result = { isBuiltin: false, // will be rectified in the corresponding handlers, if needed isInlineDeclaration: false, isForeignKeyReference: false, isArray: false, + isNotNull: element?.isRefNotNull !== undefined + ? element?.isRefNotNull + : element?.key || element?.notNull || cardinality > 1, } if (element?.type === undefined) { @@ -378,6 +386,8 @@ class Resolver { // objects and arrays if (element?.items) { result.isArray = true + // TODO: re-implement this line once {element.notNull} will be provided for array-like elements + result.isNotNull = true result.isBuiltin = true this.resolveType(element.items, file) //delete element.items diff --git a/lib/visitor.js b/lib/visitor.js index aa180d0f..9079d8ce 100644 --- a/lib/visitor.js +++ b/lib/visitor.js @@ -165,7 +165,10 @@ class Visitor { // We don't really have to care for this case, as keys from such structs are _not_ propagated to // the containing entity. for (const [kname, kelement] of Object.entries(this.csn.xtended.definitions[element.target]?.keys ?? {})) { - this.visitElement(`${ename}_${kname}`, kelement, file, buffer) + if (this.resolver.getMaxCardinality(element) === 1) { + kelement.isRefNotNull = !!element.notNull || !!element.key + this.visitElement(`${ename}_${kname}`, kelement, file, buffer) + } } } @@ -304,7 +307,7 @@ class Visitor { .filter(([, type]) => type?.type !== '$self' && !(type.items?.type === '$self')) .map(([name, type]) => [ name, - this.resolver.resolveAndRequire(type, file).typeName, + this.resolver.visitor.inlineDeclarationResolver.getPropertyDatatype(this.resolver.resolveAndRequire(type, file)), ]) : [] } @@ -315,7 +318,9 @@ class Visitor { const ns = this.resolver.resolveNamespace(name.split('.')) const file = this.getNamespaceFile(ns) const params = this.#stringifyFunctionParams(func.params, file) - const returns = this.resolver.resolveAndRequire(func.returns, file).typeName + const returns = this.resolver.visitor.inlineDeclarationResolver.getPropertyDatatype( + this.resolver.resolveAndRequire(func.returns, file) + ) file.addFunction(name.split('.').at(-1), params, returns) } @@ -324,7 +329,9 @@ class Visitor { const ns = this.resolver.resolveNamespace(name.split('.')) const file = this.getNamespaceFile(ns) const params = this.#stringifyFunctionParams(action.params, file) - const returns = this.resolver.resolveAndRequire(action.returns, file).typeName + const returns = this.resolver.visitor.inlineDeclarationResolver.getPropertyDatatype( + this.resolver.resolveAndRequire(action.returns, file) + ) file.addAction(name.split('.').at(-1), params, returns) } diff --git a/test/ast.js b/test/ast.js index 894301c0..15cbb3f7 100644 --- a/test/ast.js +++ b/test/ast.js @@ -407,7 +407,12 @@ const check = { isString: node => checkKeyword(node, 'string'), isNumber: node => checkKeyword(node, 'number'), isAny: node => checkKeyword(node, 'any'), - isStatic: node => checkKeyword(node, 'static') + isStatic: node => checkKeyword(node, 'static'), + isIndexedAccessType: node => checkKeyword(node, 'indexedaccesstype'), + isNull: node => checkKeyword(node, 'literaltype') && checkKeyword(node.literal, 'null'), + isUnionType: (node, of = []) => checkKeyword(node, 'uniontype') + && of.reduce((acc, predicate) => acc && node.subtypes.some(st => predicate(st)), true), + isNullable: (node, of = []) => check.isUnionType(node, of.concat([check.isNull])), } diff --git a/test/unit/actions.test.js b/test/unit/actions.test.js index fd9bc559..95f87490 100644 --- a/test/unit/actions.test.js +++ b/test/unit/actions.test.js @@ -16,13 +16,15 @@ describe('Actions', () => { const actions = astw.getAspectProperty('_EAspect', 'actions') expect(actions.modifiers.some(check.isStatic)).toBeTruthy() checkFunction(actions.type.members.find(fn => fn.name === 'f'), { - parameterCheck: ({members: [fst]}) => fst.name === 'x' && check.isString(fst.type) + parameterCheck: ({members: [fst]}) => fst.name === 'x' && check.isNullable(fst.type, [check.isString]) }) checkFunction(actions.type.members.find(fn => fn.name === 'g'), { parameterCheck: ({members: [fst, snd]}) => { - const fstCorrect = fst.name === 'a' && fst.type.members[0].name === 'x' && check.isNumber(fst.type.members[0].type) - && fst.type.members[1].name === 'y' && check.isNumber(fst.type.members[1].type) - const sndCorrect = snd.name === 'b' && check.isNumber(snd.type) + const fstCorrect = fst.name === 'a' + && fst.type.subtypes[0].members[0].name === 'x' + && check.isNullable(fst.type.subtypes[0].members[0].type, [check.isNumber]) + && fst.type.subtypes[0].members[1].name === 'y' && check.isNullable(fst.type.subtypes[0].members[1].type, [check.isNumber]) + const sndCorrect = snd.name === 'b' && check.isNullable(snd.type, [check.isNumber]) return fstCorrect && sndCorrect } }) @@ -33,11 +35,17 @@ describe('Actions', () => { const ast = new ASTWrapper(path.join(paths[2], 'index.ts')).tree checkFunction(ast.find(node => node.name === 'free'), { modifiersCheck: (modifiers = []) => !modifiers.some(check.isStatic), - callCheck: ({members: [fst, snd]}) => fst.name === 'a' && check.isNumber(fst.type) - && snd.name === 'b' && check.isString(snd.type), - parameterCheck: ({members: [fst]}) => fst.name === 'param' && check.isString(fst.type), - returnTypeCheck: ({members: [fst, snd]}) => fst.name === 'a' && check.isNumber(fst.type) - && snd.name === 'b' && check.isString(snd.type) + callCheck: type => check.isNullable(type) + && type.subtypes[0].members[0].name === 'a' + && check.isNullable(type.subtypes[0].members[0].type, [check.isNumber]) + && type.subtypes[0].members[1].name === 'b' + && check.isNullable(type.subtypes[0].members[1].type, [check.isString]), + parameterCheck: ({members: [fst]}) => fst.name === 'param' && check.isNullable(fst.type, [check.isString]), + returnTypeCheck: type => check.isNullable(type) + && type.subtypes[0].members[0].name === 'a' + && check.isNullable(type.subtypes[0].members[0].type, [check.isNumber]) + && type.subtypes[0].members[1].name === 'b' + && check.isNullable(type.subtypes[0].members[1].type, [check.isString]), }) }) @@ -48,7 +56,7 @@ describe('Actions', () => { expect(actions.modifiers.some(check.isStatic)).toBeTruthy() checkFunction(actions.type.members.find(fn => fn.name === 'f'), { callCheck: signature => check.isAny(signature), - parameterCheck: ({members: [fst]}) => fst.name === 'x' && check.isString(fst.type), + parameterCheck: ({members: [fst]}) => fst.name === 'x' && check.isNullable(fst.type, [check.isString]), returnTypeCheck: returns => check.isAny(returns) }) @@ -69,14 +77,14 @@ describe('Actions', () => { checkFunction(ast.find(node => node.name === 'free2'), { modifiersCheck: (modifiers = []) => !modifiers.some(check.isStatic), - callCheck: ({full}) => full === '_elsewhere.ExternalType', - returnTypeCheck: ({full}) => full === '_elsewhere.ExternalType' + callCheck: type => check.isNullable(type, [t => t?.full === '_elsewhere.ExternalType']), + returnTypeCheck: type => check.isNullable(type, [t => t?.full === '_elsewhere.ExternalType']) }) checkFunction(ast.find(node => node.name === 'free3'), { modifiersCheck: (modifiers = []) => !modifiers.some(check.isStatic), - callCheck: ({full}) => full === '_.ExternalInRoot', - returnTypeCheck: ({full}) => full === '_.ExternalInRoot' + callCheck: type => check.isNullable(type, [t => t?.full === '_.ExternalInRoot']), + returnTypeCheck: type => check.isNullable(type, [t => t?.full === '_.ExternalInRoot']) }) }) @@ -101,7 +109,7 @@ describe('Actions', () => { checkFunction(actions.type.members.find(fn => fn.name === 'sx'), { callCheck: signature => check.isAny(signature), returnTypeCheck: returns => check.isAny(returns), - parameterCheck: ({members: [fst]}) => check.isNumber(fst.type) + parameterCheck: ({members: [fst]}) => check.isNullable(fst.type, [check.isNumber]) }) }) diff --git a/test/unit/enum.test.js b/test/unit/enum.test.js index d0aa5f63..db7c2ea0 100644 --- a/test/unit/enum.test.js +++ b/test/unit/enum.test.js @@ -3,7 +3,7 @@ const fs = require('fs').promises const path = require('path') const cds2ts = require('../../lib/compile') -const { ASTWrapper } = require('../ast') +const { ASTWrapper, check } = require('../ast') const { locations } = require('../util') const dir = locations.testOutput('enums_test') @@ -31,7 +31,7 @@ describe('Enum Types', () => { test('Referring Property', async () => expect(ast.getAspects().find(({name, members}) => name === '_InlineEnumAspect' - && members?.find(member => member.name === 'gender' && member.type?.full === 'InlineEnum_gender'))) + && members?.find(member => member.name === 'gender' && check.isNullable(member.type, [t => t?.full === 'InlineEnum_gender'])))) .toBeTruthy()) }) @@ -47,7 +47,7 @@ describe('Enum Types', () => { test('Referring Property', async () => expect(ast.getAspects().find(({name, members}) => name === '_InlineEnumAspect' - && members?.find(member => member.name === 'status' && member.type?.full === 'InlineEnum_status'))) + && members?.find(member => member.name === 'status' && check.isNullable(member.type, [t => t?.full === 'InlineEnum_status'])))) .toBeTruthy()) }) @@ -62,7 +62,7 @@ describe('Enum Types', () => { test('Referring Property', async () => expect(ast.getAspects().find(({name, members}) => name === '_InlineEnumAspect' - && members?.find(member => member.name === 'yesno' && member.type?.full === 'InlineEnum_yesno'))) + && members?.find(member => member.name === 'yesno' && check.isNullable(member.type, [t => t?.full === 'InlineEnum_yesno'])))) .toBeTruthy()) }) }) diff --git a/test/unit/events.test.js b/test/unit/events.test.js index d87f3acc..a120437e 100644 --- a/test/unit/events.test.js +++ b/test/unit/events.test.js @@ -3,7 +3,7 @@ const fs = require('fs').promises const path = require('path') const cds2ts = require('../../lib/compile') -const { ASTWrapper } = require('../ast') +const { ASTWrapper, check } = require('../ast') const { locations } = require('../util') const dir = locations.testOutput('events_test') @@ -25,8 +25,8 @@ describe('events', () => { test('Top Level Event', async () => { expect(ast.tree.find(cls => cls.name === 'Bar' && cls.members.length === 2 - && cls.members[0].name === 'id' && cls.members[0].type.keyword === 'number' - && cls.members[1].name === 'name' && cls.members[1].type.keyword === 'indexedaccesstype' + && cls.members[0].name === 'id' && check.isNullable(cls.members[0].type, [check.isNumber]) + && cls.members[1].name === 'name' && check.isNullable(cls.members[1].type, [check.isIndexedAccessType]) )).toBeTruthy() }) }) diff --git a/test/unit/files/arrayof/model.cds b/test/unit/files/arrayof/model.cds index 07318488..ad281f25 100644 --- a/test/unit/files/arrayof/model.cds +++ b/test/unit/files/arrayof/model.cds @@ -11,4 +11,4 @@ entity E { inlinez: array of { a: String; b: Integer; } } -function fn (xs: array of Integer) returns array of String; +function fn (xs: array of Integer) returns array of String; \ No newline at end of file diff --git a/test/unit/files/enums/model.cds b/test/unit/files/enums/model.cds index 0e9cf143..c25a2633 100644 --- a/test/unit/files/enums/model.cds +++ b/test/unit/files/enums/model.cds @@ -32,4 +32,4 @@ entity ExternalEnums { status : Status; gender: Gender; yesno: Truthy; -} +} \ No newline at end of file diff --git a/test/unit/files/notnull/model.cds b/test/unit/files/notnull/model.cds new file mode 100644 index 00000000..71ac62c0 --- /dev/null +++ b/test/unit/files/notnull/model.cds @@ -0,0 +1,19 @@ +namespace not_null; + +entity Foo {} + +entity E { + x: Integer not null; + foo_assoc: Association to Foo not null; + foos_assoc: Association to many Foo not null; + foo_comp: Composition of Foo not null; + foos_comp: Composition of many Foo not null; + inline: { + a: Integer not null + } not null +} + actions { + action f (x: String not null); + } + +action free (param: String not null) returns Integer not null; \ No newline at end of file diff --git a/test/unit/files/typeof/model.cds b/test/unit/files/typeof/model.cds index bb8d052b..bf02d9c8 100644 --- a/test/unit/files/typeof/model.cds +++ b/test/unit/files/typeof/model.cds @@ -13,4 +13,4 @@ entity Bar { ref_a: Foo:a; ref_b: Foo:b; ref_c: Foo:c.x; -}; +}; \ No newline at end of file diff --git a/test/unit/hana.test.js b/test/unit/hana.test.js index 834d05da..ea3ad6c9 100644 --- a/test/unit/hana.test.js +++ b/test/unit/hana.test.js @@ -3,7 +3,7 @@ const path = require('path') const cds2ts = require('../../lib/compile') const { locations } = require('../util') -const { ASTWrapper } = require('../ast') +const { ASTWrapper, check } = require('../ast') const dir = locations.testOutput('hana_files') @@ -33,18 +33,18 @@ describe('Builtin HANA Datatypes', () => { }) test('Types Correct', () => { - ast.exists('_EverythingAspect', 'bar', 'string') - ast.exists('_EverythingAspect', 'smallint', 'SMALLINT') - ast.exists('_EverythingAspect', 'tinyint', 'TINYINT') - ast.exists('_EverythingAspect', 'smalldecimal', 'SMALLDECIMAL') - ast.exists('_EverythingAspect', 'real', 'REAL') - ast.exists('_EverythingAspect', 'char', 'CHAR') - ast.exists('_EverythingAspect', 'nchar', 'NCHAR') - ast.exists('_EverythingAspect', 'varchar', 'VARCHAR') - ast.exists('_EverythingAspect', 'clob', 'CLOB') - ast.exists('_EverythingAspect', 'binary', 'BINARY') - ast.exists('_EverythingAspect', 'point', 'ST_POINT') - ast.exists('_EverythingAspect', 'geometry', 'ST_GEOMETRY') - ast.exists('_EverythingAspect', 'shorthand', 'REAL') + ast.exists('_EverythingAspect', 'bar', ({type}) => check.isNullable(type, [check.isString])) + ast.exists('_EverythingAspect', 'smallint', ({type}) => check.isNullable(type, [t => t.name === 'SMALLINT'])) + ast.exists('_EverythingAspect', 'tinyint', ({type}) => check.isNullable(type, [t => t.name === 'TINYINT'])) + ast.exists('_EverythingAspect', 'smalldecimal', ({type}) => check.isNullable(type, [t => t.name === 'SMALLDECIMAL'])) + ast.exists('_EverythingAspect', 'real', ({type}) => check.isNullable(type, [t => t.name === 'REAL'])) + ast.exists('_EverythingAspect', 'char', ({type}) => check.isNullable(type, [t => t.name === 'CHAR'])) + ast.exists('_EverythingAspect', 'nchar', ({type}) => check.isNullable(type, [t => t.name === 'NCHAR'])) + ast.exists('_EverythingAspect', 'varchar', ({type}) => check.isNullable(type, [t => t.name === 'VARCHAR'])) + ast.exists('_EverythingAspect', 'clob', ({type}) => check.isNullable(type, [t => t.name === 'CLOB'])) + ast.exists('_EverythingAspect', 'binary', ({type}) => check.isNullable(type, [t => t.name === 'BINARY'])) + ast.exists('_EverythingAspect', 'point', ({type}) => check.isNullable(type, [t => t.name === 'ST_POINT'])) + ast.exists('_EverythingAspect', 'geometry', ({type}) => check.isNullable(type, [t => t.name === 'ST_GEOMETRY'])) + ast.exists('_EverythingAspect', 'shorthand', ({type}) => check.isNullable(type, [t => t.name === 'REAL'])) }) }) \ No newline at end of file diff --git a/test/unit/inline.test.js b/test/unit/inline.test.js index 6f5ab8fb..d93582e0 100644 --- a/test/unit/inline.test.js +++ b/test/unit/inline.test.js @@ -3,10 +3,11 @@ const fs = require('fs').promises const path = require('path') const cds2ts = require('../../lib/compile') -const { ASTWrapper } = require('../ast') +const { ASTWrapper, check } = require('../ast') const { locations } = require('../util') const dir = locations.testOutput('inline_test') +console.log(check) // compilation produces semantically complete Typescript describe('Inline Type Declarations', () => { @@ -18,19 +19,22 @@ describe('Inline Type Declarations', () => { // eslint-disable-next-line no-console .catch((err) => console.error(err)) const ast = new ASTWrapper(path.join(paths[1], 'index.ts')) - expect(ast.exists('_BarAspect', 'x', - m => m.name === 'x' - && m.type.members.length === 2 - && m.type.members[0].name === 'a' - && m.type.members[0].type.members.length === 2 - && m.type.members[0].type.members[0].name === 'b' - && m.type.members[0].type.members[0].type.keyword === 'number' - && m.type.members[0].type.members[1].name === 'c' - && m.type.members[0].type.members[1].type.nodeType === 'typeReference' - && m.type.members[0].type.members[1].type.args[0].full === 'Foo' - && m.type.members[1].name === 'y' - && m.type.members[1].type.keyword === 'string' - )).toBeTruthy() + expect(ast.exists('_BarAspect', 'x', ({name, type}) => { + const [nonNullType] = type.subtypes + const [a, y] = nonNullType.members + const [b, c] = a.type.subtypes[0].members + return name === 'x' + && check.isNullable(type) + && nonNullType.members.length === 2 + && a.name === 'a' + && check.isNullable(a.type) + && b.name === 'b' + && check.isNullable(b.type, [check.isNumber]) + && c.name === 'c' + && check.isNullable(c.type, [t => t.nodeType === 'typeReference' && t.args[0].full === 'Foo']) + && y.name === 'y' + && check.isNullable(y.type, [check.isString]) + })).toBeTruthy() }) test('Flat', async () => { @@ -39,10 +43,8 @@ describe('Inline Type Declarations', () => { // eslint-disable-next-line no-console .catch((err) => console.error(err)) const ast = new ASTWrapper(path.join(paths[1], 'index.ts')) - expect(ast.exists('_BarAspect', 'x_a_b', 'number')).toBeTruthy() - expect(ast.exists('_BarAspect', 'x_y', 'string')).toBeTruthy() - expect(ast.exists('_BarAspect', 'x_a_c', m => m.name === 'x_a_c' - && m.type.args[0].full === 'Foo' - )).toBeTruthy() + expect(ast.exists('_BarAspect', 'x_a_b', ({type}) => check.isNullable(type, [check.isNumber]))).toBeTruthy() + expect(ast.exists('_BarAspect', 'x_y', ({type}) => check.isNullable(type, [check.isString]))).toBeTruthy() + expect(ast.exists('_BarAspect', 'x_a_c', ({type}) => check.isNullable(type, [m => m.name === 'to' && m.args[0].full === 'Foo' ]))).toBeTruthy() }) }) \ No newline at end of file diff --git a/test/unit/notnull.test.js b/test/unit/notnull.test.js new file mode 100644 index 00000000..66c3a3ff --- /dev/null +++ b/test/unit/notnull.test.js @@ -0,0 +1,76 @@ +'use strict' + +const fs = require('fs').promises +const path = require('path') +const cds2ts = require('../../lib/compile') +const { ASTWrapper, check, checkFunction } = require('../ast') +const { locations } = require('../util') + +const dir = locations.testOutput('not_null_test') + + +describe('Not Null', () => { + let astw + + beforeEach(async () => await fs.unlink(dir).catch(() => {})) + beforeAll(async () => { + const paths = await cds2ts + .compileFromFile(locations.unit.files('notnull/model.cds'), { outputDirectory: dir, inlineDeclarations: 'structured' }) + astw = new ASTWrapper(path.join(paths[1], 'index.ts')) + }) + + describe('Properties', () => { + test('Primitive', async () => + expect(astw.getAspects().find(({name, members}) => name === '_EAspect' + && members?.find(member => member.name === 'x' && !check.isNullable(member.type) && check.isNumber(member.type)))) + .toBeTruthy()) + + test('Association to One', async () => + expect(astw.getAspects().find(({name, members}) => name === '_EAspect' + && members?.find(member => member.name === 'foo_assoc' && !check.isNullable(member.type)))) + .toBeTruthy()) + + test('Association to Many', async () => + expect(astw.getAspects().find(({name, members}) => name === '_EAspect' + && members?.find(member => member.name === 'foos_assoc' && !check.isNullable(member.type)))) + .toBeTruthy()) + + test('Composition of One', async () => + expect(astw.getAspects().find(({name, members}) => name === '_EAspect' + && members?.find(member => member.name === 'foo_comp' && !check.isNullable(member.type)))) + .toBeTruthy()) + + test('Composition of Many', async () => + expect(astw.getAspects().find(({name, members}) => name === '_EAspect' + && members?.find(member => member.name === 'foos_comp' && !check.isNullable(member.type)))) + .toBeTruthy()) + + test('Inline', async () => + expect(astw.getAspects().find(({name, members}) => name === '_EAspect' + && members?.find(member => member.name === 'inline' + && !check.isNullable(member.type) + && !check.isNullable(member.type.members[0])))) + .toBeTruthy()) + }) + + + describe('Actions', () => { + test('Bound', async () => { + const actions = astw.getAspectProperty('_EAspect', 'actions') + checkFunction(actions.type.members.find(fn => fn.name === 'f'), { + parameterCheck: ({members: [fst]}) => fst.name === 'x' && !check.isNullable(fst.type) + }) + }) + + test('Unbound', async () => { + checkFunction(astw.tree.find(node => node.name === 'free'), { + callCheck: type => !check.isNullable(type), + parameterCheck: ({members: [fst]}) => fst.name === 'param' && !check.isNullable(fst.type), + returnTypeCheck: type => !check.isNullable(type) + }) + }) + }) + + + +}) diff --git a/test/unit/output.test.js b/test/unit/output.test.js index f146f378..9dc767f5 100644 --- a/test/unit/output.test.js +++ b/test/unit/output.test.js @@ -3,7 +3,7 @@ const fs = require('fs').promises const path = require('path') const cds2ts = require('../../lib/compile') -const { ASTWrapper, JSASTWrapper } = require('../ast') +const { ASTWrapper, JSASTWrapper, check } = require('../ast') const { locations } = require('../util') const dir = locations.testOutput('output_test') @@ -127,25 +127,25 @@ describe('Compilation', () => { }) test('Primitives', () => { - expect(ast.exists('_EAspect', 'uuid', m => m.type.keyword === 'string')).toBeTruthy() - expect(ast.exists('_EAspect', 'str', m => m.type.keyword === 'string')).toBeTruthy() - expect(ast.exists('_EAspect', 'bin', m => m.type.keyword === 'string')).toBeTruthy() - expect(ast.exists('_EAspect', 'lstr', m => m.type.keyword === 'string')).toBeTruthy() - expect(ast.exists('_EAspect', 'lbin', m => m.type.keyword === 'uniontype' - && m.type.subtypes[0].full === 'Buffer' - && m.type.subtypes[1].keyword === 'string' - )).toBeTruthy() - expect(ast.exists('_EAspect', 'integ', m => m.type.keyword === 'number')).toBeTruthy() - expect(ast.exists('_EAspect', 'uint8', m => m.type.keyword === 'number')).toBeTruthy() - expect(ast.exists('_EAspect', 'int16', m => m.type.keyword === 'number')).toBeTruthy() - expect(ast.exists('_EAspect', 'int32', m => m.type.keyword === 'number')).toBeTruthy() - expect(ast.exists('_EAspect', 'int64', m => m.type.keyword === 'number')).toBeTruthy() - expect(ast.exists('_EAspect', 'integer64', m => m.type.keyword === 'number')).toBeTruthy() - expect(ast.exists('_EAspect', 'dec', m => m.type.keyword === 'number')).toBeTruthy() - expect(ast.exists('_EAspect', 'doub', m => m.type.keyword === 'number')).toBeTruthy() - expect(ast.exists('_EAspect', 'd', m => m.type.keyword === 'string')).toBeTruthy() - expect(ast.exists('_EAspect', 'dt', m => m.type.keyword === 'string')).toBeTruthy() - expect(ast.exists('_EAspect', 'ts', m => m.type.keyword === 'string')).toBeTruthy() + expect(ast.exists('_EAspect', 'uuid', m => check.isNullable(m.type, [check.isString]))).toBeTruthy() + expect(ast.exists('_EAspect', 'str', m => check.isNullable(m.type, [check.isString]))).toBeTruthy() + expect(ast.exists('_EAspect', 'bin', m => check.isNullable(m.type, [check.isString]))).toBeTruthy() + expect(ast.exists('_EAspect', 'lstr', m => check.isNullable(m.type, [check.isString]))).toBeTruthy() + expect(ast.exists('_EAspect', 'lbin', m => check.isUnionType(m.type, [ + st => st.full === 'Buffer', + check.isString + ]))).toBeTruthy() + expect(ast.exists('_EAspect', 'integ', m => check.isNullable(m.type, [check.isNumber]))).toBeTruthy() + expect(ast.exists('_EAspect', 'uint8', m => check.isNullable(m.type, [check.isNumber]))).toBeTruthy() + expect(ast.exists('_EAspect', 'int16', m => check.isNullable(m.type, [check.isNumber]))).toBeTruthy() + expect(ast.exists('_EAspect', 'int32', m => check.isNullable(m.type, [check.isNumber]))).toBeTruthy() + expect(ast.exists('_EAspect', 'int64', m => check.isNullable(m.type, [check.isNumber]))).toBeTruthy() + expect(ast.exists('_EAspect', 'integer64', m => check.isNullable(m.type, [check.isNumber]))).toBeTruthy() + expect(ast.exists('_EAspect', 'dec', m => check.isNullable(m.type, [check.isNumber]))).toBeTruthy() + expect(ast.exists('_EAspect', 'doub', m => check.isNullable(m.type, [check.isNumber]))).toBeTruthy() + expect(ast.exists('_EAspect', 'd', m => check.isNullable(m.type, [check.isString]))).toBeTruthy() + expect(ast.exists('_EAspect', 'dt', m => check.isNullable(m.type, [check.isString]))).toBeTruthy() + expect(ast.exists('_EAspect', 'ts', m => check.isNullable(m.type, [check.isString]))).toBeTruthy() }) }) @@ -235,18 +235,16 @@ describe('Compilation', () => { }) test('Annotated Assoc/ Comp', () => { - expect(ast.exists('_RefererAspect', 'a', m => true - && m.type.name === 'to' - && m.type.args[0].name === 'BazSingular' - )).toBeTruthy() + expect(ast.exists('_RefererAspect', 'a', m => check.isNullable(m.type, [ + ({name, args}) => name === 'to' && args[0].name === 'BazSingular' + ]))).toBeTruthy() expect(ast.exists('_RefererAspect', 'b', m => true && m.type.name === 'many' && m.type.args[0].name === 'BazPlural' )).toBeTruthy() - expect(ast.exists('_RefererAspect', 'c', m => true - && m.type.name === 'of' - && m.type.args[0].name === 'BazSingular' - )).toBeTruthy() + expect(ast.exists('_RefererAspect', 'c', m => check.isNullable(m.type, [ + ({name, args}) => name === 'of' && args[0].name === 'BazSingular' + ]))).toBeTruthy() expect(ast.exists('_RefererAspect', 'd', m => true && m.type.name === 'many' && m.type.args[0].name === 'BazPlural' @@ -254,18 +252,16 @@ describe('Compilation', () => { }) test('Inferred Assoc/ Comp', () => { - expect(ast.exists('_RefererAspect', 'e', m => true - && m.type.name === 'to' - && m.type.args[0].name === 'Gizmo' - )).toBeTruthy() + expect(ast.exists('_RefererAspect', 'e', m => check.isNullable(m.type, [ + ({name, args}) => name === 'to' && args[0].name === 'Gizmo' + ]))).toBeTruthy() expect(ast.exists('_RefererAspect', 'f', m => true && m.type.name === 'many' && m.type.args[0].name === 'Gizmos' )).toBeTruthy() - expect(ast.exists('_RefererAspect', 'g', m => true - && m.type.name === 'of' - && m.type.args[0].name === 'Gizmo' - )).toBeTruthy() + expect(ast.exists('_RefererAspect', 'g', m => check.isNullable(m.type, [ + ({name, args}) => name === 'of' && args[0].name === 'Gizmo' + ]))).toBeTruthy() expect(ast.exists('_RefererAspect', 'h', m => true && m.type.name === 'many' && m.type.args[0].name === 'Gizmos' diff --git a/test/unit/references.test.js b/test/unit/references.test.js index b6dc232c..298cb748 100644 --- a/test/unit/references.test.js +++ b/test/unit/references.test.js @@ -3,7 +3,7 @@ const fs = require('fs').promises const path = require('path') const cds2ts = require('../../lib/compile') -const { ASTWrapper } = require('../ast') +const { ASTWrapper, check } = require('../ast') const { locations } = require('../util') const dir = locations.unit.files('output/references') @@ -16,31 +16,23 @@ describe('References', () => { const paths = await cds2ts .compileFromFile(locations.unit.files('references/model.cds'), { outputDirectory: dir, inlineDeclarations: 'structured' }) const ast = new ASTWrapper(path.join(paths[1], 'index.ts')) - expect(ast.exists('_BarAspect', 'assoc_one', m => true - && m.type.name === 'to' - && m.type.args[0].name === 'Foo' - )).toBeTruthy() + expect(ast.exists('_BarAspect', 'assoc_one', m => check.isNullable(m.type, [ + ({name, args}) => name === 'to' && args[0].name === 'Foo' + ]))).toBeTruthy() expect(ast.exists('_BarAspect', 'assoc_many', m => true && m.type.name === 'many' && m.type.args[0].name === 'Foo_' )).toBeTruthy() - expect(ast.exists('_BarAspect', 'comp_one', m => true - && m.type.name === 'of' - && m.type.args[0].name === 'Foo' - )).toBeTruthy() + expect(ast.exists('_BarAspect', 'comp_one', m => check.isNullable(m.type, [ + ({name, args}) => name === 'of' && args[0].name === 'Foo' + ]))).toBeTruthy() expect(ast.exists('_BarAspect', 'comp_many', m => true && m.type.name === 'many' && m.type.args[0].name === 'Foo_' )).toBeTruthy() - expect(ast.exists('_BarAspect', 'assoc_one_first_key', m => true - && m.type.keyword === 'string' - )).toBeTruthy() - expect(ast.exists('_BarAspect', 'assoc_one_second_key', m => true - && m.type.keyword === 'string' - )).toBeTruthy() - expect(ast.exists('_BarAspect', 'assoc_one_ID', m => true - && m.type.keyword === 'string' - )).toBeTruthy() + expect(ast.exists('_BarAspect', 'assoc_one_first_key', m => check.isNullable(m.type, [check.isString]))).toBeTruthy() + expect(ast.exists('_BarAspect', 'assoc_one_second_key', m => check.isNullable(m.type, [check.isString]))).toBeTruthy() + expect(ast.exists('_BarAspect', 'assoc_one_ID', m => check.isNullable(m.type, [check.isString]))).toBeTruthy() }) test('Inline', async () => { @@ -49,17 +41,21 @@ describe('References', () => { // eslint-disable-next-line no-console .catch((err) => console.error(err)) const ast = new ASTWrapper(path.join(paths[1], 'index.ts')) - expect(ast.exists('_BarAspect', 'inl_comp_one', m => true - && m.type.name === 'of' - && m.type.args[0].members[0].name === 'a' - && m.type.args[0].members[0].type.keyword === 'string' - )).toBeTruthy() - expect(ast.exists('_BarAspect', 'inl_comp_many', m => true - && m.type.name === 'many' - && m.type.args[0].name === 'Array' - && m.type.args[0].args[0].members[0].name === 'a' - && m.type.args[0].args[0].members[0].type.keyword === 'string' - )).toBeTruthy() + expect(ast.exists('_BarAspect', 'inl_comp_one', m => { + const comp = m.type.subtypes[0] + const [a] = comp.args[0].members + return check.isNullable(m.type) + && comp.name === 'of' + && a.name === 'a' + && check.isNullable(a.type, [check.isString]) + })).toBeTruthy() + expect(ast.exists('_BarAspect', 'inl_comp_many', m => { + const [arr] = m.type.args + return m.type.name === 'many' + && arr.name === 'Array' + && arr.args[0].members[0].name === 'a' + && check.isNullable(arr.args[0].members[0].type, [check.isString]) + })).toBeTruthy() // inline ID is not propagated into the parent entity expect(() => ast.exists('_BarAspect', 'inl_comp_one_ID')).toThrow(Error) }) diff --git a/test/unit/typeof.test.js b/test/unit/typeof.test.js index b5392f98..e2695279 100644 --- a/test/unit/typeof.test.js +++ b/test/unit/typeof.test.js @@ -3,7 +3,7 @@ const fs = require('fs').promises const path = require('path') const cds2ts = require('../../lib/compile') -const { ASTWrapper } = require('../ast') +const { ASTWrapper, check } = require('../ast') const { locations } = require('../util') const dir = locations.testOutput('typeof') @@ -19,15 +19,15 @@ describe('Typeof Syntax', () => { .catch((err) => console.error(err)) const ast = new ASTWrapper(path.join(paths[1], 'index.ts')) expect(ast.exists('_BarAspect', 'ref_a', - m => m.type.indexType.literal === 'a' + m => check.isNullable(m.type, [st => check.isIndexedAccessType(st) && st.indexType.literal === 'a']) )).toBeTruthy() expect(ast.exists('_BarAspect', 'ref_b', - m => m.type.indexType.literal === 'b' + m => check.isNullable(m.type, [st => check.isIndexedAccessType(st) && st.indexType.literal === 'b']) )).toBeTruthy() // meh, this is not exactly correct, as I apparently did not retrieve the chained type accesses properly, // but it's kinda good enough expect(ast.exists('_BarAspect', 'ref_c', - m => m.type.indexType.literal === 'x' + m => check.isNullable(m.type, [st => check.isIndexedAccessType(st) && st.indexType.literal === 'x']) )).toBeTruthy() }) @@ -38,13 +38,13 @@ describe('Typeof Syntax', () => { .catch((err) => console.error(err)) const ast = new ASTWrapper(path.join(paths[1], 'index.ts')) expect(ast.exists('_BarAspect', 'ref_a', - m => m.type.indexType.literal === 'a' + m => check.isNullable(m.type, [st => check.isIndexedAccessType(st) && st.indexType.literal === 'a']) )).toBeTruthy() expect(ast.exists('_BarAspect', 'ref_b', - m => m.type.indexType.literal === 'b' + m => check.isNullable(m.type, [st => check.isIndexedAccessType(st) && st.indexType.literal === 'b']) )).toBeTruthy() expect(ast.exists('_BarAspect', 'ref_c', - m => m.type.indexType.literal === 'c_x' + m => check.isNullable(m.type, [st => check.isIndexedAccessType(st) && st.indexType.literal === 'c_x']) )).toBeTruthy() /* && m.type.members.length === 2