Skip to content

Commit 815eb41

Browse files
authored
Merge pull request #1362 from salesforcecli/jf/W-20495414
chore: removing dependency on deprecated schemaValidator @W-20495414@
1 parent cc08eb8 commit 815eb41

9 files changed

Lines changed: 131 additions & 187 deletions

File tree

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,8 @@
132132
"csv-parse": "^5.6.0",
133133
"csv-stringify": "^6.6.0",
134134
"form-data": "^4.0.5",
135-
"terminal-link": "^3.0.0"
135+
"terminal-link": "^3.0.0",
136+
"zod": "^4.2.1"
136137
},
137138
"devDependencies": {
138139
"@oclif/core": "^4.8.0",

schema/dataImportPlanSchema.json

Lines changed: 0 additions & 72 deletions
This file was deleted.

src/api/data/tree/importFiles.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ import {
2525
getResultsIfNoError,
2626
transformRecordTypeEntries,
2727
} from './importCommon.js';
28-
import type { ImportResult, ResponseRefs, TreeResponse } from './importTypes.js';
28+
import type { ImportResult, ImportStatus, ResponseRefs, TreeResponse } from './importTypes.js';
2929
import { hasUnresolvedRefs } from './functions.js';
3030

3131
export type FileInfo = {
@@ -35,15 +35,18 @@ export type FileInfo = {
3535
sobject: string;
3636
};
3737

38-
export const importFromFiles = async (conn: Connection, dataFilePaths: string[]): Promise<ImportResult[]> => {
38+
export const importFromFiles = async (conn: Connection, dataFilePaths: string[]): Promise<ImportStatus> => {
3939
const logger = Logger.childFromRoot('data:import:tree:importSObjectTreeFile');
4040
const fileInfos = (await Promise.all(dataFilePaths.map(parseFile))).map(logFileInfo(logger)).map(validateNoRefs);
4141
await Promise.all(fileInfos.map(async (fi) => transformRecordTypeEntries(conn, fi.records)));
4242
const refMap = createSObjectTypeMap(fileInfos.flatMap((fi) => fi.records));
4343
const results = await Promise.allSettled(
4444
fileInfos.map((fi) => sendSObjectTreeRequest(conn)(fi.sobject)(JSON.stringify({ records: fi.records })))
4545
);
46-
return results.map(getSuccessOrThrow).flatMap(getValueOrThrow(fileInfos)).map(addObjectTypes(refMap));
46+
return {
47+
results: results.map(getSuccessOrThrow).flatMap(getValueOrThrow(fileInfos)).map(addObjectTypes(refMap)),
48+
warnings: [],
49+
};
4750
};
4851

4952
const getSuccessOrThrow = (result: PromiseSettledResult<TreeResponse>): PromiseFulfilledResult<TreeResponse> =>

src/api/data/tree/importPlan.ts

Lines changed: 38 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,14 @@
1515
*/
1616
import path from 'node:path';
1717
import { EOL } from 'node:os';
18-
import { fileURLToPath } from 'node:url';
1918
import fs from 'node:fs';
2019
import { createHash } from 'node:crypto';
2120

22-
import { AnyJson, isString } from '@salesforce/ts-types';
23-
import { Logger, SchemaValidator, SfError, Connection, Messages } from '@salesforce/core';
24-
import type { GenericObject, SObjectTreeInput } from '../../../types.js';
25-
import type { DataPlanPartFilesOnly, ImportResult } from './importTypes.js';
21+
import { isString } from '@salesforce/ts-types';
22+
import { Logger, Connection, Messages } from '@salesforce/core';
23+
import type { DataPlanPart, GenericObject, SObjectTreeInput } from '../../../types.js';
24+
import { DataImportPlanArraySchema, DataImportPlanArray } from '../../../schema/dataImportPlan.js';
25+
import type { ImportResult, ImportStatus } from './importTypes.js';
2626
import {
2727
getResultsIfNoError,
2828
parseDataFileContents,
@@ -37,7 +37,7 @@ Messages.importMessagesDirectoryFromMetaUrl(import.meta.url);
3737
const messages = Messages.loadMessages('@salesforce/plugin-data', 'importApi');
3838

3939
// the "new" type for these. We're ignoring saveRefs/resolveRefs
40-
export type EnrichedPlanPart = Omit<DataPlanPartFilesOnly, 'saveRefs' | 'resolveRefs'> & {
40+
export type EnrichedPlanPart = Partial<DataPlanPart> & {
4141
filePath: string;
4242
sobject: string;
4343
records: SObjectTreeInput[];
@@ -51,19 +51,17 @@ type ResultsSoFar = {
5151
const TREE_API_LIMIT = 200;
5252

5353
const refRegex = (object: string): RegExp => new RegExp(`^@${object}Ref\\d+$`);
54-
export const importFromPlan = async (conn: Connection, planFilePath: string): Promise<ImportResult[]> => {
54+
export const importFromPlan = async (conn: Connection, planFilePath: string): Promise<ImportStatus> => {
5555
const resolvedPlanPath = path.resolve(process.cwd(), planFilePath);
5656
const logger = Logger.childFromRoot('data:import:tree:importFromPlan');
57-
57+
const warnings: string[] = [];
58+
const planResultObj = validatePlanContents(resolvedPlanPath, JSON.parse(fs.readFileSync(resolvedPlanPath, 'utf-8')));
59+
warnings.push(...planResultObj.warnings);
5860
const planContents = await Promise.all(
59-
(
60-
await validatePlanContents(logger)(
61-
resolvedPlanPath,
62-
(await JSON.parse(fs.readFileSync(resolvedPlanPath, 'utf8'))) as DataPlanPartFilesOnly[]
61+
planResultObj.parsedPlans
62+
.flatMap((planPart) =>
63+
planPart.files.map((f) => ({ ...planPart, filePath: path.resolve(path.dirname(resolvedPlanPath), f) }))
6364
)
64-
)
65-
// there *shouldn't* be multiple files for the same sobject in a plan, but the legacy code allows that
66-
.flatMap((dpp) => dpp.files.map((f) => ({ ...dpp, filePath: path.resolve(path.dirname(resolvedPlanPath), f) })))
6765
.map(async (i) => ({
6866
...i,
6967
records: parseDataFileContents(i.filePath)(await fs.promises.readFile(i.filePath, 'utf-8')),
@@ -72,7 +70,7 @@ export const importFromPlan = async (conn: Connection, planFilePath: string): Pr
7270
// using recursion to sequentially send the requests so we get refs back from each round
7371
const { results } = await getResults(conn)(logger)({ results: [], fingerprints: new Set() })(planContents);
7472

75-
return results;
73+
return { results, warnings };
7674
};
7775

7876
/** recursively splits files (for size or unresolved refs) and makes API calls, storing results for subsequent calls */
@@ -189,54 +187,37 @@ const replaceRefWithId =
189187
Object.entries(record).map(([k, v]) => [k, v === `@${ref.refId}` ? ref.id : v])
190188
) as SObjectTreeInput;
191189

192-
export const validatePlanContents =
193-
(logger: Logger) =>
194-
async (planPath: string, planContents: unknown): Promise<DataPlanPartFilesOnly[]> => {
195-
const childLogger = logger.child('validatePlanContents');
196-
const planSchema = path.join(
197-
path.dirname(fileURLToPath(import.meta.url)),
198-
'..',
199-
'..',
200-
'..',
201-
'..',
202-
'schema',
203-
'dataImportPlanSchema.json'
204-
);
205-
206-
const val = new SchemaValidator(childLogger, planSchema);
207-
try {
208-
await val.validate(planContents as AnyJson);
209-
const output = planContents as DataPlanPartFilesOnly[];
210-
if (hasRefs(output)) {
211-
childLogger.warn(
212-
"The plan contains the 'saveRefs' and/or 'resolveRefs' properties. These properties will be ignored and can be removed."
213-
);
214-
}
215-
if (!hasOnlySimpleFiles(output)) {
216-
throw messages.createError('error.NonStringFiles');
217-
}
218-
return planContents as DataPlanPartFilesOnly[];
219-
} catch (err) {
220-
if (err instanceof Error && err.name === 'ValidationSchemaFieldError') {
221-
throw messages.createError('error.InvalidDataImport', [planPath, err.message]);
222-
} else if (err instanceof Error) {
223-
throw SfError.wrap(err);
224-
}
225-
throw err;
190+
export function validatePlanContents(
191+
planPath: string,
192+
planContents: unknown
193+
): { parsedPlans: DataImportPlanArray; warnings: string[] } {
194+
const warnings: string[] = [];
195+
const parseResults = DataImportPlanArraySchema.safeParse(planContents);
196+
197+
if (parseResults.error) {
198+
throw messages.createError('error.InvalidDataImport', [
199+
planPath,
200+
parseResults.error.issues.map((e) => e.message).join('\n'),
201+
]);
202+
}
203+
const parsedPlans: DataImportPlanArray = parseResults.data;
204+
205+
for (const parsedPlan of parsedPlans) {
206+
if (parsedPlan.saveRefs !== undefined || parsedPlan.resolveRefs !== undefined) {
207+
warnings.push(
208+
"The plan contains the 'saveRefs' and/or 'resolveRefs' properties. These properties will be ignored and can be removed."
209+
);
210+
break;
226211
}
227-
};
212+
}
213+
return { parsedPlans, warnings };
214+
}
228215

229216
const matchesRefFilter =
230217
(unresolvedRefRegex: RegExp) =>
231218
(v: unknown): boolean =>
232219
typeof v === 'string' && unresolvedRefRegex.test(v);
233220

234-
const hasOnlySimpleFiles = (planParts: DataPlanPartFilesOnly[]): boolean =>
235-
planParts.every((p) => p.files.every((f) => typeof f === 'string'));
236-
237-
const hasRefs = (planParts: DataPlanPartFilesOnly[]): boolean =>
238-
planParts.some((p) => p.saveRefs !== undefined || p.resolveRefs !== undefined);
239-
240221
// TODO: change this implementation to use Object.groupBy when it's on all supported node versions
241222
const filterUnresolved = (
242223
records: SObjectTreeInput[]

src/api/data/tree/importTypes.ts

Lines changed: 4 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -13,15 +13,6 @@
1313
* See the License for the specific language governing permissions and
1414
* limitations under the License.
1515
*/
16-
import type { Dictionary } from '@salesforce/ts-types';
17-
import type { DataPlanPart } from '../../../types.js';
18-
19-
/*
20-
* Copyright (c) 2023, salesforce.com, inc.
21-
* All rights reserved.
22-
* Licensed under the BSD 3-Clause license.
23-
* For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
24-
*/
2516
export type TreeResponse = TreeResponseSuccess | TreeResponseError;
2617

2718
type TreeResponseSuccess = {
@@ -48,21 +39,14 @@ export type ResponseRefs = {
4839
referenceId: string;
4940
id: string;
5041
};
51-
export type ImportResults = {
52-
responseRefs?: ResponseRefs[];
53-
sobjectTypes?: Dictionary;
54-
errors?: string[];
42+
43+
export type ImportStatus = {
44+
results: ImportResult[];
45+
warnings: string[];
5546
};
5647

5748
export type ImportResult = {
5849
refId: string;
5950
type: string;
6051
id: string;
6152
}; /** like the original DataPlanPart but without the non-string options inside files */
62-
63-
export type DataPlanPartFilesOnly = {
64-
sobject: string;
65-
files: string[];
66-
saveRefs: boolean;
67-
resolveRefs: boolean;
68-
} & Partial<DataPlanPart>;

src/commands/data/import/tree.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,9 +61,12 @@ export default class Import extends SfCommand<ImportResult[]> {
6161
const conn = flags['target-org'].getConnection(flags['api-version']);
6262

6363
try {
64-
const results = flags.plan
64+
const { results, warnings } = flags.plan
6565
? await importFromPlan(conn, flags.plan)
6666
: await importFromFiles(conn, flags.files ?? []);
67+
for (const warning of warnings) {
68+
this.warn(warning);
69+
}
6770

6871
this.table({
6972
data: results,

src/schema/dataImportPlan.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
/*
2+
* Copyright 2025, Salesforce, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
import { z } from 'zod';
17+
18+
export const DataImportPlanArraySchema = z
19+
.array(
20+
z.object({
21+
sobject: z.string().describe('Child file references must have SObject roots of this type'),
22+
saveRefs: z
23+
.boolean()
24+
.optional()
25+
.describe(
26+
'Post-save, save references (Name/ID) to be used for reference replacement in subsequent saves. Applies to all data files for this SObject type.'
27+
),
28+
resolveRefs: z
29+
.boolean()
30+
.optional()
31+
.describe(
32+
'Pre-save, replace @<reference> with ID from previous save. Applies to all data files for this SObject type.'
33+
),
34+
files: z
35+
.array(
36+
z
37+
.string('The `files` property of the plan objects must contain only strings')
38+
.describe(
39+
'Filepath string or object to point to a JSON or XML file having data defined in SObject Tree format.'
40+
)
41+
)
42+
.describe('An array of files paths to load'),
43+
})
44+
)
45+
.describe('Schema for data import plan JSON');
46+
47+
export type DataImportPlanArray = z.infer<typeof DataImportPlanArraySchema>;

0 commit comments

Comments
 (0)