-
Notifications
You must be signed in to change notification settings - Fork 11
fix: reference variable working test #562
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,6 @@ | ||
| import type { SQL, Tx } from "@ctrlplane/db"; | ||
| import type { ResourceCondition } from "@ctrlplane/validators/resources"; | ||
| import _, { get } from "lodash"; | ||
| import _ from "lodash"; | ||
| import { isPresent } from "ts-is-present"; | ||
| import { z } from "zod"; | ||
|
|
||
|
|
@@ -32,6 +32,7 @@ import { | |
| isPassingChannelSelectorPolicy, | ||
| isPassingNoPendingJobsPolicy, | ||
| } from "@ctrlplane/job-dispatch"; | ||
| import { getReferenceVariableValue } from "@ctrlplane/rule-engine"; | ||
| import { Permission } from "@ctrlplane/validators/auth"; | ||
| import { resourceCondition } from "@ctrlplane/validators/resources"; | ||
|
|
||
|
|
@@ -383,87 +384,52 @@ export const resourceRouter = createTRPCRouter({ | |
| .on({ type: "resource", id: input }), | ||
| }) | ||
| .input(z.string().uuid()) | ||
| .query(({ ctx, input }) => | ||
| ctx.db.query.resource | ||
| .findFirst({ | ||
| where: and(eq(schema.resource.id, input), isNotDeleted), | ||
| with: { metadata: true, variables: true, provider: true }, | ||
| .query(async ({ ctx, input }) => { | ||
| const resource = await ctx.db.query.resource.findFirst({ | ||
| where: and(eq(schema.resource.id, input), isNotDeleted), | ||
| with: { metadata: true, variables: true, provider: true }, | ||
| }); | ||
| if (resource == null) return null; | ||
|
|
||
| const { relationships } = await getResourceParents(ctx.db, resource.id); | ||
|
|
||
| const parsedVariables = resource.variables | ||
| .map((v) => { | ||
| const parsed = schema.resourceVariableSchema.safeParse(v); | ||
| if (!parsed.success) return null; | ||
| return parsed.data; | ||
| }) | ||
| .then(async (t) => { | ||
| if (t == null) return null; | ||
|
|
||
| const { relationships, getTargetsWithMetadata } = | ||
| await getResourceParents(ctx.db, t.id); | ||
| const relationshipTargets = await getTargetsWithMetadata(); | ||
|
|
||
| const parsedVariables = t.variables | ||
| .map((v) => { | ||
| try { | ||
| return schema.resourceVariableSchema.parse(v); | ||
| } catch (error) { | ||
| console.error( | ||
| `Failed to parse variable ${v.key} for resource ${t.id}:`, | ||
| error, | ||
| ); | ||
| return null; | ||
| } | ||
| }) | ||
| .filter(isPresent); | ||
|
|
||
| const directVariables = parsedVariables.filter( | ||
| (v): v is schema.DirectResourceVariable => v.valueType === "direct", | ||
| ); | ||
| .filter(isPresent); | ||
|
|
||
| const referenceVariables = parsedVariables | ||
| .filter( | ||
| (v): v is schema.ReferenceResourceVariable => | ||
| v.valueType === "reference", | ||
| ) | ||
| .map((v) => { | ||
| const relationshipInfo = relationships[v.reference]; | ||
| if (!relationshipInfo) | ||
| return { ...v, resolvedValue: v.defaultValue }; | ||
|
|
||
| const targetId = relationshipInfo.target.id; | ||
| const targetResource = relationshipTargets[targetId]; | ||
|
|
||
| if (!targetResource) | ||
| return { ...v, resolvedValue: v.defaultValue }; | ||
|
|
||
| if (v.path.length === 0) | ||
| return { | ||
| ...v, | ||
| resolvedValue: get(targetResource, [], v.defaultValue), | ||
| }; | ||
|
|
||
| const metadataKey = v.path.join("/"); | ||
| const metadataValue = | ||
| metadataKey in targetResource.metadata | ||
| ? targetResource.metadata[metadataKey] | ||
| : undefined; | ||
|
|
||
| if (metadataValue !== undefined) | ||
| return { ...v, resolvedValue: metadataValue }; | ||
|
|
||
| return { | ||
| ...v, | ||
| resolvedValue: get(targetResource, v.path, v.defaultValue), | ||
| }; | ||
| }); | ||
|
|
||
| const metadata = Object.fromEntries( | ||
| t.metadata.map((m) => [m.key, m.value]), | ||
| ); | ||
| return { | ||
| ...t, | ||
| relationships, | ||
| directVariables, | ||
| referenceVariables, | ||
| metadata, | ||
| rules: await getResourceRelationshipRules(ctx.db, t.id), | ||
| }; | ||
| const directVariables = parsedVariables.filter( | ||
| (v): v is schema.DirectResourceVariable => v.valueType === "direct", | ||
| ); | ||
|
|
||
|
Comment on lines
+404
to
+407
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sensitive direct variables are returned encrypted In the webservice route ( const value = v.sensitive ? variablesAES256().decrypt(strval) : v.value;The TRPC Proposed fix: +import { variablesAES256 } from "@ctrlplane/secrets";
...
- const directVariables = parsedVariables.filter(
- (v): v is schema.DirectResourceVariable => v.valueType === "direct",
- );
+ const directVariables = parsedVariables
+ .filter(
+ (v): v is schema.DirectResourceVariable => v.valueType === "direct",
+ )
+ .map((v) => ({
+ ...v,
+ resolvedValue: v.sensitive
+ ? variablesAES256().decrypt(String(v.value))
+ : v.value,
+ }));This aligns both endpoints, avoids leaking cipher-text, and gives callers a uniform |
||
| const referenceVariables = parsedVariables.filter( | ||
| (v): v is schema.ReferenceResourceVariable => | ||
| v.valueType === "reference", | ||
| ); | ||
|
|
||
| const resolvedReferenceVariables = await Promise.all( | ||
| referenceVariables.map(async (v) => { | ||
| const resolvedValue = await getReferenceVariableValue(v); | ||
| return { ...v, resolvedValue }; | ||
| }), | ||
| ), | ||
| ); | ||
|
Comment on lines
+413
to
+418
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Gracefully degrade when a reference variable cannot be resolved Same resilience comment as for the REST handler: a single failing reference resolution should not break the whole response. |
||
|
|
||
| const metadata = Object.fromEntries( | ||
| resource.metadata.map((m) => [m.key, m.value]), | ||
| ); | ||
|
|
||
| return { | ||
| ...resource, | ||
| relationships, | ||
| directVariables, | ||
| referenceVariables: resolvedReferenceVariables, | ||
| metadata, | ||
| rules: await getResourceRelationshipRules(ctx.db, resource.id), | ||
| }; | ||
| }), | ||
|
|
||
| latestDeployedVersions: createTRPCRouter({ | ||
| byResourceAndEnvironmentId: protectedProcedure | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,27 @@ | ||
| import type * as schema from "@ctrlplane/db/schema"; | ||
| import _ from "lodash"; | ||
|
|
||
| import { db } from "@ctrlplane/db/client"; | ||
| import { getResourceParents } from "@ctrlplane/db/queries"; | ||
| import { logger } from "@ctrlplane/logger"; | ||
|
|
||
| export const getReferenceVariableValue = async ( | ||
| variable: schema.ReferenceResourceVariable, | ||
| ) => { | ||
| try { | ||
| const { relationships, getTargetsWithMetadata } = await getResourceParents( | ||
| db, | ||
| variable.resourceId, | ||
| ); | ||
| const relationshipTargets = await getTargetsWithMetadata(); | ||
|
|
||
| const targetId = relationships[variable.reference]?.target.id ?? ""; | ||
| const targetResource = relationshipTargets[targetId]; | ||
| if (targetResource == null) return variable.defaultValue; | ||
|
|
||
| return _.get(targetResource, variable.path, variable.defaultValue); | ||
| } catch (error) { | ||
| logger.error("Error resolving reference variable", { error, variable }); | ||
| return variable.defaultValue; | ||
| } | ||
| }; |
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.
🛠️ Refactor suggestion
Add defensive handling when resolving reference variables
getReferenceVariableValuelikely performs DB look-ups.If it throws (e.g., broken relationship, missing target, DB timeout) the whole handler will bubble a 500 response and the caller receives no partial data, even though we have a sensible
defaultValueon the variable object.Consider wrapping the call in a
try / catchand falling back tov.defaultValuewhile logging the incident for observability:This keeps the endpoint resilient and prevents a single bad reference from breaking the entire response.