Skip to content

Commit 8d905fa

Browse files
szluzeroodacrem
authored andcommitted
Add error handling for store copy --key
1 parent 797a9d3 commit 8d905fa

File tree

5 files changed

+223
-55
lines changed

5 files changed

+223
-55
lines changed

mytest.sh

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
#!/usr/bin/env bash
2+
3+
# Initialize arrays to store successful and failed commands
4+
SUCCESSES=()
5+
FAILURES=()
6+
7+
# Function to execute a command, track its success/failure, and print immediate feedback
8+
expect_success() {
9+
local command_string="$1"
10+
echo "Executing: ${command_string}" # Print the command being executed
11+
12+
if eval "$command_string"; then
13+
echo "${command_string}" # Indicate success
14+
SUCCESSES+=("${command_string}") # Add to successes array
15+
else
16+
echo "${command_string}" # Indicate failure
17+
FAILURES+=("${command_string}") # Add to failures array
18+
fi
19+
}
20+
pnpm nx run store:build
21+
# Define the base command for the CLI tool
22+
CMD="node packages/cli/bin/dev.js"
23+
24+
# --- Command Execution Section ---
25+
26+
# Clear the screen before starting
27+
clear
28+
29+
# Command 1: Display help for 'store copy'
30+
cmd="$CMD store copy --help"
31+
expect_success "$cmd"
32+
sleep 1 # Reduced sleep for faster execution during testing
33+
34+
# Clear the screen
35+
clear
36+
37+
# Command 2: Copy from file to store with mock and force-yes
38+
cmd="$CMD store copy --from-file=foo.sqlite --to-store=source.myshopify.com --mock -y"
39+
expect_success "$cmd"
40+
sleep 1
41+
42+
# Clear the screen
43+
clear
44+
45+
# Command 3: Copy with specific key (products:metafield:custom_id:erp_id)
46+
cmd="$CMD store copy --from-file=foo.sqlite --to-store=source.myshopify.com --key=products:metafield:custom_id:erp_id --mock -y"
47+
expect_success "$cmd"
48+
sleep 1
49+
50+
# Clear the screen
51+
clear
52+
53+
# Command 4: Copy with specific key (products:handle)
54+
cmd="$CMD store copy --from-file=foo.sqlite --to-store=source.myshopify.com --key=products:handle --mock -y"
55+
expect_success "$cmd"
56+
sleep 1
57+
58+
# Clear the screen
59+
clear
60+
61+
# Command 5: Copy with specific key (products:title)
62+
cmd="$CMD store copy --from-file=foo.sqlite --to-store=source.myshopify.com --key=products:title --mock -y"
63+
expect_success "$cmd"
64+
sleep 1
65+
66+
# Clear the screen
67+
clear
68+
69+
# Command 6: Copy with specific key (products:handle) - duplicate of Command 4
70+
cmd="$CMD store copy --from-file=foo.sqlite --to-store=source.myshopify.com --key=products:handle --mock -y"
71+
expect_success "$cmd"
72+
sleep 1
73+
74+
# Clear the screen
75+
clear
76+
77+
# Command 7: Copy from file to store with mock and force-yes - duplicate of Command 2
78+
cmd="$CMD store copy --from-file=foo.sqlite --to-store=source.myshopify.com --mock -y"
79+
expect_success "$cmd"
80+
sleep 1
81+
82+
# Clear the screen
83+
clear
84+
85+
# Command 8: Copy to file from store with mock and force-yes
86+
cmd="$CMD store copy --to-file=foo.sqlite --from-store=source.myshopify.com --mock -y"
87+
expect_success "$cmd"
88+
sleep 1
89+
90+
# Clear the screen
91+
clear
92+
93+
# Command 9: Copy from store to store with mock and force-yes
94+
cmd="$CMD store copy --from-store=source.myshopify.com --to-store=target.myshopify.com --mock -y"
95+
expect_success "$cmd" # This line was missing 'expect_success' in the original script
96+
sleep 1
97+
98+
# --- Summary Section ---
99+
100+
echo -e "\n--- Script Summary ---"
101+
102+
# Print failures if any
103+
if [ ${#FAILURES[@]} -gt 0 ]; then
104+
echo -e "\n❌ Failed Commands:"
105+
for failed_cmd in "${FAILURES[@]}"; do
106+
echo "- ${failed_cmd}"
107+
done
108+
else
109+
echo "✅ All commands succeeded!"
110+
fi
111+
112+
# Optionally, print successes (uncomment if desired)
113+
# if [ ${#SUCCESSES[@]} -gt 0 ]; then
114+
# echo -e "\n✅ Successful Commands:"
115+
# for successful_cmd in "${SUCCESSES[@]}"; do
116+
# echo "- ${successful_cmd}"
117+
# done
118+
# fi
119+
120+
echo -e "\nTotal commands executed: $(( ${#SUCCESSES[@]} + ${#FAILURES[@]} ))"
121+
echo "Successful: ${#SUCCESSES[@]}"
122+
echo "Failed: ${#FAILURES[@]}"
123+
$
124+

packages/store/src/commands/store/copy.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import {BaseBDCommand} from '../../lib/base-command.js'
22
import {commonFlags, storeFlags, fileFlags, resourceConfigFlags} from '../../lib/flags.js'
33
import {FlagOptions} from '../../lib/types.js'
4+
import {parseResourceConfigFlags} from '../../lib/resource-config.js'
45
import {OperationMode} from '../../services/store/types/operations.js'
56
import {StoreCopyOperation} from '../../services/store/operations/store-copy.js'
67
import {StoreExportOperation} from '../../services/store/operations/store-export.js'
@@ -27,6 +28,10 @@ export default class Copy extends BaseBDCommand {
2728
async runCommand(): Promise<void> {
2829
this.flags = (await this.parse(Copy)).flags as FlagOptions
2930

31+
if (this.flags.key) {
32+
parseResourceConfigFlags(this.flags.key as string[])
33+
}
34+
3035
// Check access for all organizations first
3136
const apiClient = this.flags.mock ? new MockApiClient() : new ApiClient()
3237
const bpSession = await apiClient.ensureAuthenticatedBusinessPlatform()
Lines changed: 58 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import {parseResourceConfigFlags} from './resource-config.js'
2+
import {ValidationError, ErrorCodes} from '../services/store/errors/errors.js'
23
import {describe, expect, test} from 'vitest'
34

45
describe('parseResourceConfigFlags', () => {
@@ -32,30 +33,15 @@ describe('parseResourceConfigFlags', () => {
3233
})
3334
})
3435

35-
test('parses multiple different resources', () => {
36-
const result = parseResourceConfigFlags(['products:handle', 'customers:email'])
37-
expect(result).toEqual({
38-
products: {
39-
identifier: {
40-
field: 'HANDLE',
41-
customId: undefined,
42-
},
43-
},
44-
customers: {
45-
identifier: {
46-
field: 'EMAIL',
47-
customId: undefined,
48-
},
49-
},
50-
})
51-
})
52-
5336
test('overwrites identifier when same resource appears multiple times', () => {
54-
const result = parseResourceConfigFlags(['products:handle', 'products:title'])
37+
const result = parseResourceConfigFlags(['products:handle', 'products:metafield:custom:salesforce_id'])
5538
expect(result).toEqual({
5639
products: {
5740
identifier: {
58-
field: 'TITLE',
41+
customId: {
42+
namespace: 'custom',
43+
key: 'salesforce_id',
44+
},
5945
},
6046
},
6147
})
@@ -76,34 +62,62 @@ describe('parseResourceConfigFlags', () => {
7662
},
7763
})
7864
})
65+
})
66+
67+
describe('when validating key formats and fields', () => {
68+
test('throws INVALID_KEY_FORMAT for malformed keys', () => {
69+
expect(() => parseResourceConfigFlags(['invalid'])).toThrow(ValidationError)
70+
expect(() => parseResourceConfigFlags(['invalid'])).toThrow(
71+
expect.objectContaining({
72+
code: ErrorCodes.INVALID_KEY_FORMAT,
73+
}),
74+
)
7975

80-
test('throws error for non-product unique metafield', () => {
81-
expect(() => parseResourceConfigFlags(['customers:metafield:custom:id'])).toThrow(
82-
"Invalid resource: customers don't support unique metafields as identifiers.",
76+
expect(() => parseResourceConfigFlags(['product:yes:no'])).toThrow(ValidationError)
77+
expect(() => parseResourceConfigFlags(['product:yes:no'])).toThrow(
78+
expect.objectContaining({
79+
code: ErrorCodes.INVALID_KEY_FORMAT,
80+
}),
81+
)
82+
83+
expect(() => parseResourceConfigFlags(['product/yes'])).toThrow(ValidationError)
84+
expect(() => parseResourceConfigFlags(['product/yes'])).toThrow(
85+
expect.objectContaining({
86+
code: ErrorCodes.INVALID_KEY_FORMAT,
87+
}),
8388
)
8489
})
85-
})
8690

87-
describe('when parsing mixed field and metafield configs', () => {
88-
test('returns mixed set of identifier inputs', () => {
89-
const result = parseResourceConfigFlags(['products:metafield:custom:salesforce_id', 'customers:email'])
90-
expect(result).toEqual({
91-
products: {
92-
identifier: {
93-
field: undefined,
94-
customId: {
95-
namespace: 'custom',
96-
key: 'salesforce_id',
97-
},
98-
},
99-
},
100-
customers: {
101-
identifier: {
102-
field: 'EMAIL',
103-
customId: undefined,
104-
},
105-
},
106-
})
91+
test('throws KEY_NOT_SUPPORTED for unknown resources', () => {
92+
expect(() => parseResourceConfigFlags(['unknown:field'])).toThrow(ValidationError)
93+
expect(() => parseResourceConfigFlags(['unknown:field'])).toThrow(
94+
expect.objectContaining({
95+
code: ErrorCodes.KEY_NOT_SUPPORTED,
96+
}),
97+
)
98+
})
99+
100+
test('throws KEY_DOES_NOT_EXIST for invalid fields', () => {
101+
expect(() => parseResourceConfigFlags(['products:title'])).toThrow(ValidationError)
102+
expect(() => parseResourceConfigFlags(['products:title'])).toThrow(
103+
expect.objectContaining({
104+
code: ErrorCodes.KEY_DOES_NOT_EXIST,
105+
}),
106+
)
107+
})
108+
109+
test('throws KEY_NOT_SUPPORTED for product typos', () => {
110+
expect(() => parseResourceConfigFlags(['product:handle'])).toThrow(ValidationError)
111+
expect(() => parseResourceConfigFlags(['product:handle'])).toThrow(
112+
expect.objectContaining({
113+
code: ErrorCodes.KEY_NOT_SUPPORTED,
114+
}),
115+
)
116+
})
117+
118+
test('accepts valid resource:field combinations', () => {
119+
expect(() => parseResourceConfigFlags(['products:handle'])).not.toThrow()
120+
expect(() => parseResourceConfigFlags(['products:metafield:custom:salesforce_id'])).not.toThrow()
107121
})
108122
})
109123
})

packages/store/src/lib/resource-config.ts

Lines changed: 27 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
11
import {ResourceConfigs, ResourceConfig} from './types.js'
2+
import {ValidationError, ErrorCodes} from '../services/store/errors/errors.js'
23

34
const METAFIELD_KEYWORD = 'metafield'
45
const RESOURCES_SUPPORTING_UNIQUE_METAFIELD_IDENTIFIERS = ['products']
56
const METAFIELD_FLAG_PARTS_COUNT = 4
6-
const MIN_FLAG_PARTS_COUNT = 2
7+
8+
const VALID_RESOURCE_FIELDS: {[key: string]: string[]} = {
9+
products: ['handle'],
10+
}
711

812
function createFieldBasedConfig(field: string): ResourceConfig {
913
return {
@@ -24,12 +28,6 @@ function createMetafieldBasedConfig(namespace: string, key: string): ResourceCon
2428
}
2529
}
2630

27-
function validateFlagFormat(flag: string, parts: string[]): void {
28-
if (parts.length < MIN_FLAG_PARTS_COUNT) {
29-
throw new Error(`Invalid flag format: ${flag}. Expected format: resource:key or resource:metafield:namespace:key`)
30-
}
31-
}
32-
3331
function validateMetafieldFormat(flag: string, parts: string[]): void {
3432
if (parts.length !== METAFIELD_FLAG_PARTS_COUNT) {
3533
throw new Error(`Invalid flag format: ${flag}. Expected format: resource:metafield:namespace:key`)
@@ -42,6 +40,23 @@ function validateMetafieldResource(resource: string): void {
4240
}
4341
}
4442

43+
function validateKeyFormat(flag: string): void {
44+
const colonCount = (flag.match(/:/g) ?? []).length
45+
if (colonCount !== 1 && colonCount !== 3) {
46+
throw new ValidationError(ErrorCodes.INVALID_KEY_FORMAT, {key: flag})
47+
}
48+
}
49+
50+
function validateResourceAndField(resource: string, field: string): void {
51+
if (!VALID_RESOURCE_FIELDS[resource]) {
52+
throw new ValidationError(ErrorCodes.KEY_NOT_SUPPORTED, {resource})
53+
}
54+
55+
if (!VALID_RESOURCE_FIELDS[resource].includes(field)) {
56+
throw new ValidationError(ErrorCodes.KEY_DOES_NOT_EXIST, {field})
57+
}
58+
}
59+
4560
export function parseResourceConfigFlags(flags: string[]): ResourceConfigs {
4661
const resourceConfigs: ResourceConfigs = {}
4762

@@ -50,14 +65,14 @@ export function parseResourceConfigFlags(flags: string[]): ResourceConfigs {
5065
}
5166

5267
flags.forEach((flag: string) => {
53-
const parts = flag.split(':')
54-
validateFlagFormat(flag, parts)
68+
validateKeyFormat(flag)
5569

70+
const parts = flag.split(':')
5671
const resource = parts[0]
5772
const secondPart = parts[1]
5873

5974
if (!resource || !secondPart) {
60-
throw new Error(`Invalid flag format: ${flag}`)
75+
throw new ValidationError(ErrorCodes.INVALID_KEY_FORMAT, {key: flag})
6176
}
6277

6378
if (secondPart === METAFIELD_KEYWORD) {
@@ -68,11 +83,12 @@ export function parseResourceConfigFlags(flags: string[]): ResourceConfigs {
6883
const key = parts[3]
6984

7085
if (!namespace || !key) {
71-
throw new Error(`Invalid metafield format: ${flag}`)
86+
throw new ValidationError(ErrorCodes.INVALID_KEY_FORMAT, {key: flag})
7287
}
7388

7489
resourceConfigs[resource] = createMetafieldBasedConfig(namespace, key)
7590
} else {
91+
validateResourceAndField(resource, secondPart)
7692
resourceConfigs[resource] = createFieldBasedConfig(secondPart)
7793
}
7894
})

packages/store/src/services/store/errors/errors.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ export const ErrorCodes = {
1010
FILE_TOO_LARGE: 'FILE_TOO_LARGE',
1111
EMPTY_FILE: 'EMPTY_FILE',
1212
NOT_A_FILE: 'NOT_A_FILE',
13+
INVALID_KEY_FORMAT: 'INVALID_KEY_FORMAT',
14+
KEY_DOES_NOT_EXIST: 'KEY_DOES_NOT_EXIST',
15+
KEY_NOT_SUPPORTED: 'KEY_NOT_SUPPORTED',
1316

1417
// Operation errors
1518
COPY_FAILED: 'COPY_FAILED',
@@ -75,6 +78,12 @@ function generateErrorMessage(code: string, params?: ErrorParams): string {
7578
return `File is empty: ${params?.filePath}`
7679
case ErrorCodes.NOT_A_FILE:
7780
return `Path is not a file: ${params?.filePath}`
81+
case ErrorCodes.INVALID_KEY_FORMAT:
82+
return `Key format "${params?.key}" is invalid.\nBuilt-in fields can be specified as <object_type>:<key>\n\nID metafields can be used as key by specifying\n<object_type>:metafield:<metafield_namespace>:<metafield_key>.`
83+
case ErrorCodes.KEY_DOES_NOT_EXIST:
84+
return `Key "${params?.field}" does not exist or is unsupported.\nBuilt-in fields can be specified as <object_type>:<key>\n\nID metafields can be used as key by specifying\n<object_type>:metafield:<metafield_namespace>:<metafield_key>.`
85+
case ErrorCodes.KEY_NOT_SUPPORTED:
86+
return `Object type "${params?.resource}" does not exist or is unsupported.`
7887

7988
// Operation errors
8089
case ErrorCodes.COPY_FAILED:

0 commit comments

Comments
 (0)