Skip to content

Commit 76871da

Browse files
authored
Merge pull request #6073 from Shopify/handle-errors
Add error handling for store copy --key
2 parents b023d52 + b32b946 commit 76871da

File tree

4 files changed

+99
-55
lines changed

4 files changed

+99
-55
lines changed

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)