From aea89d1862983c920580ef846bb22dcdadd7306d Mon Sep 17 00:00:00 2001 From: Joshua Feingold Date: Wed, 17 Dec 2025 10:48:24 -0600 Subject: [PATCH 1/9] chore: stop using deprecated schemaValidator @W-20495414@ --- package.json | 3 +- schema/dataImportPlanSchema.json | 72 ------------------------- src/api/data/tree/importPlan.ts | 77 ++++++++++----------------- src/api/data/tree/importTypes.ts | 6 --- src/schema/dataImportPlan.ts | 47 ++++++++++++++++ test/api/data/tree/importPlan.test.ts | 35 ++++++------ yarn.lock | 5 ++ 7 files changed, 101 insertions(+), 144 deletions(-) delete mode 100644 schema/dataImportPlanSchema.json create mode 100644 src/schema/dataImportPlan.ts diff --git a/package.json b/package.json index a706a40a..9e93991b 100644 --- a/package.json +++ b/package.json @@ -132,7 +132,8 @@ "csv-parse": "^5.6.0", "csv-stringify": "^6.6.0", "form-data": "^4.0.5", - "terminal-link": "^3.0.0" + "terminal-link": "^3.0.0", + "zod": "^4.2.1" }, "devDependencies": { "@oclif/core": "^4.8.0", diff --git a/schema/dataImportPlanSchema.json b/schema/dataImportPlanSchema.json deleted file mode 100644 index 066974d7..00000000 --- a/schema/dataImportPlanSchema.json +++ /dev/null @@ -1,72 +0,0 @@ -{ - "$comment": "Copyright (c) 2016, salesforce.com, inc. All rights reserved. Licensed under the BSD 3-Clause license. For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause", - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "array", - "title": "Data Import Plan", - "description": "Schema for data import plan JSON.", - "items": { - "type": "object", - "title": "SObject Type", - "description": "Definition of records to be insert per SObject Type", - "properties": { - "sobject": { - "type": "string", - "title": "Name of SObject", - "description": "Child file references must have SObject roots of this type" - }, - "saveRefs": { - "type": "boolean", - "title": "Save References", - "description": "Post-save, save references (Name/ID) to be used for reference replacement in subsequent saves. Applies to all data files for this SObject type.", - "default": false - }, - "resolveRefs": { - "type": "boolean", - "title": "Resolve References", - "description": "Pre-save, replace @ with ID from previous save. Applies to all data files for this SObject type.", - "default": false - }, - "files": { - "type": "array", - "title": "Files", - "description": "An array of files paths to load", - "items": { - "title": "Filepath.", - "description": "Filepath string or object to point to a JSON or XML file having data defined in SObject Tree format.", - "oneOf": [ - { - "type": "string" - }, - { - "type": "object", - "properties": { - "file": { - "type": "string", - "title": "Filepath schema", - "description": "Filepath to JSON or XML file having data defined in SObject Tree format" - }, - "contentType": { - "title": "Filepath schema.", - "description": "If data file extension is not .json or .xml, provide content type.", - "enum": ["application/json", "application/xml"] - }, - "saveRefs": { - "type": "boolean", - "title": "Save References", - "description": "Post-save, save references (Name/ID) to be used for reference replacement in subsequent saves. Overrides SObject-level 'saveRefs' setting." - }, - "resolveRefs": { - "type": "boolean", - "title": "Resolve References", - "description": "Pre-save, replace @ with ID from previous save. Overrides SObject-level 'replaceRefs' setting." - } - }, - "required": ["file"] - } - ] - } - } - }, - "required": ["sobject", "files"] - } -} diff --git a/src/api/data/tree/importPlan.ts b/src/api/data/tree/importPlan.ts index 6b394414..e19a2cd0 100644 --- a/src/api/data/tree/importPlan.ts +++ b/src/api/data/tree/importPlan.ts @@ -15,13 +15,13 @@ */ import path from 'node:path'; import { EOL } from 'node:os'; -import { fileURLToPath } from 'node:url'; import fs from 'node:fs'; import { createHash } from 'node:crypto'; -import { AnyJson, isString } from '@salesforce/ts-types'; -import { Logger, SchemaValidator, SfError, Connection, Messages } from '@salesforce/core'; +import { isString } from '@salesforce/ts-types'; +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'; import { getResultsIfNoError, @@ -56,14 +56,10 @@ export const importFromPlan = async (conn: Connection, planFilePath: string): Pr const logger = Logger.childFromRoot('data:import:tree:importFromPlan'); const planContents = await Promise.all( - ( - await validatePlanContents(logger)( - resolvedPlanPath, - (await JSON.parse(fs.readFileSync(resolvedPlanPath, 'utf8'))) as DataPlanPartFilesOnly[] + _validatePlanContents(logger, resolvedPlanPath, JSON.parse(fs.readFileSync(resolvedPlanPath, 'utf-8'))) + .flatMap((planPart) => + planPart.files.map((f) => ({ ...planPart, filePath: path.resolve(path.dirname(resolvedPlanPath), f) })) ) - ) - // there *shouldn't* be multiple files for the same sobject in a plan, but the legacy code allows that - .flatMap((dpp) => dpp.files.map((f) => ({ ...dpp, filePath: path.resolve(path.dirname(resolvedPlanPath), f) }))) .map(async (i) => ({ ...i, records: parseDataFileContents(i.filePath)(await fs.promises.readFile(i.filePath, 'utf-8')), @@ -189,54 +185,35 @@ const replaceRefWithId = Object.entries(record).map(([k, v]) => [k, v === `@${ref.refId}` ? ref.id : v]) ) as SObjectTreeInput; -export const validatePlanContents = - (logger: Logger) => - async (planPath: string, planContents: unknown): Promise => { - const childLogger = logger.child('validatePlanContents'); - const planSchema = path.join( - path.dirname(fileURLToPath(import.meta.url)), - '..', - '..', - '..', - '..', - 'schema', - 'dataImportPlanSchema.json' - ); +// eslint-disable-next-line no-underscore-dangle +export function _validatePlanContents(logger: Logger, planPath: string, planContents: unknown): DataImportPlanArray { + const childLogger: Logger = logger.child('validatePlanContents'); - const val = new SchemaValidator(childLogger, planSchema); - try { - await val.validate(planContents as AnyJson); - const output = planContents as DataPlanPartFilesOnly[]; - if (hasRefs(output)) { - childLogger.warn( - "The plan contains the 'saveRefs' and/or 'resolveRefs' properties. These properties will be ignored and can be removed." - ); - } - if (!hasOnlySimpleFiles(output)) { - throw messages.createError('error.NonStringFiles'); - } - return planContents as DataPlanPartFilesOnly[]; - } catch (err) { - if (err instanceof Error && err.name === 'ValidationSchemaFieldError') { - throw messages.createError('error.InvalidDataImport', [planPath, err.message]); - } else if (err instanceof Error) { - throw SfError.wrap(err); - } - throw err; + const parseResults = DataImportPlanArraySchema.safeParse(planContents); + + if (parseResults.error) { + throw messages.createError('error.InvalidDataImport', [ + planPath, + parseResults.error.issues.map((e) => e.message).join('\n'), + ]); + } + const parsedPlans: DataImportPlanArray = parseResults.data; + + for (const parsedPlan of parsedPlans) { + if (parsedPlan.saveRefs !== undefined || parsedPlan.resolveRefs !== undefined) { + childLogger.warn( + "The plan contains the 'saveRefs' and/or 'resolveRefs' properties. These properties will be ignored and can be removed." + ); } - }; + } + return parsedPlans; +} const matchesRefFilter = (unresolvedRefRegex: RegExp) => (v: unknown): boolean => typeof v === 'string' && unresolvedRefRegex.test(v); -const hasOnlySimpleFiles = (planParts: DataPlanPartFilesOnly[]): boolean => - planParts.every((p) => p.files.every((f) => typeof f === 'string')); - -const hasRefs = (planParts: DataPlanPartFilesOnly[]): boolean => - planParts.some((p) => p.saveRefs !== undefined || p.resolveRefs !== undefined); - // TODO: change this implementation to use Object.groupBy when it's on all supported node versions const filterUnresolved = ( records: SObjectTreeInput[] diff --git a/src/api/data/tree/importTypes.ts b/src/api/data/tree/importTypes.ts index 70aec7e7..74e59f66 100644 --- a/src/api/data/tree/importTypes.ts +++ b/src/api/data/tree/importTypes.ts @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import type { Dictionary } from '@salesforce/ts-types'; import type { DataPlanPart } from '../../../types.js'; /* @@ -48,11 +47,6 @@ export type ResponseRefs = { referenceId: string; id: string; }; -export type ImportResults = { - responseRefs?: ResponseRefs[]; - sobjectTypes?: Dictionary; - errors?: string[]; -}; export type ImportResult = { refId: string; diff --git a/src/schema/dataImportPlan.ts b/src/schema/dataImportPlan.ts new file mode 100644 index 00000000..7a7aa9f3 --- /dev/null +++ b/src/schema/dataImportPlan.ts @@ -0,0 +1,47 @@ +/* + * Copyright 2025, Salesforce, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { z } from 'zod'; + +export const DataImportPlanArraySchema = z + .array( + z.object({ + sobject: z.string().describe('Child file references must have SObject roots of this type'), + saveRefs: z + .boolean() + .optional() + .describe( + 'Post-save, save references (Name/ID) to be used for reference replacement in subsequent saves. Applies to all data files for this SObject type.' + ), + resolveRefs: z + .boolean() + .optional() + .describe( + 'Pre-save, replace @ with ID from previous save. Applies to all data files for this SObject type.' + ), + files: z + .array( + z + .string('The `files` property of the plan objects must contain only strings') + .describe( + 'Filepath string or object to point to a JSON or XML file having data defined in SObject Tree format.' + ) + ) + .describe('An array of files paths to load'), + }) + ) + .describe('Schema for data import plan JSON'); + +export type DataImportPlanArray = z.infer; diff --git a/test/api/data/tree/importPlan.test.ts b/test/api/data/tree/importPlan.test.ts index 3ff25e4b..2bc0417c 100644 --- a/test/api/data/tree/importPlan.test.ts +++ b/test/api/data/tree/importPlan.test.ts @@ -16,14 +16,14 @@ /* eslint-disable camelcase */ // for salesforce __c style fields import { expect, assert } from 'chai'; -import { shouldThrow } from '@salesforce/core/testSetup'; +import { shouldThrowSync } from '@salesforce/core/testSetup'; import { Logger } from '@salesforce/core'; import { replaceRefsInTheSameFile, EnrichedPlanPart, replaceRefs, fileSplitter, - validatePlanContents, + _validatePlanContents, } from '../../../../src/api/data/tree/importPlan.js'; describe('importPlan', () => { @@ -124,8 +124,8 @@ describe('importPlan', () => { // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access logger.memoryLogger.loggedData = []; }); - const validator = validatePlanContents(logger); - it('good plan in classic format', async () => { + + it('good plan in classic format, one file', () => { const plan = [ { sobject: 'Account', @@ -134,10 +134,12 @@ describe('importPlan', () => { files: ['Account.json'], }, ]; - expect(await validator('some/path', plan)).to.equal(plan); + + expect(_validatePlanContents(logger, 'some/path', plan)).to.deep.equal(plan); expect(getLogMessages(logger)).to.include('saveRefs'); }); - it('good plan in classic format', async () => { + + it('good plan in classic format, multiple files', () => { const plan = [ { sobject: 'Account', @@ -146,9 +148,12 @@ describe('importPlan', () => { files: ['Account.json', 'Account2.json'], }, ]; - expect(await validator('some/path', plan)).to.equal(plan); + + expect(_validatePlanContents(logger, 'some/path', plan)).to.deep.equal(plan); + expect(getLogMessages(logger)).to.include('saveRefs'); }); - it('throws on bad plan (missing the object)', async () => { + + it('throws on bad plan (missing sobject property)', () => { const plan = [ { saveRefs: true, @@ -156,15 +161,15 @@ describe('importPlan', () => { files: ['Account.json', 'Account2.json'], }, ]; + try { - await shouldThrow(validator('some/path', plan)); + shouldThrowSync(() => _validatePlanContents(logger, 'some/path', plan)); } catch (e) { assert(e instanceof Error); expect(e.name).to.equal('InvalidDataImportError'); } }); - // TODO: remove this test when schema moves to simple files only - it('throws when files are an object that meets current schema', async () => { + it('throws when files property contains non-strings', () => { const plan = [ { sobject: 'Account', @@ -174,20 +179,20 @@ describe('importPlan', () => { }, ]; try { - await shouldThrow(validator('some/path', plan)); + shouldThrowSync(() => _validatePlanContents(logger, 'some/plan', plan)); } catch (e) { assert(e instanceof Error); - expect(e.name, JSON.stringify(e)).to.equal('NonStringFilesError'); + expect(e.message).to.include('The `files` property of the plan objects must contain only strings'); } }); - it('good plan in new format is valid and produces no warnings', async () => { + it('good plan in new format', () => { const plan = [ { sobject: 'Account', files: ['Account.json'], }, ]; - expect(await validator('some/path', plan)).to.equal(plan); + expect(_validatePlanContents(logger, 'some/path', plan)).to.deep.equal(plan); expect(getLogMessages(logger)).to.not.include('saveRefs'); }); }); diff --git a/yarn.lock b/yarn.lock index 080e6888..04bd457f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7922,3 +7922,8 @@ zod@^4.1.12: version "4.1.13" resolved "https://registry.yarnpkg.com/zod/-/zod-4.1.13.tgz#93699a8afe937ba96badbb0ce8be6033c0a4b6b1" integrity sha512-AvvthqfqrAhNH9dnfmrfKzX5upOdjUVJYFqNSlkmGf64gRaTzlPwz99IHYnVs28qYAybvAlBV+H7pn0saFY4Ig== + +zod@^4.2.1: + version "4.2.1" + resolved "https://registry.yarnpkg.com/zod/-/zod-4.2.1.tgz#07f0388c7edbfd5f5a2466181cb4adf5b5dbd57b" + integrity sha512-0wZ1IRqGGhMP76gLqz8EyfBXKk0J2qo2+H3fi4mcUP/KtTocoX08nmIAHl1Z2kJIZbZee8KOpBCSNPRgauucjw== From b5985ca2597cf29a504442f7b517291c9afad88a Mon Sep 17 00:00:00 2001 From: Joshua Feingold Date: Wed, 17 Dec 2025 13:00:11 -0600 Subject: [PATCH 2/9] chore: removed DataPlanPartFileOnly type @W-20495414@ --- src/api/data/tree/importPlan.ts | 6 +++--- src/api/data/tree/importTypes.ts | 15 --------------- 2 files changed, 3 insertions(+), 18 deletions(-) diff --git a/src/api/data/tree/importPlan.ts b/src/api/data/tree/importPlan.ts index e19a2cd0..a54e1a59 100644 --- a/src/api/data/tree/importPlan.ts +++ b/src/api/data/tree/importPlan.ts @@ -20,9 +20,9 @@ import { createHash } from 'node:crypto'; import { isString } from '@salesforce/ts-types'; import { Logger, Connection, Messages } from '@salesforce/core'; -import type { GenericObject, SObjectTreeInput } from '../../../types.js'; +import type { DataPlanPart, GenericObject, SObjectTreeInput } from '../../../types.js'; import { DataImportPlanArraySchema, DataImportPlanArray } from '../../../schema/dataImportPlan.js'; -import type { DataPlanPartFilesOnly, ImportResult } from './importTypes.js'; +import type { ImportResult } from './importTypes.js'; import { getResultsIfNoError, parseDataFileContents, @@ -37,7 +37,7 @@ Messages.importMessagesDirectoryFromMetaUrl(import.meta.url); const messages = Messages.loadMessages('@salesforce/plugin-data', 'importApi'); // the "new" type for these. We're ignoring saveRefs/resolveRefs -export type EnrichedPlanPart = Omit & { +export type EnrichedPlanPart = Partial & { filePath: string; sobject: string; records: SObjectTreeInput[]; diff --git a/src/api/data/tree/importTypes.ts b/src/api/data/tree/importTypes.ts index 74e59f66..de89f37c 100644 --- a/src/api/data/tree/importTypes.ts +++ b/src/api/data/tree/importTypes.ts @@ -13,14 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import type { DataPlanPart } from '../../../types.js'; - -/* - * Copyright (c) 2023, salesforce.com, inc. - * All rights reserved. - * Licensed under the BSD 3-Clause license. - * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause - */ export type TreeResponse = TreeResponseSuccess | TreeResponseError; type TreeResponseSuccess = { @@ -53,10 +45,3 @@ export type ImportResult = { type: string; id: string; }; /** like the original DataPlanPart but without the non-string options inside files */ - -export type DataPlanPartFilesOnly = { - sobject: string; - files: string[]; - saveRefs: boolean; - resolveRefs: boolean; -} & Partial; From b88d79855beb5ea78884697812df6d37e9f71b41 Mon Sep 17 00:00:00 2001 From: Joshua Feingold Date: Wed, 17 Dec 2025 13:16:08 -0600 Subject: [PATCH 3/9] chore: warning is displayed instead of logged @W-20495414@ --- src/api/data/tree/importPlan.ts | 15 ++++++---- src/commands/data/import/tree.ts | 5 ++-- src/ux/Display.ts | 40 +++++++++++++++++++++++++++ test/api/data/tree/importPlan.test.ts | 39 ++++++++++---------------- test/stubs/stub-display.ts | 28 +++++++++++++++++++ 5 files changed, 95 insertions(+), 32 deletions(-) create mode 100644 src/ux/Display.ts create mode 100644 test/stubs/stub-display.ts diff --git a/src/api/data/tree/importPlan.ts b/src/api/data/tree/importPlan.ts index a54e1a59..3dc57492 100644 --- a/src/api/data/tree/importPlan.ts +++ b/src/api/data/tree/importPlan.ts @@ -22,6 +22,7 @@ import { isString } from '@salesforce/ts-types'; import { Logger, Connection, Messages } from '@salesforce/core'; import type { DataPlanPart, GenericObject, SObjectTreeInput } from '../../../types.js'; import { DataImportPlanArraySchema, DataImportPlanArray } from '../../../schema/dataImportPlan.js'; +import { Display } from '../../../ux/Display.js'; import type { ImportResult } from './importTypes.js'; import { getResultsIfNoError, @@ -51,12 +52,16 @@ type ResultsSoFar = { const TREE_API_LIMIT = 200; const refRegex = (object: string): RegExp => new RegExp(`^@${object}Ref\\d+$`); -export const importFromPlan = async (conn: Connection, planFilePath: string): Promise => { +export const importFromPlan = async ( + conn: Connection, + planFilePath: string, + display: Display +): Promise => { const resolvedPlanPath = path.resolve(process.cwd(), planFilePath); const logger = Logger.childFromRoot('data:import:tree:importFromPlan'); const planContents = await Promise.all( - _validatePlanContents(logger, resolvedPlanPath, JSON.parse(fs.readFileSync(resolvedPlanPath, 'utf-8'))) + _validatePlanContents(display, resolvedPlanPath, JSON.parse(fs.readFileSync(resolvedPlanPath, 'utf-8'))) .flatMap((planPart) => planPart.files.map((f) => ({ ...planPart, filePath: path.resolve(path.dirname(resolvedPlanPath), f) })) ) @@ -186,9 +191,7 @@ const replaceRefWithId = ) as SObjectTreeInput; // eslint-disable-next-line no-underscore-dangle -export function _validatePlanContents(logger: Logger, planPath: string, planContents: unknown): DataImportPlanArray { - const childLogger: Logger = logger.child('validatePlanContents'); - +export function _validatePlanContents(display: Display, planPath: string, planContents: unknown): DataImportPlanArray { const parseResults = DataImportPlanArraySchema.safeParse(planContents); if (parseResults.error) { @@ -201,7 +204,7 @@ export function _validatePlanContents(logger: Logger, planPath: string, planCont for (const parsedPlan of parsedPlans) { if (parsedPlan.saveRefs !== undefined || parsedPlan.resolveRefs !== undefined) { - childLogger.warn( + display.displayWarning( "The plan contains the 'saveRefs' and/or 'resolveRefs' properties. These properties will be ignored and can be removed." ); } diff --git a/src/commands/data/import/tree.ts b/src/commands/data/import/tree.ts index ac2551df..d4c1d776 100644 --- a/src/commands/data/import/tree.ts +++ b/src/commands/data/import/tree.ts @@ -19,6 +19,7 @@ import { SfCommand, Flags } from '@salesforce/sf-plugins-core'; import { ensureString, isObject } from '@salesforce/ts-types'; import { importFromPlan } from '../../../api/data/tree/importPlan.js'; import { importFromFiles } from '../../../api/data/tree/importFiles.js'; +import { Displayable, UxDisplay } from '../../../ux/Display.js'; import { orgFlags } from '../../../flags.js'; import type { ImportResult, TreeResponse } from '../../../api/data/tree/importTypes.js'; @@ -28,7 +29,7 @@ const messages = Messages.loadMessages('@salesforce/plugin-data', 'tree.import') /** * Command that provides data import capability via the SObject Tree Save API. */ -export default class Import extends SfCommand { +export default class Import extends SfCommand implements Displayable { public static readonly summary = messages.getMessage('summary'); public static readonly description = messages.getMessage('description'); public static readonly examples = messages.getMessages('examples'); @@ -62,7 +63,7 @@ export default class Import extends SfCommand { try { const results = flags.plan - ? await importFromPlan(conn, flags.plan) + ? await importFromPlan(conn, flags.plan, new UxDisplay(this)) : await importFromFiles(conn, flags.files ?? []); this.table({ diff --git a/src/ux/Display.ts b/src/ux/Display.ts new file mode 100644 index 00000000..787eb6a0 --- /dev/null +++ b/src/ux/Display.ts @@ -0,0 +1,40 @@ +/* + * Copyright 2025, Salesforce, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Interface that can be implemented by classes meant to display information directly to the user + * (for example, via {@code console.log()}). + * The interface/implementation pattern allows for extremely easy mocking of dependencies in tests. + */ +export interface Display { + displayWarning(msg: string): void; +} + +export interface Displayable { + warn(message: string): void; +} + +export class UxDisplay implements Display { + private readonly displayable: Displayable; + + public constructor(displayable: Displayable) { + this.displayable = displayable; + } + + public displayWarning(msg: string): void { + this.displayable.warn(msg); + } +} diff --git a/test/api/data/tree/importPlan.test.ts b/test/api/data/tree/importPlan.test.ts index 2bc0417c..1ae4bfc7 100644 --- a/test/api/data/tree/importPlan.test.ts +++ b/test/api/data/tree/importPlan.test.ts @@ -17,7 +17,7 @@ import { expect, assert } from 'chai'; import { shouldThrowSync } from '@salesforce/core/testSetup'; -import { Logger } from '@salesforce/core'; +import { StubDisplay } from '../../../stubs/stub-display.js'; import { replaceRefsInTheSameFile, EnrichedPlanPart, @@ -114,15 +114,10 @@ describe('importPlan', () => { }); }); describe('plan validation', () => { - // ensure no static rootLogger - // @ts-expect-error private stuff - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - Logger.rootLogger = undefined; - const logger = new Logger({ name: 'importPlanTest', useMemoryLogger: true }); - afterEach(() => { - // @ts-expect-error private stuff - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - logger.memoryLogger.loggedData = []; + let stubDisplay: StubDisplay; + + beforeEach(() => { + stubDisplay = new StubDisplay(); }); it('good plan in classic format, one file', () => { @@ -135,8 +130,9 @@ describe('importPlan', () => { }, ]; - expect(_validatePlanContents(logger, 'some/path', plan)).to.deep.equal(plan); - expect(getLogMessages(logger)).to.include('saveRefs'); + expect(_validatePlanContents(stubDisplay, 'some/path', plan)).to.deep.equal(plan); + expect(stubDisplay.getDisplayedWarnings()).to.be.length(1); + expect(stubDisplay.getDisplayedWarnings()[0]).to.include('saveRefs'); }); it('good plan in classic format, multiple files', () => { @@ -149,8 +145,9 @@ describe('importPlan', () => { }, ]; - expect(_validatePlanContents(logger, 'some/path', plan)).to.deep.equal(plan); - expect(getLogMessages(logger)).to.include('saveRefs'); + expect(_validatePlanContents(stubDisplay, 'some/path', plan)).to.deep.equal(plan); + expect(stubDisplay.getDisplayedWarnings()).to.be.length(1); + expect(stubDisplay.getDisplayedWarnings()[0]).to.include('saveRefs'); }); it('throws on bad plan (missing sobject property)', () => { @@ -163,7 +160,7 @@ describe('importPlan', () => { ]; try { - shouldThrowSync(() => _validatePlanContents(logger, 'some/path', plan)); + shouldThrowSync(() => _validatePlanContents(stubDisplay, 'some/path', plan)); } catch (e) { assert(e instanceof Error); expect(e.name).to.equal('InvalidDataImportError'); @@ -179,7 +176,7 @@ describe('importPlan', () => { }, ]; try { - shouldThrowSync(() => _validatePlanContents(logger, 'some/plan', plan)); + shouldThrowSync(() => _validatePlanContents(stubDisplay, 'some/plan', plan)); } catch (e) { assert(e instanceof Error); expect(e.message).to.include('The `files` property of the plan objects must contain only strings'); @@ -192,14 +189,8 @@ describe('importPlan', () => { files: ['Account.json'], }, ]; - expect(_validatePlanContents(logger, 'some/path', plan)).to.deep.equal(plan); - expect(getLogMessages(logger)).to.not.include('saveRefs'); + expect(_validatePlanContents(stubDisplay, 'some/path', plan)).to.deep.equal(plan); + expect(stubDisplay.getDisplayedWarnings()).to.be.length(0); }); }); }); - -const getLogMessages = (logger: Logger): string => - logger - .getBufferedRecords() - .map((i) => i.msg) - .join('/n'); diff --git a/test/stubs/stub-display.ts b/test/stubs/stub-display.ts new file mode 100644 index 00000000..25986c8d --- /dev/null +++ b/test/stubs/stub-display.ts @@ -0,0 +1,28 @@ +/* + * Copyright 2025, Salesforce, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { Display } from '../../src/ux/Display.js'; + +export class StubDisplay implements Display { + private readonly warningMessages: string[] = []; + + public displayWarning(msg: string): void { + this.warningMessages.push(msg); + } + + public getDisplayedWarnings(): string[] { + return this.warningMessages; + } +} From c458367f85b50fd3b235ba5413fe3b9bd5e15ac3 Mon Sep 17 00:00:00 2001 From: Joshua Feingold Date: Thu, 18 Dec 2025 08:36:55 -0600 Subject: [PATCH 4/9] chore: integrate feedback from code review @W-20495414@ --- src/api/data/tree/importFiles.ts | 9 ++++-- src/api/data/tree/importPlan.ts | 28 ++++++++++--------- src/api/data/tree/importTypes.ts | 5 ++++ src/commands/data/import/tree.ts | 10 ++++--- src/ux/Display.ts | 40 --------------------------- test/api/data/tree/importPlan.test.ts | 30 +++++++++----------- test/stubs/stub-display.ts | 28 ------------------- 7 files changed, 45 insertions(+), 105 deletions(-) delete mode 100644 src/ux/Display.ts delete mode 100644 test/stubs/stub-display.ts diff --git a/src/api/data/tree/importFiles.ts b/src/api/data/tree/importFiles.ts index e9638dd1..868fed42 100644 --- a/src/api/data/tree/importFiles.ts +++ b/src/api/data/tree/importFiles.ts @@ -25,7 +25,7 @@ import { getResultsIfNoError, transformRecordTypeEntries, } from './importCommon.js'; -import type { ImportResult, ResponseRefs, TreeResponse } from './importTypes.js'; +import type { ImportResult, ImportStatus, ResponseRefs, TreeResponse } from './importTypes.js'; import { hasUnresolvedRefs } from './functions.js'; export type FileInfo = { @@ -35,7 +35,7 @@ export type FileInfo = { sobject: string; }; -export const importFromFiles = async (conn: Connection, dataFilePaths: string[]): Promise => { +export const importFromFiles = async (conn: Connection, dataFilePaths: string[]): Promise => { const logger = Logger.childFromRoot('data:import:tree:importSObjectTreeFile'); const fileInfos = (await Promise.all(dataFilePaths.map(parseFile))).map(logFileInfo(logger)).map(validateNoRefs); await Promise.all(fileInfos.map(async (fi) => transformRecordTypeEntries(conn, fi.records))); @@ -43,7 +43,10 @@ export const importFromFiles = async (conn: Connection, dataFilePaths: string[]) const results = await Promise.allSettled( fileInfos.map((fi) => sendSObjectTreeRequest(conn)(fi.sobject)(JSON.stringify({ records: fi.records }))) ); - return results.map(getSuccessOrThrow).flatMap(getValueOrThrow(fileInfos)).map(addObjectTypes(refMap)); + return { + results: results.map(getSuccessOrThrow).flatMap(getValueOrThrow(fileInfos)).map(addObjectTypes(refMap)), + warnings: [], + }; }; const getSuccessOrThrow = (result: PromiseSettledResult): PromiseFulfilledResult => diff --git a/src/api/data/tree/importPlan.ts b/src/api/data/tree/importPlan.ts index 3dc57492..58535f49 100644 --- a/src/api/data/tree/importPlan.ts +++ b/src/api/data/tree/importPlan.ts @@ -22,8 +22,7 @@ import { isString } from '@salesforce/ts-types'; import { Logger, Connection, Messages } from '@salesforce/core'; import type { DataPlanPart, GenericObject, SObjectTreeInput } from '../../../types.js'; import { DataImportPlanArraySchema, DataImportPlanArray } from '../../../schema/dataImportPlan.js'; -import { Display } from '../../../ux/Display.js'; -import type { ImportResult } from './importTypes.js'; +import type { ImportResult, ImportStatus } from './importTypes.js'; import { getResultsIfNoError, parseDataFileContents, @@ -52,16 +51,14 @@ type ResultsSoFar = { const TREE_API_LIMIT = 200; const refRegex = (object: string): RegExp => new RegExp(`^@${object}Ref\\d+$`); -export const importFromPlan = async ( - conn: Connection, - planFilePath: string, - display: Display -): Promise => { +export const importFromPlan = async (conn: Connection, planFilePath: string): Promise => { const resolvedPlanPath = path.resolve(process.cwd(), planFilePath); const logger = Logger.childFromRoot('data:import:tree:importFromPlan'); - + const warnings: string[] = []; + const planResultObj = _validatePlanContents(resolvedPlanPath, JSON.parse(fs.readFileSync(resolvedPlanPath, 'utf-8'))); + warnings.push(...planResultObj.warnings); const planContents = await Promise.all( - _validatePlanContents(display, resolvedPlanPath, JSON.parse(fs.readFileSync(resolvedPlanPath, 'utf-8'))) + planResultObj.parsedPlans .flatMap((planPart) => planPart.files.map((f) => ({ ...planPart, filePath: path.resolve(path.dirname(resolvedPlanPath), f) })) ) @@ -73,7 +70,7 @@ export const importFromPlan = async ( // using recursion to sequentially send the requests so we get refs back from each round const { results } = await getResults(conn)(logger)({ results: [], fingerprints: new Set() })(planContents); - return results; + return { results, warnings }; }; /** recursively splits files (for size or unresolved refs) and makes API calls, storing results for subsequent calls */ @@ -191,7 +188,11 @@ const replaceRefWithId = ) as SObjectTreeInput; // eslint-disable-next-line no-underscore-dangle -export function _validatePlanContents(display: Display, planPath: string, planContents: unknown): DataImportPlanArray { +export function _validatePlanContents( + planPath: string, + planContents: unknown +): { parsedPlans: DataImportPlanArray; warnings: string[] } { + const warnings: string[] = []; const parseResults = DataImportPlanArraySchema.safeParse(planContents); if (parseResults.error) { @@ -204,12 +205,13 @@ export function _validatePlanContents(display: Display, planPath: string, planCo for (const parsedPlan of parsedPlans) { if (parsedPlan.saveRefs !== undefined || parsedPlan.resolveRefs !== undefined) { - display.displayWarning( + warnings.push( "The plan contains the 'saveRefs' and/or 'resolveRefs' properties. These properties will be ignored and can be removed." ); + break; } } - return parsedPlans; + return { parsedPlans, warnings }; } const matchesRefFilter = diff --git a/src/api/data/tree/importTypes.ts b/src/api/data/tree/importTypes.ts index de89f37c..6a988fc9 100644 --- a/src/api/data/tree/importTypes.ts +++ b/src/api/data/tree/importTypes.ts @@ -40,6 +40,11 @@ export type ResponseRefs = { id: string; }; +export type ImportStatus = { + results: ImportResult[]; + warnings: string[]; +}; + export type ImportResult = { refId: string; type: string; diff --git a/src/commands/data/import/tree.ts b/src/commands/data/import/tree.ts index d4c1d776..df82a142 100644 --- a/src/commands/data/import/tree.ts +++ b/src/commands/data/import/tree.ts @@ -19,7 +19,6 @@ import { SfCommand, Flags } from '@salesforce/sf-plugins-core'; import { ensureString, isObject } from '@salesforce/ts-types'; import { importFromPlan } from '../../../api/data/tree/importPlan.js'; import { importFromFiles } from '../../../api/data/tree/importFiles.js'; -import { Displayable, UxDisplay } from '../../../ux/Display.js'; import { orgFlags } from '../../../flags.js'; import type { ImportResult, TreeResponse } from '../../../api/data/tree/importTypes.js'; @@ -29,7 +28,7 @@ const messages = Messages.loadMessages('@salesforce/plugin-data', 'tree.import') /** * Command that provides data import capability via the SObject Tree Save API. */ -export default class Import extends SfCommand implements Displayable { +export default class Import extends SfCommand { public static readonly summary = messages.getMessage('summary'); public static readonly description = messages.getMessage('description'); public static readonly examples = messages.getMessages('examples'); @@ -62,9 +61,12 @@ export default class Import extends SfCommand implements Display const conn = flags['target-org'].getConnection(flags['api-version']); try { - const results = flags.plan - ? await importFromPlan(conn, flags.plan, new UxDisplay(this)) + const { results, warnings } = flags.plan + ? await importFromPlan(conn, flags.plan) : await importFromFiles(conn, flags.files ?? []); + for (const warning of warnings) { + this.warn(warning); + } this.table({ data: results, diff --git a/src/ux/Display.ts b/src/ux/Display.ts deleted file mode 100644 index 787eb6a0..00000000 --- a/src/ux/Display.ts +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright 2025, Salesforce, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -/** - * Interface that can be implemented by classes meant to display information directly to the user - * (for example, via {@code console.log()}). - * The interface/implementation pattern allows for extremely easy mocking of dependencies in tests. - */ -export interface Display { - displayWarning(msg: string): void; -} - -export interface Displayable { - warn(message: string): void; -} - -export class UxDisplay implements Display { - private readonly displayable: Displayable; - - public constructor(displayable: Displayable) { - this.displayable = displayable; - } - - public displayWarning(msg: string): void { - this.displayable.warn(msg); - } -} diff --git a/test/api/data/tree/importPlan.test.ts b/test/api/data/tree/importPlan.test.ts index 1ae4bfc7..8177bdeb 100644 --- a/test/api/data/tree/importPlan.test.ts +++ b/test/api/data/tree/importPlan.test.ts @@ -17,7 +17,6 @@ import { expect, assert } from 'chai'; import { shouldThrowSync } from '@salesforce/core/testSetup'; -import { StubDisplay } from '../../../stubs/stub-display.js'; import { replaceRefsInTheSameFile, EnrichedPlanPart, @@ -114,12 +113,6 @@ describe('importPlan', () => { }); }); describe('plan validation', () => { - let stubDisplay: StubDisplay; - - beforeEach(() => { - stubDisplay = new StubDisplay(); - }); - it('good plan in classic format, one file', () => { const plan = [ { @@ -130,9 +123,10 @@ describe('importPlan', () => { }, ]; - expect(_validatePlanContents(stubDisplay, 'some/path', plan)).to.deep.equal(plan); - expect(stubDisplay.getDisplayedWarnings()).to.be.length(1); - expect(stubDisplay.getDisplayedWarnings()[0]).to.include('saveRefs'); + const { parsedPlans, warnings } = _validatePlanContents('some/path', plan); + expect(parsedPlans).to.deep.equal(plan); + expect(warnings).to.be.length(1); + expect(warnings[0]).to.include('saveRefs'); }); it('good plan in classic format, multiple files', () => { @@ -145,9 +139,10 @@ describe('importPlan', () => { }, ]; - expect(_validatePlanContents(stubDisplay, 'some/path', plan)).to.deep.equal(plan); - expect(stubDisplay.getDisplayedWarnings()).to.be.length(1); - expect(stubDisplay.getDisplayedWarnings()[0]).to.include('saveRefs'); + const { parsedPlans, warnings } = _validatePlanContents('some/path', plan); + expect(parsedPlans).to.deep.equal(plan); + expect(warnings).to.be.length(1); + expect(warnings[0]).to.include('saveRefs'); }); it('throws on bad plan (missing sobject property)', () => { @@ -160,7 +155,7 @@ describe('importPlan', () => { ]; try { - shouldThrowSync(() => _validatePlanContents(stubDisplay, 'some/path', plan)); + shouldThrowSync(() => _validatePlanContents('some/path', plan)); } catch (e) { assert(e instanceof Error); expect(e.name).to.equal('InvalidDataImportError'); @@ -176,7 +171,7 @@ describe('importPlan', () => { }, ]; try { - shouldThrowSync(() => _validatePlanContents(stubDisplay, 'some/plan', plan)); + shouldThrowSync(() => _validatePlanContents('some/plan', plan)); } catch (e) { assert(e instanceof Error); expect(e.message).to.include('The `files` property of the plan objects must contain only strings'); @@ -189,8 +184,9 @@ describe('importPlan', () => { files: ['Account.json'], }, ]; - expect(_validatePlanContents(stubDisplay, 'some/path', plan)).to.deep.equal(plan); - expect(stubDisplay.getDisplayedWarnings()).to.be.length(0); + const { parsedPlans, warnings } = _validatePlanContents('some/path', plan); + expect(parsedPlans).to.deep.equal(plan); + expect(warnings).to.be.length(0); }); }); }); diff --git a/test/stubs/stub-display.ts b/test/stubs/stub-display.ts deleted file mode 100644 index 25986c8d..00000000 --- a/test/stubs/stub-display.ts +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Copyright 2025, Salesforce, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import { Display } from '../../src/ux/Display.js'; - -export class StubDisplay implements Display { - private readonly warningMessages: string[] = []; - - public displayWarning(msg: string): void { - this.warningMessages.push(msg); - } - - public getDisplayedWarnings(): string[] { - return this.warningMessages; - } -} From 981cd11622ccfcbee869f218fac9951f598874df Mon Sep 17 00:00:00 2001 From: Joshua Feingold Date: Thu, 18 Dec 2025 08:58:49 -0600 Subject: [PATCH 5/9] chore: removed leading underscore @W-20495414@ --- src/api/data/tree/importPlan.ts | 5 ++--- test/api/data/tree/importPlan.test.ts | 12 ++++++------ 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/src/api/data/tree/importPlan.ts b/src/api/data/tree/importPlan.ts index 58535f49..253bfe09 100644 --- a/src/api/data/tree/importPlan.ts +++ b/src/api/data/tree/importPlan.ts @@ -55,7 +55,7 @@ export const importFromPlan = async (conn: Connection, planFilePath: string): Pr const resolvedPlanPath = path.resolve(process.cwd(), planFilePath); const logger = Logger.childFromRoot('data:import:tree:importFromPlan'); const warnings: string[] = []; - const planResultObj = _validatePlanContents(resolvedPlanPath, JSON.parse(fs.readFileSync(resolvedPlanPath, 'utf-8'))); + const planResultObj = validatePlanContents(resolvedPlanPath, JSON.parse(fs.readFileSync(resolvedPlanPath, 'utf-8'))); warnings.push(...planResultObj.warnings); const planContents = await Promise.all( planResultObj.parsedPlans @@ -187,8 +187,7 @@ const replaceRefWithId = Object.entries(record).map(([k, v]) => [k, v === `@${ref.refId}` ? ref.id : v]) ) as SObjectTreeInput; -// eslint-disable-next-line no-underscore-dangle -export function _validatePlanContents( +export function validatePlanContents( planPath: string, planContents: unknown ): { parsedPlans: DataImportPlanArray; warnings: string[] } { diff --git a/test/api/data/tree/importPlan.test.ts b/test/api/data/tree/importPlan.test.ts index 8177bdeb..3a8daefd 100644 --- a/test/api/data/tree/importPlan.test.ts +++ b/test/api/data/tree/importPlan.test.ts @@ -22,7 +22,7 @@ import { EnrichedPlanPart, replaceRefs, fileSplitter, - _validatePlanContents, + validatePlanContents, } from '../../../../src/api/data/tree/importPlan.js'; describe('importPlan', () => { @@ -123,7 +123,7 @@ describe('importPlan', () => { }, ]; - const { parsedPlans, warnings } = _validatePlanContents('some/path', plan); + const { parsedPlans, warnings } = validatePlanContents('some/path', plan); expect(parsedPlans).to.deep.equal(plan); expect(warnings).to.be.length(1); expect(warnings[0]).to.include('saveRefs'); @@ -139,7 +139,7 @@ describe('importPlan', () => { }, ]; - const { parsedPlans, warnings } = _validatePlanContents('some/path', plan); + const { parsedPlans, warnings } = validatePlanContents('some/path', plan); expect(parsedPlans).to.deep.equal(plan); expect(warnings).to.be.length(1); expect(warnings[0]).to.include('saveRefs'); @@ -155,7 +155,7 @@ describe('importPlan', () => { ]; try { - shouldThrowSync(() => _validatePlanContents('some/path', plan)); + shouldThrowSync(() => validatePlanContents('some/path', plan)); } catch (e) { assert(e instanceof Error); expect(e.name).to.equal('InvalidDataImportError'); @@ -171,7 +171,7 @@ describe('importPlan', () => { }, ]; try { - shouldThrowSync(() => _validatePlanContents('some/plan', plan)); + shouldThrowSync(() => validatePlanContents('some/plan', plan)); } catch (e) { assert(e instanceof Error); expect(e.message).to.include('The `files` property of the plan objects must contain only strings'); @@ -184,7 +184,7 @@ describe('importPlan', () => { files: ['Account.json'], }, ]; - const { parsedPlans, warnings } = _validatePlanContents('some/path', plan); + const { parsedPlans, warnings } = validatePlanContents('some/path', plan); expect(parsedPlans).to.deep.equal(plan); expect(warnings).to.be.length(0); }); From ce091226213d7e045a6ec9b3fa9baca90b8c7b01 Mon Sep 17 00:00:00 2001 From: mshanemc Date: Thu, 18 Dec 2025 09:52:07 -0600 Subject: [PATCH 6/9] fix: error actions had old command name --- messages/importApi.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/messages/importApi.md b/messages/importApi.md index b06c76da..65085753 100644 --- a/messages/importApi.md +++ b/messages/importApi.md @@ -20,7 +20,7 @@ Data plan file %s did not validate against the schema. Errors: %s. - Make sure you're importing a plan definition file. -- Get help with the import plan schema by running "sf data import beta tree --help". +- Get help with the import plan schema by running "sf data import tree --help". # error.NonStringFiles From 5d6d6bae859772d5db2b50a251ae1eaa5abe97c0 Mon Sep 17 00:00:00 2001 From: mshanemc Date: Thu, 18 Dec 2025 09:57:47 -0600 Subject: [PATCH 7/9] refactor: align types with zod schema --- src/api/data/tree/importPlan.ts | 3 +-- src/types.ts | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/api/data/tree/importPlan.ts b/src/api/data/tree/importPlan.ts index 253bfe09..ba8051e9 100644 --- a/src/api/data/tree/importPlan.ts +++ b/src/api/data/tree/importPlan.ts @@ -37,9 +37,8 @@ Messages.importMessagesDirectoryFromMetaUrl(import.meta.url); const messages = Messages.loadMessages('@salesforce/plugin-data', 'importApi'); // the "new" type for these. We're ignoring saveRefs/resolveRefs -export type EnrichedPlanPart = Partial & { +export type EnrichedPlanPart = DataPlanPart & { filePath: string; - sobject: string; records: SObjectTreeInput[]; }; /** an accumulator for api results. Fingerprints exist to break recursion */ diff --git a/src/types.ts b/src/types.ts index 48af7583..d3914180 100644 --- a/src/types.ts +++ b/src/types.ts @@ -62,7 +62,7 @@ export type SObjectTreeInput = Omit & { }; export type DataPlanPart = { sobject: string; - files: Array; + files: string[]; }; export type SObjectTreeFileContents = { From c5014a2b3bc619abf64c02500bccb91e079840f9 Mon Sep 17 00:00:00 2001 From: mshanemc Date: Thu, 18 Dec 2025 10:11:55 -0600 Subject: [PATCH 8/9] fix: improve error message for schema problems --- src/api/data/tree/importPlan.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/api/data/tree/importPlan.ts b/src/api/data/tree/importPlan.ts index ba8051e9..d27e7560 100644 --- a/src/api/data/tree/importPlan.ts +++ b/src/api/data/tree/importPlan.ts @@ -50,6 +50,7 @@ type ResultsSoFar = { const TREE_API_LIMIT = 200; const refRegex = (object: string): RegExp => new RegExp(`^@${object}Ref\\d+$`); + export const importFromPlan = async (conn: Connection, planFilePath: string): Promise => { const resolvedPlanPath = path.resolve(process.cwd(), planFilePath); const logger = Logger.childFromRoot('data:import:tree:importFromPlan'); @@ -194,9 +195,10 @@ export function validatePlanContents( const parseResults = DataImportPlanArraySchema.safeParse(planContents); if (parseResults.error) { + // console.log(parseResults); throw messages.createError('error.InvalidDataImport', [ planPath, - parseResults.error.issues.map((e) => e.message).join('\n'), + parseResults.error.issues.map((e) => JSON.stringify(e, null, 2)).join('\n'), ]); } const parsedPlans: DataImportPlanArray = parseResults.data; From 84b7a9769a9472a50626397fd68170966dd2186f Mon Sep 17 00:00:00 2001 From: mshanemc Date: Thu, 18 Dec 2025 10:52:11 -0600 Subject: [PATCH 9/9] chore: comment removal --- src/api/data/tree/importPlan.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/api/data/tree/importPlan.ts b/src/api/data/tree/importPlan.ts index d27e7560..6978532d 100644 --- a/src/api/data/tree/importPlan.ts +++ b/src/api/data/tree/importPlan.ts @@ -195,7 +195,6 @@ export function validatePlanContents( const parseResults = DataImportPlanArraySchema.safeParse(planContents); if (parseResults.error) { - // console.log(parseResults); throw messages.createError('error.InvalidDataImport', [ planPath, parseResults.error.issues.map((e) => JSON.stringify(e, null, 2)).join('\n'),