Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Generate View Types #123

Merged
merged 7 commits into from
Dec 13, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,11 @@ All notable changes to this project will be documented in this file.
This project adheres to [Semantic Versioning](http://semver.org/).
The format is based on [Keep a Changelog](http://keepachangelog.com/).

## Version 0.14.0 - TBD
## Version 0.15.0 - TBD

## Version 0.14.0 - 2023-12-13
### Added
- Entities that are database views now also receive typings

## Version 0.13.0 - 2023-12-06
### Changes
Expand Down
16 changes: 15 additions & 1 deletion lib/csn.js
Original file line number Diff line number Diff line change
Expand Up @@ -226,4 +226,18 @@ function amendCSN(csn) {
propagateForeignKeys(csn)
}

module.exports = { amendCSN }
/**
* FIXME: this is pretty handwavey: we are looking for view-entities,
* i.e. ones that have a query, but are not a cds level projection.
* Those are still not expanded and we have to retrieve their definition
* with all properties from the inferred model.
*/
const isView = entity => entity.query && !entity.projection

/**
* @see isView
* Unresolved entities have to be looked up from inferred csn.
*/
const isUnresolved = entity => entity._unresolved === true

module.exports = { amendCSN, isView, isUnresolved }
10 changes: 6 additions & 4 deletions lib/visitor.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

const util = require('./util')

const { amendCSN } = require('./csn')
const { amendCSN, isView, isUnresolved } = require('./csn')
// eslint-disable-next-line no-unused-vars
const { SourceFile, baseDefinitions, Buffer } = require('./file')
const { FlatInlineDeclarationResolver, StructuredInlineDeclarationResolver } = require('./components/inline')
Expand Down Expand Up @@ -99,10 +99,12 @@ class Visitor {
*/
visitDefinitions() {
for (const [name, entity] of Object.entries(this.csn.xtended.definitions)) {
if (entity._unresolved === true) {
this.logger.error(`Skipping unresolved entity: ${JSON.stringify(entity)}`)
} else {
if (isView(entity)) {
this.visitEntity(name, this.csn.inferred.definitions[name])
} else if (!isUnresolved(entity)) {
this.visitEntity(name, entity)
} else {
this.logger.warning(`Skipping unresolved entity: ${name}`)
}
}
// FIXME: optimise
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@cap-js/cds-typer",
"version": "0.13.0",
"version": "0.14.0",
"description": "Generates .ts files for a CDS model to receive code completion in VS Code",
"main": "index.js",
"repository": "github:cap-js/cds-typer",
Expand Down
1 change: 1 addition & 0 deletions test/ast.js
Original file line number Diff line number Diff line change
Expand Up @@ -410,6 +410,7 @@ const checkKeyword = (node, expected) => node?.keyword === expected
const check = {
isString: node => checkKeyword(node, 'string'),
isNumber: node => checkKeyword(node, 'number'),
isBoolean: node => checkKeyword(node, 'boolean'),
isAny: node => checkKeyword(node, 'any'),
isStatic: node => checkKeyword(node, 'static'),
isIndexedAccessType: node => checkKeyword(node, 'indexedaccesstype'),
Expand Down
28 changes: 14 additions & 14 deletions test/unit/enum.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,57 +11,57 @@ const dir = locations.testOutput('enums_test')
// FIXME: missing: inline enums (entity Foo { bar: String enum { ... }})

describe('Enum Types', () => {
let ast
let astw

beforeEach(async () => await fs.unlink(dir).catch(() => {}))
beforeAll(async () => {
const paths = await cds2ts
.compileFromFile(locations.unit.files('enums/model.cds'), { outputDirectory: dir, inlineDeclarations: 'structured' })
ast = new ASTWrapper(path.join(paths[1], 'index.ts'))
astw = new ASTWrapper(path.join(paths[1], 'index.ts'))
})

describe('Anonymous', () => {
describe('String Enum', () => {
test('Definition Present', async () =>
expect(ast.tree.find(n => n.name === 'InlineEnum_gender'
expect(astw.tree.find(n => n.name === 'InlineEnum_gender'
&& n.initializer.expression.female === 'female'
&& n.initializer.expression.male === 'male'
&& n.initializer.expression.non_binary === 'non-binary'))
.toBeTruthy())

test('Referring Property', async () =>
expect(ast.getAspects().find(({name, members}) => name === '_InlineEnumAspect'
expect(astw.getAspects().find(({name, members}) => name === '_InlineEnumAspect'
&& members?.find(member => member.name === 'gender' && check.isNullable(member.type, [t => t?.full === 'InlineEnum_gender']))))
.toBeTruthy())

})

describe('Int Enum', () => {
test('Definition Present', async () =>
expect(ast.tree.find(n => n.name === 'InlineEnum_status'
expect(astw.tree.find(n => n.name === 'InlineEnum_status'
&& n.initializer.expression.submitted === 1
&& n.initializer.expression.fulfilled === 2
&& n.initializer.expression.canceled === -1
&& n.initializer.expression.shipped === 42))
.toBeTruthy())

test('Referring Property', async () =>
expect(ast.getAspects().find(({name, members}) => name === '_InlineEnumAspect'
expect(astw.getAspects().find(({name, members}) => name === '_InlineEnumAspect'
&& members?.find(member => member.name === 'status' && check.isNullable(member.type, [t => t?.full === 'InlineEnum_status']))))
.toBeTruthy())
})

describe('Mixed Enum', () => {
test('Definition Present', async () =>
expect(ast.tree.find(n => n.name === 'InlineEnum_yesno'
expect(astw.tree.find(n => n.name === 'InlineEnum_yesno'
&& n.initializer.expression.catchall === 42
&& n.initializer.expression.no === false
&& n.initializer.expression.yes === true
&& n.initializer.expression.yesnt === false))
.toBeTruthy())

test('Referring Property', async () =>
expect(ast.getAspects().find(({name, members}) => name === '_InlineEnumAspect'
expect(astw.getAspects().find(({name, members}) => name === '_InlineEnumAspect'
&& members?.find(member => member.name === 'yesno' && check.isNullable(member.type, [t => t?.full === 'InlineEnum_yesno']))))
.toBeTruthy())
})
Expand All @@ -70,42 +70,42 @@ describe('Enum Types', () => {
describe('Named', () => {
describe('String Enum', () => {
test('Values', async () =>
expect(ast.tree.find(n => n.name === 'Gender'
expect(astw.tree.find(n => n.name === 'Gender'
&& n.initializer.expression.female === 'female'
&& n.initializer.expression.male === 'male'
&& n.initializer.expression.non_binary === 'non-binary'))
.toBeTruthy())

test('Type Alias', async () =>
expect(ast.getTypeAliasDeclarations().find(n => n.name === 'Gender'
expect(astw.getTypeAliasDeclarations().find(n => n.name === 'Gender'
&& ['male', 'female', 'non-binary'].every(t => n.types.includes(t))))
.toBeTruthy())
})

describe('Int Enum', () => {
test('Values', async () =>
expect(ast.tree.find(n => n.name === 'Status'
expect(astw.tree.find(n => n.name === 'Status'
&& n.initializer.expression.submitted === 1
&& n.initializer.expression.unknown === 0
&& n.initializer.expression.cancelled === -1))
.toBeTruthy())

test('Type Alias', async () =>
expect(ast.getTypeAliasDeclarations().find(n => n.name === 'Status'
expect(astw.getTypeAliasDeclarations().find(n => n.name === 'Status'
&& [-1, 0, 1].every(t => n.types.includes(t))))
.toBeTruthy())
})

describe('Mixed Enum', () => {
test('Values', async () =>
ast.tree.find(n => n.name === 'Truthy'
astw.tree.find(n => n.name === 'Truthy'
&& n.yes === true
&& n.no === false
&& n.yesnt === false
&& n.catchall === 42))

test('Type Alias', async () =>
expect(ast.getTypeAliasDeclarations().find(n => n.name === 'Truthy'
expect(astw.getTypeAliasDeclarations().find(n => n.name === 'Truthy'
&& [true, false, 42].every(t => n.types.includes(t))))
.toBeTruthy())
})
Expand Down
14 changes: 14 additions & 0 deletions test/unit/files/views/model.cds
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
namespace views_test;

entity Foo {
id: Integer;
code: String;
flag: Boolean;
}

entity FooView as
select from Foo {
id,
code,
code as alias
};
2 changes: 1 addition & 1 deletion test/unit/output.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,7 @@ describe('Compilation', () => {
])
// ...are currently exceptions where both singular _and_ plural
// are annotated and the original name is used as an export on top of that.
// So _three_ exports per entity. If we every choose to remove this third one,
// So _three_ exports per entity. If we ever choose to remove this third one,
// then this test has to reflect that.
})

Expand Down
2 changes: 1 addition & 1 deletion test/unit/references.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ const cds2ts = require('../../lib/compile')
const { ASTWrapper, check } = require('../ast')
const { locations } = require('../util')

const dir = locations.unit.files('output/references')
const dir = locations.testOutput('output/references')

// compilation produces semantically complete Typescript
describe('References', () => {
Expand Down
35 changes: 35 additions & 0 deletions test/unit/views.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
'use strict'

const fs = require('fs').promises
const path = require('path')
const cds2ts = require('../../lib/compile')
const { ASTWrapper, check } = require('../ast')
const { locations } = require('../util')

const dir = locations.testOutput('views_test')

describe('View Entities', () => {
let astw

beforeEach(async () => await fs.unlink(dir).catch(() => {}))
beforeAll(async () => {
const paths = await cds2ts
.compileFromFile(locations.unit.files('views/model.cds'), { outputDirectory: dir, inlineDeclarations: 'structured' })
astw = new ASTWrapper(path.join(paths[1], 'index.ts'))
})

test('View Entity Present', () => {
astw.exists('_FooViewAspect')
})

test('Expected Properties Present', () => {
astw.exists('_FooViewAspect', 'id', ({type}) => check.isNullable(type, [check.isNumber]))
astw.exists('_FooViewAspect', 'code', ({type}) => check.isNullable(type, [check.isString]))
// including alias
astw.exists('_FooViewAspect', 'alias', ({type}) => check.isNullable(type, [check.isString]))
})

test('Unselected Field Not Present', () => {
expect(() => astw.exists('_FooViewAspect', 'flag', ({type}) => check.isNullable(type, [check.isString]))).toThrow(Error)
})
})