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

Satisfy minimum requirements for full typescript project #37

Merged
merged 16 commits into from
Apr 7, 2025

Conversation

FranjoMindek
Copy link
Contributor

@FranjoMindek FranjoMindek commented Mar 21, 2025

This is satisfying all tsc and eslint requirements when we convert each of the js files to ts.
Rest of changes will be in further PRs

Contains:

  • import changes.
  • logger.ts has been fully changed, due to using invalid constructor syntax.
  • bugfix on arrays on discord-bot.ts
  • let -> const where no reference change
  • miniumum types needed to typescript doesn't complain
  • handle nullable fields properly in cli.ts

@FranjoMindek FranjoMindek self-assigned this Mar 21, 2025
@FranjoMindek FranjoMindek requested a review from Martinsos March 21, 2025 12:45
Copy link
Member

@Martinsos Martinsos left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok we are almost there I think!

Copy link
Member

@Martinsos Martinsos left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice! Left some comments, I think they are quite clear, and I will let the @sodic take over the PR review and do the final accept, since we are getting more into TS types and he knows this stuff better. I will be lurking so I can see the final result and learn.

@FranjoMindek FranjoMindek requested a review from sodic March 24, 2025 16:39
Copy link
Member

@Martinsos Martinsos left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am giving a neutral "comment" to remove my "request changes", so that @sodic can do the final approve when he finds it fit.

@@ -12,19 +11,44 @@ import { generateCohortRetentionReport } from "./cohortRetentionReport";
// they are prepared (our events removed, sorted) and that they are all events available for CLI,
// for the whole history. You should obtain them with fetchAllCliEvents(), in that case they will
// be all good.
export async function generatePeriodReport(
export async function generateFullPeriodReport(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about typing these guys also? Or are we leaving this for later?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can do that now.


type ReportName = string;

export type SimpleReport = {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I am correct, we have these individual report files importing from this index file, because they need SImpleReport, but then we also have this file importing from them because it is using them. So we have a cyclic dependency. I know JS can suffer those to a degree, but still, might be good to avoid it. Therefore, I would consider extracting SimpleReport into separate file, simpleReport.ts .

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would also restructure then.
We have maybe too much files in the reports folder, and also the fact that both simpleReport and totalReport are located in the same reports directory. The naming indicates that they are similar, but one is only a type while the other is an actual report. The current structure is a bit weird. I wanted to do that in next PR but we could change it here.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't really mind that one has type and another does not though, types and functions are all important. Ideally files would be organized by domain. I would break the circ dep here now immediately, and then in the next PR you can try to improve the structure further if you wish.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll just wait for @sodic to give his opinion on types (they might change completely), and after that, if it's still a circ dep I will break it.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I figured out what's bothering me here.

  • Partial<SimpleReport> is a supertype of all reports, but its name suggests otherwise. And pure SimpleReport isn't use anywhere except to define other types.
  • SimpleReport's name is derived from CompositeReport (i.e., because there's a composite thing, this is simple thing)
  • The relationship is defined backwards - it's the printing that requires this interface, not the reports.
  • The string in [reportName: string] should be bounded. The entire type is probably underspecified.

This is a sketch of how I'd expect the type definitions to look like:

export type GenericReport = Partial<{
  text: string[];
  csv: (number | string)[][];
  chart: ImageCharts;
}>;

export type CompositeReport = PeriodReport | TotalReport;

type TotalReport = {
  totalReport: Pick<GenericReport, "text">;
}

type PeriodReport = {
  cohortRetentionReport: CohortRetentionReport;
  userActivityReport: UserActivityReport;
  projectsReport: ProjectsReport;
};

export type CohortRetentionReport = Pick<GenericReport, "text">;

export type UserActivityReport = Pick<GenericReport, "text" | "csv" | "chart">;

export type ProjectsReport = Pick<GenericReport, "text" | "csv">;

I might have missed something as I'm still not 100% familiar with the code, but these types convey an explicit structure and tell a story. Ideally we'd also colocate them.

@FranjoMindek Try to work this in and let me know if you need help. We can have a call tomorrow (today :))

Copy link
Member

@Martinsos Martinsos left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@FranjoMindek did a bit more of reviewing!

Copy link

@sodic sodic left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice work! Left some comments, will come back with more probably :)

import ImageCharts from "image-charts";

export type ChartData = {
series: Record<string, number[]>;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just FYI, no need to change. Read this and pick your preference: wasp-lang/wasp#1235 (comment)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like the idea of having different intents with records and index signatures. Though, for that to work the whole team must be educated on it. Personally, I like it.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Glad to hear it! Time for an inquisition then :)

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You put in a rocket here but didn't change it. Forgot to push something?


// Expects data to be:
// data = { series: { name: [number] }, periodEnds: [string] }
// Returns a string, URL leading to image-charts.com that contains query with exact
// instructions how to display this chart via image.
export function buildChartImageUrl(data, title, type = "line") {
const chart = ImageCharts()
export function buildChartImageUrl(
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know we're typing this in the next PR, so no changes necessary here.

Still, FYI: wasp-lang/vscode-wasp#6 (comment)

export { fetchEventsForReportGenerator } from "./events";
export { generateTotalReport } from "./totalReport";
import { generatePeriodReport } from "./periodReport";

type ReportName = string;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unbranded type aliases can be avoided/improved in most cases. This is usually desriable because aliases can give a false sense of type safety and obfuscate primitive types.

In this case, it's used to name a Record's key. A better way to do this is an index signature:

export type CompositeReport = { [reportName: string]: Partial<SimpleReport> };

Further reference: wasp-lang/wasp#1235 (comment)

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Apologies, I forgot to mention this in writing.

I have a strong feeling the key type should be a bounded subset of string. Is that right?

@FranjoMindek FranjoMindek requested a review from sodic March 28, 2025 15:00
Copy link

@sodic sodic left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Finally got around to it :)

Comment on lines 21 to 30
type BasePeriodReport = {
userActivityReport: UserActivityReport;
projectsReport: ProjectsReport;
};

export type AllTimePeriodReort = BasePeriodReport;

export type PeriodReport = BasePeriodReport & {
cohortRetentionReport: CohortRetentionReport;
};
Copy link

@sodic sodic Mar 31, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's going on here, why is the all time report different than the period report?

Also, the names seem to off:

  • AllTimePeriodReport sounds like a specialization of a PeriodReport but it isn't, meaning that PeriodReport is also missing some kind of a prefix (or that AllTimePeriodReport shouldn't have Period in it)
  • After fixing that, I believe the Base prefix on BasePeriodReport becomes unnecessary.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That is a bit weird relationship.

PeriodReport is a general report over a period of time.

AllTimePeriodReport is ?variation? of PeriodReport which excludes CohortRetentionReport because of it's complexity. This is because a period report which spans the whole timeline has too much data.

Maybe PeriodReport and AllTimeReport?

If it's true that they share part of reports by accident (they both relate to the time domain) but are actually different, it would be best to extract AllTimeReport to a different file?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now that I look at it TotalReport also covers the whole period of time. There is again some overlap.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Report restructuring was decided as separate PR

export { fetchEventsForReportGenerator } from "./events";
export { generateTotalReport } from "./totalReport";
import { generatePeriodReport } from "./periodReport";

type ReportName = string;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Apologies, I forgot to mention this in writing.

I have a strong feeling the key type should be a bounded subset of string. Is that right?


type ReportName = string;

export type SimpleReport = {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I figured out what's bothering me here.

  • Partial<SimpleReport> is a supertype of all reports, but its name suggests otherwise. And pure SimpleReport isn't use anywhere except to define other types.
  • SimpleReport's name is derived from CompositeReport (i.e., because there's a composite thing, this is simple thing)
  • The relationship is defined backwards - it's the printing that requires this interface, not the reports.
  • The string in [reportName: string] should be bounded. The entire type is probably underspecified.

This is a sketch of how I'd expect the type definitions to look like:

export type GenericReport = Partial<{
  text: string[];
  csv: (number | string)[][];
  chart: ImageCharts;
}>;

export type CompositeReport = PeriodReport | TotalReport;

type TotalReport = {
  totalReport: Pick<GenericReport, "text">;
}

type PeriodReport = {
  cohortRetentionReport: CohortRetentionReport;
  userActivityReport: UserActivityReport;
  projectsReport: ProjectsReport;
};

export type CohortRetentionReport = Pick<GenericReport, "text">;

export type UserActivityReport = Pick<GenericReport, "text" | "csv" | "chart">;

export type ProjectsReport = Pick<GenericReport, "text" | "csv">;

I might have missed something as I'm still not 100% familiar with the code, but these types convey an explicit structure and tell a story. Ideally we'd also colocate them.

@FranjoMindek Try to work this in and let me know if you need help. We can have a call tomorrow (today :))

projectsReport: ProjectsReport;
};

export type AllTimePeriodReort = BasePeriodReport;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This guy has a typo.

): Promise<PeriodReport> {
const events = prefetchedEvents ?? (await fetchEventsForReportGenerator());

const basePeriodReeport = await generatePeriodReportBaseReports(
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo in "reeport"

@FranjoMindek
Copy link
Contributor Author

The problem of

export type CompositeReport = PeriodReport | TotalReport | ...;

Is that when we have a function which accepts CompositeReport, if we want to do generic operations on values of any of composite report's reports, it will automatically restrict them to the intersection between all of the composite reports components.

In the end: Object.values(compositeReport) will return { text: string; }.
If in the future we create a simple report without text field, it will return any.
You will need to assert them to GenericReport.

They can be check for other fields ("text" in simpleReport) but intellisense won't tell you that they exist before you check for them yourself. You need to have that knowledge yourself. Actually you can't even expand them like that.

export interface TextReport {
text: string[];
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

report specific functions will also go here

@FranjoMindek FranjoMindek requested a review from sodic April 7, 2025 07:34
Copy link

@sodic sodic left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good!

Don't forget to take care of the restructuring in a different PR :)

@FranjoMindek FranjoMindek merged commit a29ed01 into master Apr 7, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants