-
Notifications
You must be signed in to change notification settings - Fork 2
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
wip: feat: collection service & controller #3971
base: main
Are you sure you want to change the base?
Conversation
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.
A few minor comment and questions on the code.
I think a stronger pattern here would be to store Metabase collection IDs to the PlanX database. This will mean we don't have to query all Metabase collections and find the team each time we make a request. We can manually populate these values for existing teams.
When we add a new team, we want to automatically create a collection for them. This action should return an ID which we can persist in our database. When a new flow is created within this team, we'll then already have the collection ID to hand to ensure the dashboard is created in the correct collection.
I think this would be both simpler and a bit more robust (less room for failures) 👍
const name = _req.query.name as string; | ||
|
||
if (!name) { | ||
return res.status(400).json({ error: "Name parameter is required" }); |
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.
We should be using the type for this request handler defined in types.ts
- this is already handling this validation and parsing for us.
/** Checks if a collection exists with name matching `teamName` exists. | ||
* Returns the matching collection ID if exists, otherwise false. */ | ||
export async function checkCollections(teamName: string): Promise<any> { |
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'm not yet quite picking up when and where this would be used - I guess we need the collection ID (i.e. teamId) in order to make a new dashboard?
If this is the case, we likely won't need a controller and exposed endpoint here - we can just call this in the service when and where we need it.
console.error("Error: ", error); | ||
if (error instanceof MetabaseError) { | ||
console.error("MetabaseError:", { | ||
message: error.message, | ||
statusCode: error.statusCode, | ||
response: error.response, | ||
}); | ||
} else if (error instanceof Error) { | ||
console.error("Unexpected error:", { | ||
message: error.message, | ||
stack: error.stack, | ||
}); | ||
} else { | ||
console.error("Unknown error:", error); | ||
} | ||
throw error; | ||
} |
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.
We already have error handling set up in the API via Express and it's errorHandler()
- this should mean we can just throw here with a helpful message and allow it to be caught upstream.
I think we'll need to add a check for MetabaseError
there, but that should be it and it will allow us to simplify a lot of this repeated logic 👍
}), | ||
}); | ||
|
||
export type CheckCollectionsRequest = ValidatedRequestHandler< |
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.
export type CheckCollectionsRequest = ValidatedRequestHandler< | |
export type CheckCollections = ValidatedRequestHandler< |
nit: These aren't requests, but request handlers - ValidatedRequestHandler<Req, Res>
export interface CheckCollectionResponse { | ||
collectionId: number | false; | ||
} |
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.
This is a bit of an unexpected response - I'd expect maybe number | null
or number | undefined
;
} | ||
|
||
export interface NewCollectionParams { | ||
/** The name of the new collection */ |
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.
nit: Redundant
body: z.object({ | ||
name: z.string(), | ||
description: z.string().optional(), | ||
parent_id: z.number().optional(), |
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 think it's safe to assume all new collections will be grouped here by default.
/** Metbase collection ID for the the "Council" collection **/
const COUNCIL_COLLECTION_ID = 58;
...
parent_id: z.number().default(COUNCIL_COLLECTION_ID)
7323e37
to
b3947be
Compare
for Metabase client
checks for collection and creates new one
and let errorHandler catch
f873db3
to
1a4ca85
Compare
This makes a lot of sense to me! I haven't tried to address this in this PR because a.) I don't know how to deal with the PlanX database and b.) I thought this would get a bit too messy. Let me know if you think this is the right place for it! |
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.
For functions like this, I'd suggest importing from lodash (which we already use in the API) instead of rolling our own. It means we're using well tested and efficient utility functions, and there's not much overhead in terms of package size.
const requestBody = toSnakeCase({ | ||
name, | ||
description, | ||
parent_id, | ||
}); |
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.
Rather than casting to snake_case, the API should accept snake_case, and then we can cast or transform as necessary for the Metabase request.
Incoming request (camelCase) → Validate → Transform to snake_case → Make request to Metabase
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.
Ended up using transform
instead of importing the Lodash method.
filterValue: string; | ||
} | ||
|
||
export function validateInput(input: unknown): input is Input { |
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.
This looks like it's currently unused - is that right?
In terms of validation, it would be best to follow the existing pattern in the API.
→ Incoming request of type unknown
→
→ Passes through existing validate()
middleware in route →
→ Using Zod, is checked against a provided schema →
→ Validated payload is automatically passed along as res.locals.parsedReq
→
→ Controller and Service have both type safety and a validated payload
Object.keys(requestBody).forEach( | ||
(key) => requestBody[key] === undefined && delete requestBody[key], | ||
); |
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.
Passing through the validate()
middleware should handle this step already - no additional keys would be present.
export async function authentication(): Promise<boolean> { | ||
try { | ||
const response = await client.get("/user/current"); | ||
return response.status === 200; | ||
} catch (error) { | ||
console.error("Error testing Metabase connection:", error); | ||
return false; | ||
} | ||
} |
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.
This also looks unused currently.
I think this should be a middleware added to the route - before proceeding to make additional requests to Metabase, we can first check the API key is valid.
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 had a quick chat with Jo, who pointed out that it might not be necessary as the authentication function was basically checking that we could get
before actually get
-ing (which happens in service.ts, in checkCollections()
:
const response = await client.get(`/api/collection/`);
Between validateConfig()
in client.ts
and running the get
above, does that mean authentication is redundant? Or do we still need another check for the valid API key? I believe the first get
would just throw a MetabaseError
if it was invalid, is that sufficient?
🤖 Hasura Change Summary compared a subset of table metadata including permissions: Updated Tables (1)
|
e00a6c8
to
c384fee
Compare
* @param id | ||
* @returns | ||
*/ | ||
// TODO: is this more suited to be part of the collection.test.ts? |
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.
Probably better in its own file getCollection.ts
or something like that :)
* @returns | ||
*/ | ||
// TODO: is this more suited to be part of the collection.test.ts? | ||
export async function getCollection(id: number): Promise<any> { |
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.
Probably would be nice to type the response rather than have <any>
*/ | ||
export async function checkCollections( | ||
params: NewCollectionParams, | ||
): Promise<any> { |
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.
Same comment as below - would be nice to type the response rather than have <any>
}[]; | ||
} | ||
|
||
export const getTeamAndMetabaseId = async (name: string) => { |
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 think this is fine as one gql request, maybe I'd change the function name to be getTeamIdAndMetabaseId
just to be completely explicit
const teamAndMetabaseId = await getTeamAndMetabaseId(params.name); | ||
const { metabaseId, id } = teamAndMetabaseId; |
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.
You can do this on one line like this:
const { metabaseId, id } = await getTeamAndMetabaseId(params.name);
And if you want to recast id
to teamId
so it's more obvious what it is, you can do:
const { metabaseId, id: teamId } = await getTeamAndMetabaseId(params.name);
Then make sure you use teamId
in the rest of the codeblock.
* @params `name` is required, but `description` and `parent_id` are optional. | ||
* @returns `response.data`, so use dot notation to access `id` or `parent_id`. | ||
*/ | ||
export async function checkCollections( |
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.
Maybe I'd call this createCollectionIfDoesNotExist
- "checking" is a bit of a vague term
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 maybe the controller could just be called CollectionsController
or even MetabaseCollectionsController
Added a few comments to try and answer some questions! :) I think your intuition about splitting on metabase/planX db lines is correct. I haven't tested this locally (think I'm missing the metabase |
fafd734
to
5248d51
Compare
Everything seems to work now--all of my tests pass and I have also successfully used Insomnia to check and update the PlanX DB (locally) and create collections in Metabase (staging).
I think the next steps would be to break this up into two PRs with cleaner commit histories. Instinctively I would put everything that interacts with the PlanX DB into one (
getTeamAndMetabaseId
andupdateMetabaseId
) plus tests, and everything that only interacts with Metabase into another (createCollection
). OR I could put thecontroller
in one along with theroutes
update, and leave everything else in a bigger one? Feedback and thoughts very welcome!What does this PR do?
PlanX DB
metabase_id
toteams
api
permissions to be able to updatemetabase_id
and select othersController and routes
Service
checkCollections
is the main service, which runs other relevant functionsgetTeamAndMetabaseId
fetchesid
andmetabase_id
fromteams
metabase-id
, thencreateCollection
runs, creating new collection and returning Metabase collection IDupdateMetabaseId
runs updatingteams.metabase_id
There are also tests!
Questions
getTeamAndMetabaseId
be broken up intogetTeam
andgetMetabaseId
?api
permissions correctly 😅