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

feat/refactor: dedicated classes for texts and inline comps #356

Merged
merged 33 commits into from
Jan 9, 2025
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
07dbaf8
refactor: overhaul/fix creation of `drafts` properties in entities
stockbal Oct 5, 2024
8260c32
refactor: rename cds file that causes transpilation errors
stockbal Oct 5, 2024
8d2b697
fix: fixes lint issues
stockbal Oct 5, 2024
6050b06
refactor: remove xtended csn
stockbal Oct 10, 2024
ac1552f
fix: transfer `key` value of associations to foreign key element
stockbal Oct 10, 2024
1c5844c
fix: do not skip inherited elements for views/projections
stockbal Oct 10, 2024
383b541
fix: fixes inline composition resolution
stockbal Oct 10, 2024
cf955ff
fix: fixes scoped entities
stockbal Oct 11, 2024
9e48f81
feat: include texts entities
stockbal Oct 11, 2024
61a429f
test: adjust tests to handle entities in subnamespacees
stockbal Oct 11, 2024
935995e
refactor: remove unused imports
stockbal Oct 12, 2024
4b8f1ec
Merge branch 'main' into refactor/one-csn
stockbal Oct 23, 2024
a2034a2
Merge branch 'main' into refactor/one-csn
stockbal Nov 23, 2024
3c8c5b4
fix: imports/exports for esm mode
stockbal Nov 23, 2024
f55e26f
Merge branch 'main' into refactor/one-csn
stockbal Dec 1, 2024
dc56572
feat: propagate inflection annotations from xtended
stockbal Dec 1, 2024
2d00a01
fix: fix output test
stockbal Dec 1, 2024
f2d462a
docs: add preliminary changelog
stockbal Dec 1, 2024
6d17ed5
feat: print inline enum to subnamespace buffer if necessary
stockbal Dec 3, 2024
e2d7756
Merge branch 'main' into refactor/one-csn
stockbal Dec 4, 2024
9524c6a
fix: use index access for inline entities in js exports
stockbal Dec 4, 2024
b274d05
fix: do not discard of inflection annotations exclusive to inferred csn
stockbal Dec 4, 2024
05b26c1
test: add test cases
stockbal Dec 4, 2024
f4080c3
refactor: cache inherited elements of entity inside repository
stockbal Dec 6, 2024
e1957ed
test: add/enhance test for inline compositions
stockbal Dec 6, 2024
a71c475
Merge branch 'main' into refactor/one-csn
stockbal Dec 6, 2024
d17f8ba
Merge branch 'main' into refactor/one-csn
stockbal Dec 12, 2024
4b0f3d3
Merge branch 'main' into refactor/one-csn
stockbal Dec 17, 2024
7e8d370
chore: update changelog
stockbal Dec 17, 2024
9b47593
Merge branch 'main' into refactor/one-csn
daogrady Jan 8, 2025
8ac088c
refactor: resolve review remarks
stockbal Jan 8, 2025
c6ea598
refactor: resolve review remarks
stockbal Jan 8, 2025
d1aa000
Merge branch 'main' into refactor/one-csn
daogrady Jan 9, 2025
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
5 changes: 2 additions & 3 deletions lib/compile.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ const writeJsConfig = path => {

/**
* Compiles a CSN object to Typescript types.
* @param {{xtended: import('./typedefs').resolver.CSN, inferred: import('./typedefs').resolver.CSN}} csn - csn tuple
* @param {import('./typedefs').resolver.CSN} csn - csn tuple
*/
const compileFromCSN = async csn => {

Expand All @@ -56,9 +56,8 @@ const compileFromCSN = async csn => {
*/
const compileFromFile = async inputFile => {
const paths = typeof inputFile === 'string' ? normalize(inputFile) : inputFile.map(f => normalize(f))
const xtended = await cds.linked(await cds.load(paths, { docs: true, flavor: 'xtended' }))
const inferred = await cds.linked(await cds.load(paths, { docs: true }))
return compileFromCSN({xtended, inferred})
return compileFromCSN(inferred)
}

module.exports = {
Expand Down
39 changes: 19 additions & 20 deletions lib/resolution/resolver.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ const { configuration } = require('../config')
/** @typedef {{typeName: string, typeInfo: TypeResolveInfo & { inflection: Inflection }}} ResolveAndRequireInfo */

class Resolver {
get csn() { return this.visitor.csn.inferred }
get csn() { return this.visitor.csn }

/** @param {Visitor} visitor - the visitor */
constructor(visitor) {
Expand Down Expand Up @@ -165,11 +165,13 @@ class Resolver {
*/
const isPropertyOf = (property, entity) => property && Object.hasOwn(entity?.elements ?? {}, property)

const defs = this.visitor.csn.inferred.definitions
const defs = this.visitor.csn.definitions

// check if name is already an entity, then we do not have a property access, but a nested entity
if (defs[p]?.kind === 'entity') return []

// assume parts to contain [Namespace, Service, Entity1, Entity2, Entity3, property1, property2]
/** @type {string} */
// @ts-expect-error - nope, we know there is at least one element
let qualifier = parts.shift()
let qualifier = /** @type {string} */ (parts.shift())
// find first entity from left (Entity1)
while ((!defs[qualifier] || !isEntity(defs[qualifier])) && parts.length) {
qualifier += `.${parts.shift()}`
Expand Down Expand Up @@ -240,6 +242,8 @@ class Resolver {
} else {
// TODO: make sure the resolution still works. Currently, we only cut off the namespace!
plural = util.getPluralAnnotation(typeInfo.csn) ?? typeInfo.plainName
// remove leading entity name
if (plural.indexOf('.') !== -1) plural = last(plural)
stockbal marked this conversation as resolved.
Show resolved Hide resolved
singular = util.getSingularAnnotation(typeInfo.csn) ?? util.singular4(typeInfo.csn, true) // util.singular4(typeInfo.csn, true) // can not use `plural` to honor possible @singular annotation

// don't slice off namespace if it isn't part of the inflected name.
Expand Down Expand Up @@ -311,18 +315,6 @@ class Resolver {
} else {
let { singular, plural } = targetTypeInfo.typeInfo.inflection

// FIXME: super hack!!
// Inflection currently does not retain the scope of the entity.
daogrady marked this conversation as resolved.
Show resolved Hide resolved
// But we can't just fix it in inflection(...), as that would break several other things
// So we bandaid-fix it back here, as it is the least intrusive place -- but this should get fixed asap!
if (target.type) {
const untangled = this.visitor.entityRepository.getByFqOrThrow(target.type)
const scope = untangled.scope.join('.')
if (scope && !singular.startsWith(scope)) {
singular = `${scope}.${singular}`
}
}

typeName = cardinality > 1
? toMany(plural)
: toOne(this.visitor.isSelfReference(target) ? 'this' : singular)
Expand Down Expand Up @@ -370,8 +362,14 @@ class Resolver {
// handle typeof (unless it has already been handled above)
const target = element.target?.name ?? element.type?.ref?.join('.') ?? element.type
if (target && !typeInfo.isDeepRequire) {
const { propertyAccess } = this.visitor.entityRepository.getByFq(target) ?? {}
if (propertyAccess?.length) {
const { propertyAccess, scope } = this.visitor.entityRepository.getByFq(target) ?? {}
if (scope?.length) {
// update inflections with proper prefix, e.g. Books.text, Books.texts
typeInfo.inflection = {
singular: [...scope, typeInfo.inflection?.singular].join('.'),
plural: [...scope, typeInfo.inflection?.plural].join('.')
}
} else if (propertyAccess?.length) {
const element = target.slice(0, -propertyAccess.join('.').length - 1)
const access = this.visitor.inlineDeclarationResolver.getTypeLookup(propertyAccess)
// singular, as we have to access the property of the entity
Expand Down Expand Up @@ -452,6 +450,7 @@ class Resolver {

const cardinality = getMaxCardinality(element)

/** @type {TypeResolveInfo} */
const result = {
isBuiltin: false, // will be rectified in the corresponding handlers, if needed
isInlineDeclaration: false,
Expand Down Expand Up @@ -569,7 +568,7 @@ class Resolver {
* @returns @see resolveType
*/
resolveTypeName(t, into) {
const result = into ?? {}
const result = into ?? /** @type {TypeResolveInfo} */({})
const path = t.split('.')
const builtin = this.builtinResolver.resolveBuiltin(path)
if (builtin === undefined) {
Expand Down
2 changes: 1 addition & 1 deletion lib/typedefs.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export module resolver {
compositions?: { target: string }[]
doc?: string,
elements?: { [key: string]: EntityCSN }
key?: string // custom!!
key?: boolean // custom!!
keys?: { [key:string]: any }
kind: string,
includes?: string[]
Expand Down
114 changes: 40 additions & 74 deletions lib/visitor.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
'use strict'

const util = require('./util')

const { isView, isUnresolved, propagateForeignKeys, collectDraftEnabledEntities, isDraftEnabled, isType, isProjection, getMaxCardinality, isViewOrProjection, isEnum, isEntity } = require('./csn')
const { propagateForeignKeys, collectDraftEnabledEntities, isDraftEnabled, isType, getMaxCardinality, isViewOrProjection, isEnum, isEntity } = require('./csn')
// eslint-disable-next-line no-unused-vars
const { SourceFile, FileRepository, Buffer, Path } = require('./file')
const { FlatInlineDeclarationResolver, StructuredInlineDeclarationResolver } = require('./components/inline')
Expand Down Expand Up @@ -39,13 +37,11 @@ class Visitor {
}

/**
* @param {{xtended: CSN, inferred: CSN}} csn - root CSN
* @param {CSN} csn - root CSN
*/
constructor(csn) {
propagateForeignKeys(csn.xtended)
propagateForeignKeys(csn.inferred)
// has to be executed on the inferred model as autoexposed entities are not included in the xtended csn
collectDraftEnabledEntities(csn.inferred)
propagateForeignKeys(csn)
collectDraftEnabledEntities(csn)
this.csn = csn

/** @type {Context[]} **/
Expand All @@ -72,41 +68,8 @@ class Visitor {
* Visits all definitions within the CSN definitions.
*/
visitDefinitions() {
for (const [name, entity] of Object.entries(this.csn.xtended.definitions)) {
if (isView(entity)) {
this.visitEntity(name, this.csn.inferred.definitions[name])
} else if (isProjection(entity) || !isUnresolved(entity)) {
this.visitEntity(name, entity)
} else {
LOG.warn(`Skipping unresolved entity: ${name}`)
}
}
// FIXME: optimise
// We are currently working with two flavours of CSN:
// xtended, as it is as close as possible to an OOP class hierarchy
// inferred, as it contains information missing in xtended
// This is less than optimal and has to be revisited at some point!
const handledKeys = new Set(Object.keys(this.csn.xtended.definitions))
// we are looking for autoexposed entities in services
const missing = Object.entries(this.csn.inferred.definitions).filter(([key]) => !key.endsWith('.texts') &&!handledKeys.has(key))
for (const [name, entity] of missing) {
// instead of using the definition from inferred CSN, we refer to the projected entity from xtended CSN instead.
// The latter contains the CSN fixes (propagated foreign keys, etc) and none of the localised fields we don't handle yet.
if (entity.projection) {
const targetName = entity.projection.from.ref[0]
// FIXME: references to types of entity properties may be missing from xtendend flavour (see #103)
// this should be revisted once we settle on a single flavour.
const target = this.csn.xtended.definitions[targetName] ?? this.csn.inferred.definitions[targetName]
if (target.kind !== 'type') {
// skip if the target is a property, like in:
// books: Association to many Author.books ...
// as this would result in a type definition that
// name-clashes with the actual declaration of Author
this.visitEntity(name, target)
}
} else {
LOG.error(`Expecting an autoexposed projection within a service. Skipping ${name}`)
}
for (const [name, entity] of Object.entries(this.csn.definitions)) {
this.visitEntity(name, entity)
daogrady marked this conversation as resolved.
Show resolved Hide resolved
}
}

Expand All @@ -117,15 +80,8 @@ class Visitor {
* @returns {[string, object][]} array of key name and key element pairs
*/
#keys(fq) {
// 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[fq]?.keys ?? {},
...this.csn.xtended.definitions[fq]?.keys ?? {}
...this.csn.definitions[fq]?.keys ?? {}
})
}

Expand Down Expand Up @@ -225,7 +181,7 @@ class Visitor {
// FIXME: replace with resolution/entity::asIdentifier
const toLocalIdent = ({ns, clean, fq}) => {
// types are not inflected, so don't change those to singular
const csn = this.csn.inferred.definitions[fq]
const csn = this.csn.definitions[fq]
const ident = isType(csn)
? clean
: this.resolver.inflect({csn, plainName: clean}).singular
Expand Down Expand Up @@ -262,14 +218,12 @@ class Visitor {
buffer.addIndentedBlock(`return class ${clean} extends ${ancestorsAspects} {`, () => {
/** @type {import('./typedefs').resolver.EnumCSN[]} */
const enums = []
const inheritedElements = !isViewOrProjection(entity) ? this.#getInheritedElements(entity) : []
/** @type {TypeResolveOptions} */
const resolverOptions = { forceInlineStructs: isEntity(entity) && configuration.inlineDeclarations === 'flat'}

for (let [ename, element] of Object.entries(entity.elements ?? [])) {
if (element.target && /\.texts?/.test(element.target)) {
LOG.warn(`referring to .texts property in ${fq}. This is currently not supported and will be ignored.`)
continue
}
if (inheritedElements.includes(ename)) continue
this.visitElement({name: ename, element, file, buffer, resolverOptions})

// make foreign keys explicit
Expand All @@ -284,7 +238,8 @@ class Visitor {
LOG.error(`Attempting to generate a foreign key reference called '${foreignKey}' in type definition for entity ${fq}. But a property of that name is already defined explicitly. Consider renaming that property.`)
} else {
const kelement = Object.assign(Object.create(originalKeyElement), {
isRefNotNull: !!element.notNull || !!element.key
isRefNotNull: !!element.notNull || !!element.key,
key: element.key
})
this.visitElement({name: foreignKey, element: kelement, file, buffer, resolverOptions})
}
Expand All @@ -293,7 +248,7 @@ class Visitor {
}

// store inline enums for later handling, as they have to go into one common "static elements" wrapper
if (isInlineEnumType(element, this.csn.xtended)) {
if (isInlineEnumType(element, this.csn)) {
enums.push(element)
}
}
Expand Down Expand Up @@ -333,6 +288,22 @@ class Visitor {
this.contexts.pop()
}

/**
* @param {EntityCSN} entity -
* @returns {string[]}
*/
#getInheritedElements(entity) {
stockbal marked this conversation as resolved.
Show resolved Hide resolved
/** @type {string[]} */
const inheritedElements = []
for (const parent of entity.includes ?? []) {
for (const element of Object.keys(this.csn.definitions[parent]?.elements ?? {})) {
inheritedElements.push(element)
}
}

return inheritedElements
}

/**
* @param {string} fq - fully qualified name of the entity
* @param {string} clean - the clean name of the entity
Expand All @@ -352,7 +323,7 @@ class Visitor {
* @param {string} content - the content to set the name property to
*/
const overrideNameProperty = (clazz, content) => `Object.defineProperty(${clazz}, 'name', { value: '${content}' })`
const { namespace: ns, entityName: clean, inflection } = this.entityRepository.getByFqOrThrow(fq)
const { namespace: ns, entityName: clean, inflection, scope } = this.entityRepository.getByFqOrThrow(fq)
const file = this.fileRepository.getNamespaceFile(ns)
let { singular, plural } = inflection

Expand All @@ -368,7 +339,7 @@ class Visitor {
// as types are not inflected, their singular will always clash and there is also no plural for them anyway -> skip
// if the user defined their entities in singular form we would also have a false positive here -> skip
const namespacedSingular = `${ns.asNamespace()}.${singular}`
if (!isType(entity) && namespacedSingular !== fq && namespacedSingular in this.csn.xtended.definitions) {
if (!isType(entity) && namespacedSingular !== fq && namespacedSingular in this.csn.definitions) {
LOG.error(
`Derived singular '${singular}' for your entity '${fq}', already exists. The resulting types will be erronous. Consider using '@singular:'/ '@plural:' annotations in your model or move the offending declarations into different namespaces to resolve this collision.`
)
Expand All @@ -381,20 +352,15 @@ class Visitor {
? file.getSubNamespace(this.resolver.trimNamespace(parent.name))
: file.classes

// we can't just use "singular" here, as it may have the subnamespace removed:
// "Books.text" is just "text" in "singular". Within the inflected exports we need
// to have Books.texts = Books.text, so we derive the singular once more without cutting off the ns.
// Directly deriving it from the plural makes sure we retain any parent namespaces of kind "entity",
// which would not be possible while already in singular form, as "Book.text" could not be resolved in CSN.
// edge case: @singular annotation present. singular4 will take care of that.
file.addInflection(util.singular4(entity, true), plural, clean)

// in case of projections `entity` is empty -> retrieve from inferred csn where the actual properties are rolled out
const target = isProjection(entity) || isView(entity)
? this.csn.inferred.definitions[fq]
: entity
if (scope?.length > 0) {
/** @param {string} n - name of entity */
const scoped = n => [...scope, n].join('.')
file.addInflection(scoped(singular), scoped(plural), scoped(clean))
} else {
file.addInflection(singular, plural, clean)
}

this.#aspectify(fq, target, buffer, { cleanName: singular })
this.#aspectify(fq, entity, buffer, { cleanName: singular })

buffer.add(overrideNameProperty(singular, entity.name))
buffer.add(`Object.defineProperty(${singular}, 'is_singular', { value: true })`)
Expand Down Expand Up @@ -500,7 +466,7 @@ class Visitor {
if (isEnum(type) && !isReferenceType(type) && this.resolver.builtinResolver.resolveBuiltin(type.type)) {
file.addEnum(fq, entityName, csnToEnumPairs(type), docify(type.doc))
} else {
const isEnumReference = typeof type.type === 'string' && isEnum(this.csn.inferred.definitions[type?.type])
const isEnumReference = typeof type.type === 'string' && isEnum(this.csn.definitions[type?.type])
// alias
file.addType(fq, entityName, this.resolver.resolveAndRequire(type, file).typeName, isEnumReference)
}
Expand Down
Loading
Loading