ecomcoder-cli 1.2.12 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +293 -0
- package/dist/__tests__/test-utils.d.ts +123 -0
- package/dist/__tests__/test-utils.d.ts.map +1 -0
- package/dist/__tests__/test-utils.js +133 -0
- package/dist/__tests__/test-utils.js.map +1 -0
- package/dist/cli.d.ts +8 -7
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +70 -65
- package/dist/cli.js.map +1 -1
- package/dist/commands/docs/index.d.ts +10 -0
- package/dist/commands/docs/index.d.ts.map +1 -0
- package/dist/commands/docs/index.js +43 -0
- package/dist/commands/docs/index.js.map +1 -0
- package/dist/commands/metafield/index.d.ts +10 -0
- package/dist/commands/metafield/index.d.ts.map +1 -0
- package/dist/commands/metafield/index.js +55 -0
- package/dist/commands/metafield/index.js.map +1 -0
- package/dist/commands/product/__tests__/service.test.d.ts +7 -0
- package/dist/commands/product/__tests__/service.test.d.ts.map +1 -0
- package/dist/commands/product/__tests__/service.test.js +299 -0
- package/dist/commands/product/__tests__/service.test.js.map +1 -0
- package/dist/commands/product/__tests__/update-description.test.d.ts +5 -0
- package/dist/commands/product/__tests__/update-description.test.d.ts.map +1 -0
- package/dist/commands/product/__tests__/update-description.test.js +110 -0
- package/dist/commands/product/__tests__/update-description.test.js.map +1 -0
- package/dist/commands/product/get.d.ts +2 -0
- package/dist/commands/product/get.d.ts.map +1 -0
- package/dist/commands/product/get.js +305 -0
- package/dist/commands/product/get.js.map +1 -0
- package/dist/commands/product/index.d.ts +10 -0
- package/dist/commands/product/index.d.ts.map +1 -0
- package/dist/commands/product/index.js +73 -0
- package/dist/commands/product/index.js.map +1 -0
- package/dist/commands/product/queries.d.ts +32 -0
- package/dist/commands/product/queries.d.ts.map +1 -0
- package/dist/commands/product/queries.js +195 -0
- package/dist/commands/product/queries.js.map +1 -0
- package/dist/commands/product/service.d.ts +51 -0
- package/dist/commands/product/service.d.ts.map +1 -0
- package/dist/commands/product/service.js +237 -0
- package/dist/commands/product/service.js.map +1 -0
- package/dist/commands/product/types.d.ts +165 -0
- package/dist/commands/product/types.d.ts.map +1 -0
- package/dist/commands/product/types.js +6 -0
- package/dist/commands/product/types.js.map +1 -0
- package/dist/commands/product/update-description.d.ts +8 -0
- package/dist/commands/product/update-description.d.ts.map +1 -0
- package/dist/commands/product/update-description.js +86 -0
- package/dist/commands/product/update-description.js.map +1 -0
- package/dist/commands/product/update-price.d.ts +8 -0
- package/dist/commands/product/update-price.d.ts.map +1 -0
- package/dist/commands/product/update-price.js +101 -0
- package/dist/commands/product/update-price.js.map +1 -0
- package/dist/commands/product/update-template.d.ts +8 -0
- package/dist/commands/product/update-template.d.ts.map +1 -0
- package/dist/commands/product/update-template.js +114 -0
- package/dist/commands/product/update-template.js.map +1 -0
- package/dist/commands/product/utils.d.ts +69 -0
- package/dist/commands/product/utils.d.ts.map +1 -0
- package/dist/commands/product/utils.js +180 -0
- package/dist/commands/product/utils.js.map +1 -0
- package/dist/commands/products/index.d.ts +10 -0
- package/dist/commands/products/index.d.ts.map +1 -0
- package/dist/commands/products/index.js +62 -0
- package/dist/commands/products/index.js.map +1 -0
- package/dist/lib/api-client.d.ts.map +1 -1
- package/dist/lib/api-client.js +11 -0
- package/dist/lib/api-client.js.map +1 -1
- package/dist/lib/args-parser.d.ts.map +1 -1
- package/dist/lib/args-parser.js +2 -1
- package/dist/lib/args-parser.js.map +1 -1
- package/dist/lib/command-registry.d.ts +64 -0
- package/dist/lib/command-registry.d.ts.map +1 -0
- package/dist/lib/command-registry.js +76 -0
- package/dist/lib/command-registry.js.map +1 -0
- package/package.json +11 -3
- package/dist/lib/gemini-client.d.ts +0 -6
- package/dist/lib/gemini-client.d.ts.map +0 -1
- package/dist/lib/gemini-client.js +0 -27
- package/dist/lib/gemini-client.js.map +0 -1
package/dist/cli.js
CHANGED
|
@@ -1,16 +1,28 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* EcomCoder CLI - Main Entry Point
|
|
3
|
+
* EcomCoder CLI - Main Entry Point (Refactored)
|
|
4
4
|
*
|
|
5
|
-
* Command
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
5
|
+
* Architecture: Command Registry Pattern
|
|
6
|
+
*
|
|
7
|
+
* Benefits:
|
|
8
|
+
* ✅ Open/Closed Principle - Add commands without modifying this file
|
|
9
|
+
* ✅ Single Responsibility - This file only routes, doesn't know command details
|
|
10
|
+
* ✅ Scalability - Supports unlimited commands
|
|
11
|
+
* ✅ Testability - Easy to test routing logic
|
|
11
12
|
*/
|
|
12
13
|
import { getPositionalArgs } from './lib/args-parser.js';
|
|
13
|
-
|
|
14
|
+
import { registry } from './lib/command-registry.js';
|
|
15
|
+
// Register all command groups
|
|
16
|
+
import { registerProductCommands } from './commands/product/index.js';
|
|
17
|
+
import { registerMetafieldCommands } from './commands/metafield/index.js';
|
|
18
|
+
import { registerProductsCommands } from './commands/products/index.js';
|
|
19
|
+
import { registerDocsCommands } from './commands/docs/index.js';
|
|
20
|
+
// Initialize registry
|
|
21
|
+
registerProductCommands();
|
|
22
|
+
registerMetafieldCommands();
|
|
23
|
+
registerProductsCommands();
|
|
24
|
+
registerDocsCommands();
|
|
25
|
+
function showMainHelp() {
|
|
14
26
|
console.log(`
|
|
15
27
|
EcomCoder CLI - Shopify Development Utilities
|
|
16
28
|
|
|
@@ -18,12 +30,16 @@ USAGE:
|
|
|
18
30
|
ecomcoder <command> <subcommand> [options]
|
|
19
31
|
|
|
20
32
|
COMMANDS:
|
|
21
|
-
products create
|
|
22
|
-
products list
|
|
23
|
-
products set-rating
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
33
|
+
products create Create dummy Shopify products for testing
|
|
34
|
+
products list List products in your Shopify store
|
|
35
|
+
products set-rating Set rating metafields on a product
|
|
36
|
+
product get Get detailed product data
|
|
37
|
+
product update-description Update product description
|
|
38
|
+
product update-price Update product price and compare-at price
|
|
39
|
+
product update-template Change product template assignment
|
|
40
|
+
metafield create Create metafield definition
|
|
41
|
+
metafield get Get metafield definition(s)
|
|
42
|
+
docs search Search Shopify theme documentation
|
|
27
43
|
|
|
28
44
|
GLOBAL OPTIONS:
|
|
29
45
|
--session-id Session ID for authentication (auto-provided when used with Claude)
|
|
@@ -46,6 +62,19 @@ EXAMPLES:
|
|
|
46
62
|
# Set product rating
|
|
47
63
|
ecomcoder products set-rating --product-id="gid://shopify/Product/123" --rating=4.5 --count=127
|
|
48
64
|
|
|
65
|
+
# Get product details
|
|
66
|
+
ecomcoder product get --id=123456789
|
|
67
|
+
ecomcoder product get --title="Blue T-Shirt"
|
|
68
|
+
|
|
69
|
+
# Update product description
|
|
70
|
+
ecomcoder product update-description --id=123 --description="Premium cotton t-shirt"
|
|
71
|
+
|
|
72
|
+
# Update product price
|
|
73
|
+
ecomcoder product update-price --id=123 --price=34.99 --compare-at-price=49.99
|
|
74
|
+
|
|
75
|
+
# Change product template
|
|
76
|
+
ecomcoder product update-template --id=123 --template=premium
|
|
77
|
+
|
|
49
78
|
# Create product-level metafield
|
|
50
79
|
ecomcoder metafield create --name="Rating" --key=rating --type=number_decimal --owner-type=PRODUCT
|
|
51
80
|
|
|
@@ -64,71 +93,47 @@ For more help on specific commands, run:
|
|
|
64
93
|
}
|
|
65
94
|
async function main() {
|
|
66
95
|
const args = process.argv.slice(2);
|
|
67
|
-
// Show help
|
|
96
|
+
// Show help if no arguments
|
|
68
97
|
if (args.length === 0) {
|
|
69
|
-
|
|
98
|
+
showMainHelp();
|
|
70
99
|
process.exit(0);
|
|
71
100
|
}
|
|
72
101
|
const positional = getPositionalArgs(args);
|
|
73
102
|
const [command, subcommand] = positional;
|
|
74
103
|
// Show main help if help flag with no command
|
|
75
104
|
if ((args.includes('--help') || args.includes('-h')) && !command) {
|
|
76
|
-
|
|
105
|
+
showMainHelp();
|
|
77
106
|
process.exit(0);
|
|
78
107
|
}
|
|
79
|
-
// Route to appropriate command handler
|
|
80
108
|
try {
|
|
81
|
-
if
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
else if (subcommand === 'list') {
|
|
87
|
-
const { run } = await import('./commands/list-products.js');
|
|
88
|
-
await run(args.slice(2));
|
|
89
|
-
}
|
|
90
|
-
else if (subcommand === 'set-rating') {
|
|
91
|
-
const { run } = await import('./commands/set-product-rating.js');
|
|
92
|
-
await run(args.slice(2));
|
|
93
|
-
}
|
|
94
|
-
else {
|
|
95
|
-
console.error(`Unknown products subcommand: ${subcommand}`);
|
|
96
|
-
console.error(`Valid subcommands: create, list, set-rating`);
|
|
97
|
-
process.exit(1);
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
else if (command === 'metafield') {
|
|
101
|
-
if (subcommand === 'create') {
|
|
102
|
-
const { run } = await import('./commands/create-metafield.js');
|
|
103
|
-
await run(args.slice(2));
|
|
104
|
-
}
|
|
105
|
-
else if (subcommand === 'get') {
|
|
106
|
-
const { run } = await import('./commands/get-metafield.js');
|
|
107
|
-
await run(args.slice(2));
|
|
108
|
-
}
|
|
109
|
-
else {
|
|
110
|
-
console.error(`Unknown metafield subcommand: ${subcommand}`);
|
|
111
|
-
console.error(`Valid subcommands: create, get`);
|
|
112
|
-
process.exit(1);
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
else if (command === 'docs') {
|
|
116
|
-
if (subcommand === 'search') {
|
|
117
|
-
const { run } = await import('./commands/docs-search.js');
|
|
118
|
-
await run(args.slice(2));
|
|
119
|
-
}
|
|
120
|
-
else {
|
|
121
|
-
console.error(`Unknown docs subcommand: ${subcommand}`);
|
|
122
|
-
console.error(`Valid subcommands: search`);
|
|
123
|
-
process.exit(1);
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
else {
|
|
109
|
+
// Check if command group exists
|
|
110
|
+
const group = registry.getGroup(command);
|
|
111
|
+
if (!group) {
|
|
112
|
+
// Command not found
|
|
113
|
+
const availableCommands = registry.getAllCommands();
|
|
127
114
|
console.error(`Unknown command: ${command}`);
|
|
128
|
-
console.error(`Valid commands:
|
|
115
|
+
console.error(`Valid commands: ${availableCommands.join(', ')}`);
|
|
129
116
|
console.error(`Run 'ecomcoder --help' for more information`);
|
|
130
117
|
process.exit(1);
|
|
131
118
|
}
|
|
119
|
+
// Show group help ONLY if no subcommand
|
|
120
|
+
if (!subcommand) {
|
|
121
|
+
group.showHelp();
|
|
122
|
+
process.exit(0);
|
|
123
|
+
}
|
|
124
|
+
// Try to get command handler
|
|
125
|
+
const handler = await registry.get(command, subcommand);
|
|
126
|
+
if (!handler) {
|
|
127
|
+
// Subcommand not found
|
|
128
|
+
const availableCommands = registry.getCommandsInGroup(command)
|
|
129
|
+
.map(cmd => cmd.subcommand)
|
|
130
|
+
.filter(Boolean);
|
|
131
|
+
console.error(`Unknown ${command} subcommand: ${subcommand}`);
|
|
132
|
+
console.error(`Valid subcommands: ${availableCommands.join(', ')}`);
|
|
133
|
+
process.exit(1);
|
|
134
|
+
}
|
|
135
|
+
// Execute command (command itself handles --help)
|
|
136
|
+
await handler.run(args.slice(2)); // Pass remaining args
|
|
132
137
|
}
|
|
133
138
|
catch (error) {
|
|
134
139
|
console.error(`Error executing command: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
package/dist/cli.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AAEA
|
|
1
|
+
{"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/commands/docs/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAIH;;GAEG;AACH,wBAAgB,oBAAoB,IAAI,IAAI,CAiC3C"}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Docs Commands Registration
|
|
3
|
+
*
|
|
4
|
+
* Following Open/Closed Principle - commands register themselves
|
|
5
|
+
*/
|
|
6
|
+
import { registry } from '../../lib/command-registry.js';
|
|
7
|
+
/**
|
|
8
|
+
* Register all docs commands
|
|
9
|
+
*/
|
|
10
|
+
export function registerDocsCommands() {
|
|
11
|
+
// Register command group
|
|
12
|
+
registry.registerGroup({
|
|
13
|
+
command: 'docs',
|
|
14
|
+
description: 'Search Shopify theme documentation',
|
|
15
|
+
showHelp: () => {
|
|
16
|
+
console.log(`
|
|
17
|
+
Docs Commands - Search Shopify theme documentation
|
|
18
|
+
|
|
19
|
+
USAGE:
|
|
20
|
+
ecomcoder docs <subcommand> [options]
|
|
21
|
+
|
|
22
|
+
SUBCOMMANDS:
|
|
23
|
+
search Search Shopify theme docs using RAG
|
|
24
|
+
|
|
25
|
+
EXAMPLES:
|
|
26
|
+
# Search Shopify theme docs
|
|
27
|
+
ecomcoder docs search --query="How do sections work?"
|
|
28
|
+
ecomcoder docs search --query="Liquid filters for dates"
|
|
29
|
+
|
|
30
|
+
For detailed help on a specific subcommand:
|
|
31
|
+
ecomcoder docs <subcommand> --help
|
|
32
|
+
`);
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
// Register individual subcommands
|
|
36
|
+
registry.register({
|
|
37
|
+
command: 'docs',
|
|
38
|
+
subcommand: 'search',
|
|
39
|
+
description: 'Search Shopify theme documentation',
|
|
40
|
+
handler: async () => await import('../docs-search.js')
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/commands/docs/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,QAAQ,EAAE,MAAM,+BAA+B,CAAC;AAEzD;;GAEG;AACH,MAAM,UAAU,oBAAoB;IAClC,yBAAyB;IACzB,QAAQ,CAAC,aAAa,CAAC;QACrB,OAAO,EAAE,MAAM;QACf,WAAW,EAAE,oCAAoC;QACjD,QAAQ,EAAE,GAAG,EAAE;YACb,OAAO,CAAC,GAAG,CAAC;;;;;;;;;;;;;;;;CAgBjB,CAAC,CAAC;QACC,CAAC;KACF,CAAC,CAAC;IAEH,kCAAkC;IAClC,QAAQ,CAAC,QAAQ,CAAC;QAChB,OAAO,EAAE,MAAM;QACf,UAAU,EAAE,QAAQ;QACpB,WAAW,EAAE,oCAAoC;QACjD,OAAO,EAAE,KAAK,IAAI,EAAE,CAAC,MAAM,MAAM,CAAC,mBAAmB,CAAC;KACvD,CAAC,CAAC;AACL,CAAC"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Metafield Commands Registration
|
|
3
|
+
*
|
|
4
|
+
* Following Open/Closed Principle - commands register themselves
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* Register all metafield commands
|
|
8
|
+
*/
|
|
9
|
+
export declare function registerMetafieldCommands(): void;
|
|
10
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/commands/metafield/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAIH;;GAEG;AACH,wBAAgB,yBAAyB,IAAI,IAAI,CA8ChD"}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Metafield Commands Registration
|
|
3
|
+
*
|
|
4
|
+
* Following Open/Closed Principle - commands register themselves
|
|
5
|
+
*/
|
|
6
|
+
import { registry } from '../../lib/command-registry.js';
|
|
7
|
+
/**
|
|
8
|
+
* Register all metafield commands
|
|
9
|
+
*/
|
|
10
|
+
export function registerMetafieldCommands() {
|
|
11
|
+
// Register command group
|
|
12
|
+
registry.registerGroup({
|
|
13
|
+
command: 'metafield',
|
|
14
|
+
description: 'Manage Shopify metafield definitions',
|
|
15
|
+
showHelp: () => {
|
|
16
|
+
console.log(`
|
|
17
|
+
Metafield Commands - Manage Shopify metafield definitions
|
|
18
|
+
|
|
19
|
+
USAGE:
|
|
20
|
+
ecomcoder metafield <subcommand> [options]
|
|
21
|
+
|
|
22
|
+
SUBCOMMANDS:
|
|
23
|
+
create Create metafield definition
|
|
24
|
+
get Get metafield definition(s)
|
|
25
|
+
|
|
26
|
+
EXAMPLES:
|
|
27
|
+
# Create product-level metafield
|
|
28
|
+
ecomcoder metafield create --name="Rating" --key=rating --type=number_decimal --owner-type=PRODUCT
|
|
29
|
+
|
|
30
|
+
# Create shop-level metafield (global)
|
|
31
|
+
ecomcoder metafield create --name="Cart Upsell" --key=cart_upsell --type=product_reference --owner-type=SHOP
|
|
32
|
+
|
|
33
|
+
# Get metafield definition
|
|
34
|
+
ecomcoder metafield get --namespace=ecomcoder --key=upsell --owner-type=PRODUCT
|
|
35
|
+
|
|
36
|
+
For detailed help on a specific subcommand:
|
|
37
|
+
ecomcoder metafield <subcommand> --help
|
|
38
|
+
`);
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
// Register individual subcommands
|
|
42
|
+
registry.register({
|
|
43
|
+
command: 'metafield',
|
|
44
|
+
subcommand: 'create',
|
|
45
|
+
description: 'Create metafield definition',
|
|
46
|
+
handler: async () => await import('../create-metafield.js')
|
|
47
|
+
});
|
|
48
|
+
registry.register({
|
|
49
|
+
command: 'metafield',
|
|
50
|
+
subcommand: 'get',
|
|
51
|
+
description: 'Get metafield definition(s)',
|
|
52
|
+
handler: async () => await import('../get-metafield.js')
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/commands/metafield/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,QAAQ,EAAE,MAAM,+BAA+B,CAAC;AAEzD;;GAEG;AACH,MAAM,UAAU,yBAAyB;IACvC,yBAAyB;IACzB,QAAQ,CAAC,aAAa,CAAC;QACrB,OAAO,EAAE,WAAW;QACpB,WAAW,EAAE,sCAAsC;QACnD,QAAQ,EAAE,GAAG,EAAE;YACb,OAAO,CAAC,GAAG,CAAC;;;;;;;;;;;;;;;;;;;;;;CAsBjB,CAAC,CAAC;QACC,CAAC;KACF,CAAC,CAAC;IAEH,kCAAkC;IAClC,QAAQ,CAAC,QAAQ,CAAC;QAChB,OAAO,EAAE,WAAW;QACpB,UAAU,EAAE,QAAQ;QACpB,WAAW,EAAE,6BAA6B;QAC1C,OAAO,EAAE,KAAK,IAAI,EAAE,CAAC,MAAM,MAAM,CAAC,wBAAwB,CAAC;KAC5D,CAAC,CAAC;IAEH,QAAQ,CAAC,QAAQ,CAAC;QAChB,OAAO,EAAE,WAAW;QACpB,UAAU,EAAE,KAAK;QACjB,WAAW,EAAE,6BAA6B;QAC1C,OAAO,EAAE,KAAK,IAAI,EAAE,CAAC,MAAM,MAAM,CAAC,qBAAqB,CAAC;KACzD,CAAC,CAAC;AACL,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"service.test.d.ts","sourceRoot":"","sources":["../../../../src/commands/product/__tests__/service.test.ts"],"names":[],"mappings":"AAAA;;;;GAIG"}
|
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ProductService Tests
|
|
3
|
+
*
|
|
4
|
+
* Tests for product service layer business logic
|
|
5
|
+
*/
|
|
6
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
7
|
+
import { ProductService } from '../service.js';
|
|
8
|
+
import { mockCredentials, mockProduct, mockProductMultipleVariants, mockVariant } from '../../../__tests__/test-utils.js';
|
|
9
|
+
import * as shopifyClient from '../../../lib/shopify-client.js';
|
|
10
|
+
// Mock the shopify-client module
|
|
11
|
+
vi.mock('../../../lib/shopify-client.js', () => ({
|
|
12
|
+
shopifyGraphQL: vi.fn()
|
|
13
|
+
}));
|
|
14
|
+
describe('ProductService', () => {
|
|
15
|
+
beforeEach(() => {
|
|
16
|
+
vi.clearAllMocks();
|
|
17
|
+
});
|
|
18
|
+
describe('getProductById', () => {
|
|
19
|
+
it('should fetch product by ID', async () => {
|
|
20
|
+
const mockResponse = {
|
|
21
|
+
data: {
|
|
22
|
+
product: mockProduct
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
vi.mocked(shopifyClient.shopifyGraphQL).mockResolvedValue(mockResponse);
|
|
26
|
+
const result = await ProductService.getProductById(mockCredentials, '123456789');
|
|
27
|
+
expect(result).toEqual(mockProduct);
|
|
28
|
+
expect(shopifyClient.shopifyGraphQL).toHaveBeenCalledWith(mockCredentials, expect.stringContaining('query getProduct'), { id: 'gid://shopify/Product/123456789' });
|
|
29
|
+
});
|
|
30
|
+
it('should throw error if product not found', async () => {
|
|
31
|
+
const mockResponse = {
|
|
32
|
+
data: {
|
|
33
|
+
product: null
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
vi.mocked(shopifyClient.shopifyGraphQL).mockResolvedValue(mockResponse);
|
|
37
|
+
await expect(ProductService.getProductById(mockCredentials, '123456789')).rejects.toThrow('Product not found');
|
|
38
|
+
});
|
|
39
|
+
it('should throw error on GraphQL errors', async () => {
|
|
40
|
+
const mockResponse = {
|
|
41
|
+
data: null,
|
|
42
|
+
errors: [{ message: 'Invalid ID' }]
|
|
43
|
+
};
|
|
44
|
+
vi.mocked(shopifyClient.shopifyGraphQL).mockResolvedValue(mockResponse);
|
|
45
|
+
await expect(ProductService.getProductById(mockCredentials, '123456789')).rejects.toThrow('GraphQL error');
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
describe('updateDescription', () => {
|
|
49
|
+
it('should update description with plain text (wrapped in <p> tags)', async () => {
|
|
50
|
+
const mockUpdateResponse = {
|
|
51
|
+
data: {
|
|
52
|
+
productUpdate: {
|
|
53
|
+
product: { id: mockProduct.id, title: mockProduct.title, descriptionHtml: '<p>New description</p>' },
|
|
54
|
+
userErrors: []
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
const mockProductResponse = {
|
|
59
|
+
data: {
|
|
60
|
+
product: { ...mockProduct, descriptionHtml: '<p>New description</p>' }
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
vi.mocked(shopifyClient.shopifyGraphQL)
|
|
64
|
+
.mockResolvedValueOnce(mockUpdateResponse)
|
|
65
|
+
.mockResolvedValueOnce(mockProductResponse);
|
|
66
|
+
const result = await ProductService.updateDescription(mockCredentials, {
|
|
67
|
+
id: '123456789',
|
|
68
|
+
description: 'New description',
|
|
69
|
+
isHtml: false
|
|
70
|
+
});
|
|
71
|
+
expect(result.success).toBe(true);
|
|
72
|
+
expect(shopifyClient.shopifyGraphQL).toHaveBeenCalledWith(mockCredentials, expect.stringContaining('mutation productUpdate'), expect.objectContaining({
|
|
73
|
+
input: expect.objectContaining({
|
|
74
|
+
descriptionHtml: '<p>New description</p>'
|
|
75
|
+
})
|
|
76
|
+
}));
|
|
77
|
+
});
|
|
78
|
+
it('should update description with HTML (no wrapping)', async () => {
|
|
79
|
+
const htmlContent = '<h1>Title</h1><p>Content</p>';
|
|
80
|
+
const mockUpdateResponse = {
|
|
81
|
+
data: {
|
|
82
|
+
productUpdate: {
|
|
83
|
+
product: { id: mockProduct.id, title: mockProduct.title, descriptionHtml: htmlContent },
|
|
84
|
+
userErrors: []
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
const mockProductResponse = {
|
|
89
|
+
data: {
|
|
90
|
+
product: { ...mockProduct, descriptionHtml: htmlContent }
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
vi.mocked(shopifyClient.shopifyGraphQL)
|
|
94
|
+
.mockResolvedValueOnce(mockUpdateResponse)
|
|
95
|
+
.mockResolvedValueOnce(mockProductResponse);
|
|
96
|
+
const result = await ProductService.updateDescription(mockCredentials, {
|
|
97
|
+
id: '123456789',
|
|
98
|
+
description: htmlContent,
|
|
99
|
+
isHtml: true
|
|
100
|
+
});
|
|
101
|
+
expect(result.success).toBe(true);
|
|
102
|
+
expect(shopifyClient.shopifyGraphQL).toHaveBeenCalledWith(mockCredentials, expect.stringContaining('mutation productUpdate'), expect.objectContaining({
|
|
103
|
+
input: expect.objectContaining({
|
|
104
|
+
descriptionHtml: htmlContent
|
|
105
|
+
})
|
|
106
|
+
}));
|
|
107
|
+
});
|
|
108
|
+
it('should handle user errors', async () => {
|
|
109
|
+
const mockResponse = {
|
|
110
|
+
data: {
|
|
111
|
+
productUpdate: {
|
|
112
|
+
product: { id: mockProduct.id, title: mockProduct.title },
|
|
113
|
+
userErrors: [{ field: ['description'], message: 'Description is invalid' }]
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
vi.mocked(shopifyClient.shopifyGraphQL).mockResolvedValue(mockResponse);
|
|
118
|
+
const result = await ProductService.updateDescription(mockCredentials, {
|
|
119
|
+
id: '123456789',
|
|
120
|
+
description: 'Test'
|
|
121
|
+
});
|
|
122
|
+
expect(result.success).toBe(false);
|
|
123
|
+
expect(result.error).toContain('Description is invalid');
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
describe('updatePrice', () => {
|
|
127
|
+
it('should update price using bulk mutation', async () => {
|
|
128
|
+
const mockProductResponse = {
|
|
129
|
+
data: {
|
|
130
|
+
product: mockProduct
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
const mockUpdateResponse = {
|
|
134
|
+
data: {
|
|
135
|
+
productVariantsBulkUpdate: {
|
|
136
|
+
productVariants: [
|
|
137
|
+
{
|
|
138
|
+
id: mockVariant.id,
|
|
139
|
+
price: '34.99',
|
|
140
|
+
compareAtPrice: null
|
|
141
|
+
}
|
|
142
|
+
],
|
|
143
|
+
userErrors: []
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
vi.mocked(shopifyClient.shopifyGraphQL)
|
|
148
|
+
.mockResolvedValueOnce(mockProductResponse)
|
|
149
|
+
.mockResolvedValueOnce(mockUpdateResponse)
|
|
150
|
+
.mockResolvedValueOnce(mockProductResponse);
|
|
151
|
+
const result = await ProductService.updatePrice(mockCredentials, {
|
|
152
|
+
id: '123456789',
|
|
153
|
+
price: '34.99'
|
|
154
|
+
});
|
|
155
|
+
expect(result.success).toBe(true);
|
|
156
|
+
expect(shopifyClient.shopifyGraphQL).toHaveBeenCalledWith(mockCredentials, expect.stringContaining('productVariantsBulkUpdate'), expect.objectContaining({
|
|
157
|
+
productId: 'gid://shopify/Product/123456789',
|
|
158
|
+
variants: expect.arrayContaining([
|
|
159
|
+
expect.objectContaining({
|
|
160
|
+
price: '34.99'
|
|
161
|
+
})
|
|
162
|
+
])
|
|
163
|
+
}));
|
|
164
|
+
});
|
|
165
|
+
it('should update price with compare-at price', async () => {
|
|
166
|
+
const mockProductResponse = {
|
|
167
|
+
data: {
|
|
168
|
+
product: mockProduct
|
|
169
|
+
}
|
|
170
|
+
};
|
|
171
|
+
const mockUpdateResponse = {
|
|
172
|
+
data: {
|
|
173
|
+
productVariantsBulkUpdate: {
|
|
174
|
+
productVariants: [
|
|
175
|
+
{
|
|
176
|
+
id: mockVariant.id,
|
|
177
|
+
price: '24.99',
|
|
178
|
+
compareAtPrice: '39.99'
|
|
179
|
+
}
|
|
180
|
+
],
|
|
181
|
+
userErrors: []
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
};
|
|
185
|
+
vi.mocked(shopifyClient.shopifyGraphQL)
|
|
186
|
+
.mockResolvedValueOnce(mockProductResponse)
|
|
187
|
+
.mockResolvedValueOnce(mockUpdateResponse)
|
|
188
|
+
.mockResolvedValueOnce(mockProductResponse);
|
|
189
|
+
const result = await ProductService.updatePrice(mockCredentials, {
|
|
190
|
+
id: '123456789',
|
|
191
|
+
price: '24.99',
|
|
192
|
+
compareAtPrice: '39.99'
|
|
193
|
+
});
|
|
194
|
+
expect(result.success).toBe(true);
|
|
195
|
+
expect(shopifyClient.shopifyGraphQL).toHaveBeenCalledWith(mockCredentials, expect.stringContaining('productVariantsBulkUpdate'), expect.objectContaining({
|
|
196
|
+
variants: expect.arrayContaining([
|
|
197
|
+
expect.objectContaining({
|
|
198
|
+
price: '24.99',
|
|
199
|
+
compareAtPrice: '39.99'
|
|
200
|
+
})
|
|
201
|
+
])
|
|
202
|
+
}));
|
|
203
|
+
});
|
|
204
|
+
it('should fail with multiple variants without variant-id', async () => {
|
|
205
|
+
const mockResponse = {
|
|
206
|
+
data: {
|
|
207
|
+
product: mockProductMultipleVariants
|
|
208
|
+
}
|
|
209
|
+
};
|
|
210
|
+
vi.mocked(shopifyClient.shopifyGraphQL).mockResolvedValue(mockResponse);
|
|
211
|
+
const result = await ProductService.updatePrice(mockCredentials, {
|
|
212
|
+
id: '123456789',
|
|
213
|
+
price: '29.99'
|
|
214
|
+
});
|
|
215
|
+
expect(result.success).toBe(false);
|
|
216
|
+
expect(result.error).toContain('multiple variants');
|
|
217
|
+
expect(result.error).toContain('--variant-id');
|
|
218
|
+
});
|
|
219
|
+
it('should validate price format', async () => {
|
|
220
|
+
const result = await ProductService.updatePrice(mockCredentials, {
|
|
221
|
+
id: '123456789',
|
|
222
|
+
price: 'invalid'
|
|
223
|
+
});
|
|
224
|
+
expect(result.success).toBe(false);
|
|
225
|
+
expect(result.error).toContain('Invalid price format');
|
|
226
|
+
});
|
|
227
|
+
it('should validate compare-at price format', async () => {
|
|
228
|
+
const result = await ProductService.updatePrice(mockCredentials, {
|
|
229
|
+
id: '123456789',
|
|
230
|
+
price: '29.99',
|
|
231
|
+
compareAtPrice: 'invalid'
|
|
232
|
+
});
|
|
233
|
+
expect(result.success).toBe(false);
|
|
234
|
+
expect(result.error).toContain('Invalid compare-at price format');
|
|
235
|
+
});
|
|
236
|
+
});
|
|
237
|
+
describe('updateTemplate', () => {
|
|
238
|
+
it('should update template suffix', async () => {
|
|
239
|
+
const mockUpdateResponse = {
|
|
240
|
+
data: {
|
|
241
|
+
productUpdate: {
|
|
242
|
+
product: {
|
|
243
|
+
id: mockProduct.id,
|
|
244
|
+
title: mockProduct.title,
|
|
245
|
+
templateSuffix: 'premium'
|
|
246
|
+
},
|
|
247
|
+
userErrors: []
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
};
|
|
251
|
+
const mockProductResponse = {
|
|
252
|
+
data: {
|
|
253
|
+
product: { ...mockProduct, templateSuffix: 'premium' }
|
|
254
|
+
}
|
|
255
|
+
};
|
|
256
|
+
vi.mocked(shopifyClient.shopifyGraphQL)
|
|
257
|
+
.mockResolvedValueOnce(mockUpdateResponse)
|
|
258
|
+
.mockResolvedValueOnce(mockProductResponse);
|
|
259
|
+
const result = await ProductService.updateTemplate(mockCredentials, {
|
|
260
|
+
id: '123456789',
|
|
261
|
+
template: 'premium'
|
|
262
|
+
});
|
|
263
|
+
expect(result.success).toBe(true);
|
|
264
|
+
expect(shopifyClient.shopifyGraphQL).toHaveBeenCalledWith(mockCredentials, expect.stringContaining('mutation productUpdate'), expect.objectContaining({
|
|
265
|
+
input: expect.objectContaining({
|
|
266
|
+
templateSuffix: 'premium'
|
|
267
|
+
})
|
|
268
|
+
}));
|
|
269
|
+
});
|
|
270
|
+
it('should clear template (set to null)', async () => {
|
|
271
|
+
const mockUpdateResponse = {
|
|
272
|
+
data: {
|
|
273
|
+
productUpdate: {
|
|
274
|
+
product: {
|
|
275
|
+
id: mockProduct.id,
|
|
276
|
+
title: mockProduct.title,
|
|
277
|
+
templateSuffix: null
|
|
278
|
+
},
|
|
279
|
+
userErrors: []
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
};
|
|
283
|
+
const mockProductResponse = {
|
|
284
|
+
data: {
|
|
285
|
+
product: { ...mockProduct, templateSuffix: null }
|
|
286
|
+
}
|
|
287
|
+
};
|
|
288
|
+
vi.mocked(shopifyClient.shopifyGraphQL)
|
|
289
|
+
.mockResolvedValueOnce(mockUpdateResponse)
|
|
290
|
+
.mockResolvedValueOnce(mockProductResponse);
|
|
291
|
+
const result = await ProductService.updateTemplate(mockCredentials, {
|
|
292
|
+
id: '123456789',
|
|
293
|
+
template: 'default'
|
|
294
|
+
});
|
|
295
|
+
expect(result.success).toBe(true);
|
|
296
|
+
});
|
|
297
|
+
});
|
|
298
|
+
});
|
|
299
|
+
//# sourceMappingURL=service.test.js.map
|