Skip to content

Commit 2ec8a8c

Browse files
authored
Merge pull request #6084 from Shopify/07-09-update_error_message_structure
Update error message structure
2 parents fc2f380 + 7c7c05c commit 2ec8a8c

File tree

4 files changed

+44
-60
lines changed

4 files changed

+44
-60
lines changed

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

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ describe('ValidationError', () => {
88
expect(error).toBeInstanceOf(ValidationError)
99
expect(error.code).toBe(ErrorCodes.FILE_NOT_FOUND)
1010
expect(error.params).toEqual({filePath: '/test/file.sqlite'})
11-
expect(error.message).toBe('File not found: /test/file.sqlite')
11+
expect(error.message).toBe('File "/test/file.sqlite" not found.')
1212
})
1313

1414
test('should create error with code only', () => {
@@ -26,7 +26,7 @@ describe('ValidationError', () => {
2626
expect(error.code).toBe(ErrorCodes.SHOP_NOT_FOUND)
2727
expect(error.params).toEqual({shop: 'test-shop.myshopify.com'})
2828
expect(error.message).toBe(
29-
'Shop (test-shop.myshopify.com) not found in any of the Early Access enabled organizations you have access to.',
29+
'Shop "test-shop.myshopify.com" not found in any of the Early Access enabled organizations you have access to.',
3030
)
3131
})
3232
})
@@ -62,11 +62,11 @@ describe('OperationError', () => {
6262
describe('Error message generation', () => {
6363
test('should generate correct messages for all error codes', () => {
6464
// Validation errors
65-
expect(new ValidationError(ErrorCodes.FILE_TOO_LARGE, {sizeGB: 6}).message).toBe(
66-
'File is too large (6GB). Maximum size is 5GB.',
67-
)
68-
expect(new ValidationError(ErrorCodes.EMPTY_FILE, {filePath: '/test.db'}).message).toBe('File is empty: /test.db')
69-
expect(new ValidationError(ErrorCodes.NOT_A_FILE, {filePath: '/test'}).message).toBe('Path is not a file: /test')
65+
expect(
66+
new ValidationError(ErrorCodes.FILE_TOO_LARGE, {filePath: '/test.db', fileSize: '5MB', maxSize: '4MB'}).message,
67+
).toBe('File "/test.db" (5MB) exceeds maximum size of 4MB.')
68+
expect(new ValidationError(ErrorCodes.EMPTY_FILE, {filePath: '/test.db'}).message).toBe('File "/test.db" is empty.')
69+
expect(new ValidationError(ErrorCodes.NOT_A_FILE, {filePath: '/test'}).message).toBe('Path "/test" is not a file.')
7070
expect(new ValidationError(ErrorCodes.DIFFERENT_ORG).message).toBe(
7171
'Source and target shops must be in the same organization.',
7272
)

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

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -61,23 +61,23 @@ function generateErrorMessage(code: string, params?: ErrorParams): string {
6161
switch (code) {
6262
// Validation errors
6363
case ErrorCodes.SHOP_NOT_FOUND:
64-
return `Shop (${params?.shop}) not found in any of the Early Access enabled organizations you have access to.`
64+
return `Shop "${params?.shop}" not found in any of the Early Access enabled organizations you have access to.`
6565
case ErrorCodes.FILE_NOT_FOUND:
66-
return `File not found: ${params?.filePath}`
66+
return `File "${params?.filePath}" not found.`
6767
case ErrorCodes.INVALID_FILE_FORMAT:
6868
return params?.filePath
69-
? `File does not appear to be a valid SQLite database: ${params.filePath}`
69+
? `File "${params.filePath}" does not appear to be a valid SQLite database.`
7070
: 'Invalid file format'
7171
case ErrorCodes.SAME_SHOP:
7272
return 'Source and target shops must not be the same.'
7373
case ErrorCodes.DIFFERENT_ORG:
7474
return 'Source and target shops must be in the same organization.'
7575
case ErrorCodes.FILE_TOO_LARGE:
76-
return `File is too large (${params?.sizeGB}GB). Maximum size is 5GB.`
76+
return `File "${params?.filePath}" (${params?.fileSize}) exceeds maximum size of ${params?.maxSize}.`
7777
case ErrorCodes.EMPTY_FILE:
78-
return `File is empty: ${params?.filePath}`
78+
return `File "${params?.filePath}" is empty.`
7979
case ErrorCodes.NOT_A_FILE:
80-
return `Path is not a file: ${params?.filePath}`
80+
return `Path "${params?.filePath}" is not a file.`
8181
case ErrorCodes.INVALID_KEY_FORMAT:
8282
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>.`
8383
case ErrorCodes.KEY_DOES_NOT_EXIST:

packages/store/src/services/store/utils/file-uploader.test.ts

Lines changed: 12 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,12 @@ import {createStagedUploadAdmin} from '../../../apis/admin/index.js'
33
import {ValidationError, OperationError, ErrorCodes} from '../errors/errors.js'
44
import {fetch} from '@shopify/cli-kit/node/http'
55
import {describe, test, expect, vi, beforeEach} from 'vitest'
6-
import {readFileSync, statSync} from 'node:fs'
6+
import {isDirectory, readFileSync, fileSize, fileExistsSync} from '@shopify/cli-kit/node/fs'
77

88
vi.mock('../../../apis/admin/index.js')
99
vi.mock('@shopify/cli-kit/node/http')
1010
vi.mock('node:fs')
11+
vi.mock('@shopify/cli-kit/node/fs')
1112

1213
describe('FileUploader', () => {
1314
let fileUploader: FileUploader
@@ -20,10 +21,6 @@ describe('FileUploader', () => {
2021

2122
describe('uploadSqliteFile', () => {
2223
const mockFileBuffer = Buffer.from('SQLite format 3\0test data')
23-
const mockFileStats = {
24-
isFile: () => true,
25-
size: 1024,
26-
}
2724
const mockStagedUploadResponse = {
2825
stagedUploadsCreate: {
2926
stagedTargets: [
@@ -41,8 +38,10 @@ describe('FileUploader', () => {
4138
}
4239

4340
beforeEach(() => {
44-
vi.mocked(statSync).mockReturnValue(mockFileStats as any)
4541
vi.mocked(readFileSync).mockReturnValue(mockFileBuffer)
42+
vi.mocked(fileExistsSync).mockReturnValue(true)
43+
vi.mocked(fileSize).mockResolvedValue(1024)
44+
vi.mocked(isDirectory).mockResolvedValue(false)
4645
vi.mocked(createStagedUploadAdmin).mockResolvedValue(mockStagedUploadResponse)
4746
vi.mocked(fetch).mockResolvedValue({
4847
ok: true,
@@ -54,7 +53,6 @@ describe('FileUploader', () => {
5453
const result = await fileUploader.uploadSqliteFile(mockFilePath, mockStoreFqdn)
5554

5655
expect(result).toBe('https://shopify.com/resource/123')
57-
expect(statSync).toHaveBeenCalledWith(mockFilePath)
5856
expect(readFileSync).toHaveBeenCalledWith(mockFilePath)
5957
expect(createStagedUploadAdmin).toHaveBeenCalledWith(mockStoreFqdn, [
6058
{
@@ -72,9 +70,7 @@ describe('FileUploader', () => {
7270
})
7371

7472
test('should throw error when file does not exist', async () => {
75-
vi.mocked(statSync).mockImplementation(() => {
76-
throw new Error('ENOENT: no such file or directory')
77-
})
73+
vi.mocked(fileExistsSync).mockReturnValue(false)
7874

7975
const promise = fileUploader.uploadSqliteFile(mockFilePath, mockStoreFqdn)
8076
await expect(promise).rejects.toThrow(ValidationError)
@@ -85,11 +81,8 @@ describe('FileUploader', () => {
8581
})
8682

8783
test('should throw error when path is not a file', async () => {
88-
vi.mocked(statSync).mockReturnValue({
89-
isFile: () => false,
90-
size: 1024,
91-
} as any)
92-
84+
vi.mocked(fileSize).mockResolvedValue(1024)
85+
vi.mocked(isDirectory).mockResolvedValue(true)
9386
const promise = fileUploader.uploadSqliteFile(mockFilePath, mockStoreFqdn)
9487
await expect(promise).rejects.toThrow(ValidationError)
9588
await expect(promise).rejects.toMatchObject({
@@ -99,10 +92,7 @@ describe('FileUploader', () => {
9992
})
10093

10194
test('should throw error when file is empty', async () => {
102-
vi.mocked(statSync).mockReturnValue({
103-
isFile: () => true,
104-
size: 0,
105-
} as any)
95+
vi.mocked(fileSize).mockResolvedValue(0)
10696

10797
const promise = fileUploader.uploadSqliteFile(mockFilePath, mockStoreFqdn)
10898
await expect(promise).rejects.toThrow(ValidationError)
@@ -113,18 +103,14 @@ describe('FileUploader', () => {
113103
})
114104

115105
test('should throw error when file is too large', async () => {
116-
// 6GB
117-
const largeSize = 6 * 1024 * 1024 * 1024
118-
vi.mocked(statSync).mockReturnValue({
119-
isFile: () => true,
120-
size: largeSize,
121-
} as any)
106+
const largeSize = 30 * 1024 * 1024
107+
vi.mocked(fileSize).mockResolvedValue(largeSize)
122108

123109
const promise = fileUploader.uploadSqliteFile(mockFilePath, mockStoreFqdn)
124110
await expect(promise).rejects.toThrow(ValidationError)
125111
await expect(promise).rejects.toMatchObject({
126112
code: ErrorCodes.FILE_TOO_LARGE,
127-
params: {sizeGB: 6},
113+
params: {filePath: mockFilePath, fileSize: '30MB', maxSize: '20MB'},
128114
})
129115
})
130116

@@ -187,14 +173,5 @@ describe('FileUploader', () => {
187173
params: {details: '403 Forbidden. Access denied'},
188174
})
189175
})
190-
191-
test('should handle non-Error exceptions in validateSqliteFile', async () => {
192-
vi.mocked(statSync).mockImplementation(() => {
193-
// eslint-disable-next-line @typescript-eslint/only-throw-error
194-
throw 'string error'
195-
})
196-
197-
await expect(fileUploader.uploadSqliteFile(mockFilePath, mockStoreFqdn)).rejects.toThrow(ValidationError)
198-
})
199176
})
200177
})

packages/store/src/services/store/utils/file-uploader.ts

Lines changed: 19 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,23 @@
11
import {StagedUploadInput, createStagedUploadAdmin} from '../../../apis/admin/index.js'
22
import {ValidationError, OperationError, ErrorCodes} from '../errors/errors.js'
33
import {fetch} from '@shopify/cli-kit/node/http'
4-
import {readFileSync, statSync} from 'node:fs'
4+
import {fileExistsSync, fileSize, isDirectory, readFileSync} from '@shopify/cli-kit/node/fs'
5+
import {outputDebug} from '@shopify/cli-kit/node/output'
56

67
export class FileUploader {
8+
private readonly MAX_FILE_SIZE = 20 * 1024 * 1024
9+
710
async uploadSqliteFile(filePath: string, storeFqdn: string): Promise<string> {
8-
this.validateSqliteFile(filePath)
11+
await this.validateSqliteFile(filePath)
912

10-
const fileStats = statSync(filePath)
1113
const fileBuffer = readFileSync(filePath)
12-
14+
const sizeOfFile = await fileSize(filePath)
1315
const uploadInput: StagedUploadInput = {
1416
resource: 'FILE',
1517
filename: 'database.sqlite',
1618
mimeType: 'application/x-sqlite3',
1719
httpMethod: 'POST',
18-
fileSize: fileStats.size.toString(),
20+
fileSize: sizeOfFile.toString(),
1921
}
2022

2123
const stagedUploadResponse = await createStagedUploadAdmin(storeFqdn, [uploadInput])
@@ -64,21 +66,26 @@ export class FileUploader {
6466
return resourceUrl
6567
}
6668

67-
private validateSqliteFile(filePath: string): void {
69+
private async validateSqliteFile(filePath: string): Promise<void> {
6870
try {
69-
const stats = statSync(filePath)
70-
71-
if (!stats.isFile()) {
71+
if (!fileExistsSync(filePath)) {
72+
throw new ValidationError(ErrorCodes.FILE_NOT_FOUND, {filePath})
73+
}
74+
if (await isDirectory(filePath)) {
7275
throw new ValidationError(ErrorCodes.NOT_A_FILE, {filePath})
7376
}
77+
const sizeOfFile = await fileSize(filePath)
78+
outputDebug(`Validating SQLite file at ${filePath} with size ${sizeOfFile} bytes`)
7479

75-
if (stats.size === 0) {
80+
if (sizeOfFile === 0) {
7681
throw new ValidationError(ErrorCodes.EMPTY_FILE, {filePath})
7782
}
7883

79-
if (stats.size > 5 * 1024 * 1024 * 1024) {
84+
if (sizeOfFile > this.MAX_FILE_SIZE) {
8085
throw new ValidationError(ErrorCodes.FILE_TOO_LARGE, {
81-
sizeGB: Math.round(stats.size / 1024 / 1024 / 1024),
86+
filePath,
87+
fileSize: `${Math.round(sizeOfFile / 1024 / 1024)}MB`,
88+
maxSize: `${Math.round(this.MAX_FILE_SIZE / 1024 / 1024)}MB`,
8289
})
8390
}
8491

0 commit comments

Comments
 (0)