Skip to content

Conversation

@jfeingold35
Copy link
Contributor

What does this PR do?

This PR replaces the plugin-data usage of the now-deprecated schemaValidator class from sfdx-core with a zod-based validation of the schema. This is apparently the last remaining reference to schemaValidator, clearing the way for us to eventually remove it.

What issues does this PR fix or reference?

@W-20495414@

) as SObjectTreeInput;

export const validatePlanContents =
(logger: Logger) =>
Copy link
Contributor

Choose a reason for hiding this comment

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

DataPlanPartFilesOnly shouldn't exist, it should be derived/inferred from the zod type.
and that schema should only have the real fields (not saveRefs/resolveRefs)

pro-tip: definitely read and understand the zod docs. by default, zod.object will strip unknown props. .strictObject will throw if any are present, and .looseObject will let them stay on the result.

so maybe warn about extra properties based on the planContents and then .object will strip them for you.

[FWIW, I'd also suggest using the actual CLI (oclif) warnings to help people migrate, rather than Logger which nobody will see]

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Okay, I didn't realize that part of the plan here was to remove the DataPlanPartFIlesOnly. I was trying to preserve the existing type definitions and behavior, changing only the implementation of the validation mechanism. If it's actually desirable to change those things, then I will do so. I will ask if I have any follow-up questions.

if (!hasOnlySimpleFiles(output)) {
throw messages.createError('error.NonStringFiles');
}
return Promise.resolve(planContents as DataPlanPartFilesOnly[]);
Copy link
Contributor

Choose a reason for hiding this comment

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

I will almost never approve an assertion in /src (I understand there's been one here for a really long time)

If we don't want to allow the DataPlanPart side of the TS union then let's block it, or manipulate it into the string[] that we expect.

The compiler here is trying to tell you that the types don't match the result type (zod definitely makes that clearer) and as is sweeping that under the rug.

Copy link
Contributor

@mshanemc mshanemc Dec 16, 2025

Choose a reason for hiding this comment

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

Or let me back up...the output of zod.parse/safeParse is a validated, typed object (why the TS world loves it so much). And you can use .infer to get that from the schema like https://zod.dev/basics?id=inferring-types

I'd suggest looking through the consuming code to see whether it handles both scenarios (string[] vs. DataImportFile) and

  • if yes, account for both types in this fn's return type (simplified if you return the .infer)
  • if not, then we massage it here to return the expected type.

Copy link
Contributor

Choose a reason for hiding this comment

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

we should also be able to get rid of this assertion above since it's being passed unknown for proper validation now.

(await JSON.parse(fs.readFileSync(resolvedPlanPath, 'utf8'))) as DataPlanPartFilesOnly[]

files: z
.array(
z
.string('The `files` property of the plan objects must contain only strings')
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Instead of having zod accept an object here and then having us throw an error later if that object is actually used, decided to just have zod reject with the same message.

Copy link
Contributor

Choose a reason for hiding this comment

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

the goals of the change are

  1. schemaValidator => zod
  2. warning user in the UI instead of the logs
  3. use proper types instead of as

it's not meant to be a breaking change (imagine someone's script doing a data load, been working fine since 2020, etc)

If someone were using the old style did this break them?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

My understanding is that it would not. The only thing that has changed is where the error is being thrown. Even the text of the error remains the same.

Copy link
Contributor

Choose a reason for hiding this comment

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

yeah, I think you're right. I'm good with this functionally, then; everything else is "how to code" stuff.

referenceId: string;
id: string;
};
export type ImportResults = {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This type was never being used anywhere.

Copy link
Contributor

Choose a reason for hiding this comment

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

cool. definitely get to know npx knip (we've included that in our cursor instructions within the extensions to avoid that kind of thing)

import { Logger, Connection, Messages } from '@salesforce/core';
import type { GenericObject, SObjectTreeInput } from '../../../types.js';
import { DataImportPlanArraySchema, DataImportPlanArray } from '../../../schema/dataImportPlan.js';
import type { DataPlanPartFilesOnly, ImportResult } from './importTypes.js';
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Next step is to fully remove the DataPlanPartFIlesOnly type, since now it's only used to define a different type.


// the "new" type for these. We're ignoring saveRefs/resolveRefs
export type EnrichedPlanPart = Omit<DataPlanPartFilesOnly, 'saveRefs' | 'resolveRefs'> & {
export type EnrichedPlanPart = Partial<DataPlanPart> & {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

The properties coming from Omit<DataPlanPartFilesOnly, 'saveRefs' | 'resolveRefs'> are all also hard-defined in this type here, except for this Partial, which I included as well.
Now that type serves no purpose and could be deleted.

export const importFromPlan = async (
conn: Connection,
planFilePath: string,
display: Display
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Adding a new Display parameter let me show the warning in the console instead of logging it to some never-looked-at file.

Copy link
Contributor

Choose a reason for hiding this comment

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

Copy link
Contributor

Choose a reason for hiding this comment

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

another nice thing you can do is user Lifecycle from sfdx-core with its warn feature. All the CLI plugins automatically send those to the output.

Copy link
Contributor

Choose a reason for hiding this comment

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

and one more option (the most test-friendly, and I think the best option) would be return an object of {result: ImportResult, warnings: string[]} so that

  1. it's a pure function, no mocks or stubs needed to test it
  2. the command, which has warn already, can do "ux things" and helpers like this don't need to think about ux, only data.


for (const parsedPlan of parsedPlans) {
if (parsedPlan.saveRefs !== undefined || parsedPlan.resolveRefs !== undefined) {
display.displayWarning(
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Using the display instead of the logger.

* (for example, via {@code console.log()}).
* The interface/implementation pattern allows for extremely easy mocking of dependencies in tests.
*/
export interface Display {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This paradigm is something @stephen-carter-at-sf developed for the Code Analyzer Team. It's easily mockable, which makes it really easy to test. If we ever (for some reason) need to add new methods, we can do that by putting them in the interface, and having the UxDisplay just invoke the corresponding method on the Displayable.

Copy link
Contributor

@mshanemc mshanemc Dec 17, 2025

Choose a reason for hiding this comment

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

Stephen probably didn't know about
https://github.com/salesforcecli/sf-plugins-core/blob/main/src/ux/ux.ts
and https://github.com/salesforcecli/sf-plugins-core/blob/main/src/stubUx.ts#L42.

I'd like for this not to exist. not only to use existing patterns with plugins, but to encourage other teams and AIs to do the same

meta feedback: when you think of somethign like this, ask on slack because often something exists.

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 look over this tomorrow to see how I would use it in place of the Display system.

Copy link
Contributor

Choose a reason for hiding this comment

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

it's good to know about, would still recommend returning {results, warnings} from the fn

}

export interface Displayable {
warn(message: string): void;
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Using the warn name allows us to have the tree command implement Displayable and pass itself into the constructor of the Display that it creates and passes through into validation.

Object.entries(record).map(([k, v]) => [k, v === `@${ref.refId}` ? ref.id : v])
) as SObjectTreeInput;

export const validatePlanContents =
Copy link
Contributor

Choose a reason for hiding this comment

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

meta: don't break the rule and then add the comment to allow you to break the rule unless you've got a really compelling reason (and preferably explain your reason!)

here: you can export it for test only, it won't cause it to be publicly available unless you export it from index.js or some other package.json#exports file (I'm trying to guess why you'd want to do that)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fair enough. I'm used to naming things that are exported solely for tests _blah instead of blah, because it makes it visually obvious to anyone reading the code that this method should be treated as private. The underscore signals 'oh, I shouldn't import this to whatever other code I'm writing'.
Part of my hesitance to futz with the code heavily the first time around was because so many things were exported. It wasn't immediatley obvious what was test-only and what wasn't.

Copy link
Contributor

Choose a reason for hiding this comment

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

Sure, we can talk about why that distinction is important.

@jfeingold35 jfeingold35 merged commit 815eb41 into main Dec 18, 2025
64 of 65 checks passed
@jfeingold35 jfeingold35 deleted the jf/W-20495414 branch December 18, 2025 15:29
@mshanemc
Copy link
Contributor

QA notes: (using ./bin/dev.js and the test/data-project stuff from this repo
✅ warning about unused props appears, looks good in yellow
✅ no warning without unused props
✅ error happens when you have bad schema
⚠️ it's kinda not clear what I did wrong (omit the files prop on one of the plan objects

Data plan file /Users/shane.mclaughlin/eng/salesforcecli/plugin-data/test/test-files/data-project/data/accounts-contacts-plan-object.json did not validate against the schema. Errors: Invalid input: expected array, received undefined.

zod error looks like this

error: ZodError: [
    {
      "expected": "array",
      "code": "invalid_type",
      "path": [
        0,
        "files"
      ],
      "message": "Invalid input: expected array, received undefined"
    }
  ]

so that path prop is important.

❌ when the schema validation fails, there's a reference to a command that no longer exists in the "try this" (was not caused by this PR)

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