-
Notifications
You must be signed in to change notification settings - Fork 11
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
Conversation
Hi Ludwig, very impressive work, thanks for putting in the effort! As this is quite the intrusive change, I'd like to give this a more thorough look. I will be out of office for two weeks, starting Friday, so I won't be able to look into this (or other PRs) until mid November. Just so you know this is not being shelved, just put on hold until I have the time to give it a proper review. 🙂 Best, |
One particular problem we formerly overcame with our two-flavour-approach was dealing with the provenance of annotations. This is, as far as I am aware, generally an unsolved problem when you don't have access to the xtended flavour. Consider the following model: @singular: 'A'
entity A {}
entity B: A {} Using only the inferred flavour, both // This is an automatically generated file. Please do not change its contents manually!
import * as __ from './_';
export function _AAspect<TBase extends new (...args: any[]) => object>(Base: TBase) {
return class A extends Base {
static readonly kind: "entity" | "type" | "aspect" = 'entity';
declare static readonly keys: __.KeysOf<A>;
declare static readonly elements: __.ElementsOf<A>;
static readonly actions: Record<never, never>;
};
}
export class A extends _AAspect(__.Entity) {}
Object.defineProperty(A, 'name', { value: 'A' })
Object.defineProperty(A, 'is_singular', { value: true })
export class A_ extends Array<A> {$count?: number}
Object.defineProperty(A_, 'name', { value: 'A' })
export function _AAspect<TBase extends new (...args: any[]) => object>(Base: TBase) {
return class A extends _AAspect(Base) {
declare x?: number | null
static override readonly kind: "entity" | "type" | "aspect" = 'entity';
declare static readonly keys: __.KeysOf<A>;
declare static readonly elements: __.ElementsOf<A>;
static readonly actions: typeof A.actions & Record<never, never>;
};
}
// v ❌ v
export class A extends _AAspect(__.Entity) {}
Object.defineProperty(A, 'name', { value: 'B' })
Object.defineProperty(A, 'is_singular', { value: true })
export class B extends Array<A> {$count?: number}
Object.defineProperty(B, 'name', { value: 'B' }) did you consider this aspect already or do you see a way to handle this with only the inferred flavour available? |
To be honest I didn't think of that. Considerung your sample I personally would say that such entity inheritence scenarios are not very realistic. Until now, I only encountered the use of aspects to enrich other entities with a set of annotations or fields (e.g. cuid, managed, CodeList to name a few). In that case we would be safe as one would not add That being said, the So there a times a propagation of e.g. // schema.cds
namespace db;
// -> MyBooks, MyBook
@singular: 'MyBook'
@plural: 'MyBooks'
entity Books {}
// -> Pubs, Pub
@singular: 'Pub'
@plural: 'Pubs'
entity Publishers {}
// -> BestsellingBooks, BestsellingBook
@singular: null // stop propagation and keep original name of entity
@plural: null // stop propagation and keep original name of entity
entity BestsellingBooks as select * from Books {} // service.cds
using {db} from '../db/schema';
service MyService {
// -> Books, Book
@singular: null // stop propagation and keep original name of entity
@plural: null // stop propagation and keep original name of entity
entity Books as projection on db.Books;
// -> BestsellingBooks, BestsellingBook (annotation propagation was stopped)
entity BestsellingBooks as projection on db.BestsellingBooks;
// -> Pubs, Pubs (via propagation)
entity Publishers as projection on db.Publishers;
} At the end, it is of course a breaking change and users would need to adjust their models to correct their generated types, but cds-typer would follow the documented rules of how annotations are propagated. At times it may force users to add more annotations to their models as before. So 1) keep the xtended model for knowing the true origin of |
🤔 why is that? Aspects will be generated as classes by cds-typer, just as entities are. They therefore suffer from the same shortcomings our current inflection algorithm has, and thus can be annotated with custom inflection.
not sure I grasp that either. Are you talking about having to repeat the naming for service-entities, which bears the risk of being inconsistent during renamings? As for your proposed solutions: I agree, as I don't see any third solution either. I will bring this up in the next cds-typer sync and see if there is any preferred way forward. |
The array class type is not generated for those, only for entities. Therefore I am not even sure why inflection is required for aspects at all. For example an aspect e.g.: // model.cds
aspect Status {}
entity MyStatus : Status { } // index.ts
export class Statu extends _StatuAspect(__.Entity) {}
export class MyStatu extends _MyStatuAspect(__.Entity) {}
export class MyStatus extends Array<MyStatu> {$count?: number}
Exactly. I added the annotations on db level because of unsatisfactory inflected names and then had to repeat the annotations again on service level. Consistent renaming in both db and service model was actually not an issue, at least not yet. We just expected the annotations to be inherited to services level, because of the mentioned propagation |
Hi Ludwig, we discussed your proposal internally and we would very much like to have this change merged. Especially the simplified CSN handling, inline compositions, and localization are a great benefit in themselves. Is that an acceptable way forward for you? Best, |
Hi Daniel, that's really great news 😊. Regards, |
Hi Ludwig, that sounds like another step to driving back Best, |
I just realised there seems to the hypothetical szenario of adding (my heartfelt condolences for the GitHub user plural who receives a notification whenever I forget to put |
Hi Ludwig, quick update: we discussed this internally and agree with your proposed solution. Best, |
Hi Daniel, ok... Regards, |
Hi Ludwig, sorry, I meant the post I had already agreed to before, but now got confirmation of the PO as well:
Best, |
Hi Daniel, sorry, I am still at a loss as to if I still have an open task in this PR or not😅. P.S.: Maybe I am little slow today as I was sick the last few days. Regards, |
Hi Ludwig, sorry for the confusion again. 😃 Best, |
Then this PR is ready for review now |
Hi Ludwig, thanks for finishing this feature! I'm afraid I won't be able to give this a proper review before the holidays, as other tasks unfortunately currently take precedence, but if I don't get to it today or tomorrow, this will be one of the first things on my plate in the new year. Best, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you for this contribution!
Looks very good and even if it didn't bring support for locale classes, having weeded out several huge "FIXME this is a hack!" blocks makes this a valuable change.
I have added a few remarks and mostly annotated them with severity markers. Especially the syntax ones are largely dust to me.
#inheritedElements = null | ||
|
||
/** @returns set of inherited elements (e.g. ID of aspect cuid) */ | ||
get inheritedElements() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Have you considered property renaming in this context? I.e., does your code exhibit well-defined behaviour for when a property is present in a parent, but also in the entity itself (but possibly with a changed type)?
entity A {
name:String
}
entity B: A {
name:Integer
}
As of today, this is a known restriction. Even if you do not explicitly solve the issue, this could be an opportunity to error out if we find redefined properties.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No I haven't considered them - why would anyone do that 😶🌫️. No proper solution comes to mind right away, I would have to think about it some more.
If I understand you correctly I could implement the following approach though, to error out and stop the type generation in case of a type mismatch.
// visitor.js - #aspectify
for (let [ename, element] of Object.entries(entity.elements ?? [])) {
const inheritedElem = inheritedElements?.get(ename)
if (inheritedElem) {
const type = stringifyType(element.type)
const inheritedElemType = stringifyType(inheritedElem.type)
if (stringifiedType === inheritedElemType) {
continue
} else {
throw new Error(`Type '${type}' of element '${entity.name}.${ename}' does not match the type '${inheritedElemType}' from the parent`)
}
}
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't know what the architectural reasons would be, but it has happened in the past. 🙂
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
P.S.: I asked this mainly out of curiosity. As it is beyond the scope of your PR, this can totally be done at a later point in time and by someone else.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Then I would actually table this for later if this is ok for you
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Absolutely! 🙂
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you for this contribution!
Looks very good and even if it didn't bring support for locale classes, having weeded out several huge "FIXME this is a hack!" blocks makes this a valuable change.
I have added a few remarks and mostly annotated them with severity markers. Especially the syntax ones are largely dust to me, so feel free to just mark them as resolved without change if you feel strongly about your syntax choices.
@stockbal seeing that all immediate points have been addressed, do you consider this PR done and ready for merge, or do you need to make any more changes? |
No, unless you still found something that I should change, the MR is done from my point of view. |
Great! Then thanks once more and let's merge 🚀 |
Hi @daogrady,
proposal to get rid of one of the CSN flavors. I chose to remove the
xtended
flavor. All in all the required fixes/changes were not many and had some nice side effects.localized
elementsTasks
Here is short sample to show the changes to the generated classes (only the important parts are included)
Sample cds schema
Generated
index.ts
for namespacebookshop
Generated
index.js
for namespacebookshop
This branch is again based on the fix for the draftable state (see #348)
Fixes #116
Closes #77
Closes #128
Let me know if you want to go forward with this branch/approach.
Regards,
Ludwig