1515 */
1616import path from 'node:path' ;
1717import { EOL } from 'node:os' ;
18- import { fileURLToPath } from 'node:url' ;
1918import fs from 'node:fs' ;
2019import { 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' ;
2626import {
2727 getResultsIfNoError ,
2828 parseDataFileContents ,
@@ -37,7 +37,7 @@ Messages.importMessagesDirectoryFromMetaUrl(import.meta.url);
3737const 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 = {
5151const TREE_API_LIMIT = 200 ;
5252
5353const 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
229216const 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
241222const filterUnresolved = (
242223 records : SObjectTreeInput [ ]
0 commit comments