Skip to content

Commit aea89d1

Browse files
committed
chore: stop using deprecated schemaValidator @W-20495414@
1 parent cc08eb8 commit aea89d1

File tree

7 files changed

+101
-144
lines changed

7 files changed

+101
-144
lines changed

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/importPlan.ts

Lines changed: 27 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,13 @@
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';
21+
import { isString } from '@salesforce/ts-types';
22+
import { Logger, Connection, Messages } from '@salesforce/core';
2423
import type { GenericObject, SObjectTreeInput } from '../../../types.js';
24+
import { DataImportPlanArraySchema, DataImportPlanArray } from '../../../schema/dataImportPlan.js';
2525
import type { DataPlanPartFilesOnly, ImportResult } from './importTypes.js';
2626
import {
2727
getResultsIfNoError,
@@ -56,14 +56,10 @@ export const importFromPlan = async (conn: Connection, planFilePath: string): Pr
5656
const logger = Logger.childFromRoot('data:import:tree:importFromPlan');
5757

5858
const planContents = await Promise.all(
59-
(
60-
await validatePlanContents(logger)(
61-
resolvedPlanPath,
62-
(await JSON.parse(fs.readFileSync(resolvedPlanPath, 'utf8'))) as DataPlanPartFilesOnly[]
59+
_validatePlanContents(logger, resolvedPlanPath, JSON.parse(fs.readFileSync(resolvedPlanPath, 'utf-8')))
60+
.flatMap((planPart) =>
61+
planPart.files.map((f) => ({ ...planPart, filePath: path.resolve(path.dirname(resolvedPlanPath), f) }))
6362
)
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) })))
6763
.map(async (i) => ({
6864
...i,
6965
records: parseDataFileContents(i.filePath)(await fs.promises.readFile(i.filePath, 'utf-8')),
@@ -189,54 +185,35 @@ const replaceRefWithId =
189185
Object.entries(record).map(([k, v]) => [k, v === `@${ref.refId}` ? ref.id : v])
190186
) as SObjectTreeInput;
191187

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-
);
188+
// eslint-disable-next-line no-underscore-dangle
189+
export function _validatePlanContents(logger: Logger, planPath: string, planContents: unknown): DataImportPlanArray {
190+
const childLogger: Logger = logger.child('validatePlanContents');
205191

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;
192+
const parseResults = DataImportPlanArraySchema.safeParse(planContents);
193+
194+
if (parseResults.error) {
195+
throw messages.createError('error.InvalidDataImport', [
196+
planPath,
197+
parseResults.error.issues.map((e) => e.message).join('\n'),
198+
]);
199+
}
200+
const parsedPlans: DataImportPlanArray = parseResults.data;
201+
202+
for (const parsedPlan of parsedPlans) {
203+
if (parsedPlan.saveRefs !== undefined || parsedPlan.resolveRefs !== undefined) {
204+
childLogger.warn(
205+
"The plan contains the 'saveRefs' and/or 'resolveRefs' properties. These properties will be ignored and can be removed."
206+
);
226207
}
227-
};
208+
}
209+
return parsedPlans;
210+
}
228211

229212
const matchesRefFilter =
230213
(unresolvedRefRegex: RegExp) =>
231214
(v: unknown): boolean =>
232215
typeof v === 'string' && unresolvedRefRegex.test(v);
233216

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-
240217
// TODO: change this implementation to use Object.groupBy when it's on all supported node versions
241218
const filterUnresolved = (
242219
records: SObjectTreeInput[]

src/api/data/tree/importTypes.ts

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +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';
1716
import type { DataPlanPart } from '../../../types.js';
1817

1918
/*
@@ -48,11 +47,6 @@ export type ResponseRefs = {
4847
referenceId: string;
4948
id: string;
5049
};
51-
export type ImportResults = {
52-
responseRefs?: ResponseRefs[];
53-
sobjectTypes?: Dictionary;
54-
errors?: string[];
55-
};
5650

5751
export type ImportResult = {
5852
refId: string;

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>;

test/api/data/tree/importPlan.test.ts

Lines changed: 20 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,14 @@
1616
/* eslint-disable camelcase */ // for salesforce __c style fields
1717

1818
import { expect, assert } from 'chai';
19-
import { shouldThrow } from '@salesforce/core/testSetup';
19+
import { shouldThrowSync } from '@salesforce/core/testSetup';
2020
import { Logger } from '@salesforce/core';
2121
import {
2222
replaceRefsInTheSameFile,
2323
EnrichedPlanPart,
2424
replaceRefs,
2525
fileSplitter,
26-
validatePlanContents,
26+
_validatePlanContents,
2727
} from '../../../../src/api/data/tree/importPlan.js';
2828

2929
describe('importPlan', () => {
@@ -124,8 +124,8 @@ describe('importPlan', () => {
124124
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
125125
logger.memoryLogger.loggedData = [];
126126
});
127-
const validator = validatePlanContents(logger);
128-
it('good plan in classic format', async () => {
127+
128+
it('good plan in classic format, one file', () => {
129129
const plan = [
130130
{
131131
sobject: 'Account',
@@ -134,10 +134,12 @@ describe('importPlan', () => {
134134
files: ['Account.json'],
135135
},
136136
];
137-
expect(await validator('some/path', plan)).to.equal(plan);
137+
138+
expect(_validatePlanContents(logger, 'some/path', plan)).to.deep.equal(plan);
138139
expect(getLogMessages(logger)).to.include('saveRefs');
139140
});
140-
it('good plan in classic format', async () => {
141+
142+
it('good plan in classic format, multiple files', () => {
141143
const plan = [
142144
{
143145
sobject: 'Account',
@@ -146,25 +148,28 @@ describe('importPlan', () => {
146148
files: ['Account.json', 'Account2.json'],
147149
},
148150
];
149-
expect(await validator('some/path', plan)).to.equal(plan);
151+
152+
expect(_validatePlanContents(logger, 'some/path', plan)).to.deep.equal(plan);
153+
expect(getLogMessages(logger)).to.include('saveRefs');
150154
});
151-
it('throws on bad plan (missing the object)', async () => {
155+
156+
it('throws on bad plan (missing sobject property)', () => {
152157
const plan = [
153158
{
154159
saveRefs: true,
155160
resolveRefs: true,
156161
files: ['Account.json', 'Account2.json'],
157162
},
158163
];
164+
159165
try {
160-
await shouldThrow(validator('some/path', plan));
166+
shouldThrowSync(() => _validatePlanContents(logger, 'some/path', plan));
161167
} catch (e) {
162168
assert(e instanceof Error);
163169
expect(e.name).to.equal('InvalidDataImportError');
164170
}
165171
});
166-
// TODO: remove this test when schema moves to simple files only
167-
it('throws when files are an object that meets current schema', async () => {
172+
it('throws when files property contains non-strings', () => {
168173
const plan = [
169174
{
170175
sobject: 'Account',
@@ -174,20 +179,20 @@ describe('importPlan', () => {
174179
},
175180
];
176181
try {
177-
await shouldThrow(validator('some/path', plan));
182+
shouldThrowSync(() => _validatePlanContents(logger, 'some/plan', plan));
178183
} catch (e) {
179184
assert(e instanceof Error);
180-
expect(e.name, JSON.stringify(e)).to.equal('NonStringFilesError');
185+
expect(e.message).to.include('The `files` property of the plan objects must contain only strings');
181186
}
182187
});
183-
it('good plan in new format is valid and produces no warnings', async () => {
188+
it('good plan in new format', () => {
184189
const plan = [
185190
{
186191
sobject: 'Account',
187192
files: ['Account.json'],
188193
},
189194
];
190-
expect(await validator('some/path', plan)).to.equal(plan);
195+
expect(_validatePlanContents(logger, 'some/path', plan)).to.deep.equal(plan);
191196
expect(getLogMessages(logger)).to.not.include('saveRefs');
192197
});
193198
});

yarn.lock

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7922,3 +7922,8 @@ zod@^4.1.12:
79227922
version "4.1.13"
79237923
resolved "https://registry.yarnpkg.com/zod/-/zod-4.1.13.tgz#93699a8afe937ba96badbb0ce8be6033c0a4b6b1"
79247924
integrity sha512-AvvthqfqrAhNH9dnfmrfKzX5upOdjUVJYFqNSlkmGf64gRaTzlPwz99IHYnVs28qYAybvAlBV+H7pn0saFY4Ig==
7925+
7926+
zod@^4.2.1:
7927+
version "4.2.1"
7928+
resolved "https://registry.yarnpkg.com/zod/-/zod-4.2.1.tgz#07f0388c7edbfd5f5a2466181cb4adf5b5dbd57b"
7929+
integrity sha512-0wZ1IRqGGhMP76gLqz8EyfBXKk0J2qo2+H3fi4mcUP/KtTocoX08nmIAHl1Z2kJIZbZee8KOpBCSNPRgauucjw==

0 commit comments

Comments
 (0)