@hasna/connectors 1.3.22 → 1.3.23
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/bin/index.js +109 -59
- package/bin/mcp.js +236 -182
- package/bin/serve.js +77 -33
- package/connectors/connect-dropbox/src/api/bulk.ts +285 -0
- package/connectors/connect-dropbox/src/api/index.ts +4 -0
- package/connectors/connect-dropbox/src/cli/index.ts +151 -0
- package/connectors/connect-github/src/api/bulk.ts +328 -0
- package/connectors/connect-github/src/api/index.ts +4 -0
- package/connectors/connect-github/src/cli/index.ts +145 -0
- package/connectors/connect-gmail/src/api/history.ts +82 -0
- package/connectors/connect-gmail/src/api/index.ts +12 -0
- package/connectors/connect-gmail/src/api/settings.ts +280 -0
- package/connectors/connect-gmail/src/api/watch.ts +40 -0
- package/connectors/connect-gmail/src/cli/index.ts +237 -0
- package/connectors/connect-googleads/src/api/bulk.ts +354 -0
- package/connectors/connect-googleads/src/api/index.ts +4 -0
- package/connectors/connect-googleads/src/cli/index.ts +192 -0
- package/connectors/connect-googlecalendar/src/api/acl.ts +66 -0
- package/connectors/connect-googlecalendar/src/api/bulk.ts +452 -0
- package/connectors/connect-googlecalendar/src/api/freebusy.ts +35 -0
- package/connectors/connect-googlecalendar/src/api/index.ts +9 -0
- package/connectors/connect-googlecalendar/src/cli/index.ts +522 -0
- package/connectors/connect-googlecalendar/src/types/index.ts +90 -0
- package/connectors/connect-googlecontacts/src/api/bulk.ts +365 -0
- package/connectors/connect-googlecontacts/src/api/groups.ts +164 -0
- package/connectors/connect-googlecontacts/src/api/index.ts +8 -0
- package/connectors/connect-googlecontacts/src/cli/index.ts +340 -0
- package/connectors/connect-googledocs/src/api/bulk.ts +301 -0
- package/connectors/connect-googledocs/src/api/index.ts +4 -0
- package/connectors/connect-googledocs/src/cli/index.ts +187 -0
- package/connectors/connect-googledrive/src/api/bulk.ts +505 -0
- package/connectors/connect-googledrive/src/api/changes.ts +139 -0
- package/connectors/connect-googledrive/src/api/client.ts +34 -0
- package/connectors/connect-googledrive/src/api/index.ts +12 -0
- package/connectors/connect-googledrive/src/api/revisions.ts +132 -0
- package/connectors/connect-googledrive/src/cli/index.ts +624 -0
- package/connectors/connect-googledrive/src/index.ts +2 -0
- package/connectors/connect-googlephotos/src/api/bulk.ts +455 -0
- package/connectors/connect-googlephotos/src/api/index.ts +8 -0
- package/connectors/connect-googlephotos/src/cli/index.ts +185 -0
- package/connectors/connect-googletasks/src/api/bulk.ts +359 -0
- package/connectors/connect-googletasks/src/api/index.ts +1 -0
- package/connectors/connect-googletasks/src/cli/index.ts +178 -0
- package/connectors/connect-linear/src/api/bulk.ts +219 -0
- package/connectors/connect-linear/src/api/index.ts +4 -0
- package/connectors/connect-linear/src/cli/index.ts +116 -0
- package/connectors/connect-shopify/src/api/bulk.ts +288 -0
- package/connectors/connect-shopify/src/api/index.ts +4 -0
- package/connectors/connect-shopify/src/cli/index.ts +108 -0
- package/connectors/connect-slack/src/api/bulk.ts +237 -0
- package/connectors/connect-slack/src/api/index.ts +4 -0
- package/connectors/connect-slack/src/cli/index.ts +157 -0
- package/connectors/connect-stripe/src/api/bulk.ts +488 -0
- package/connectors/connect-stripe/src/api/index.ts +4 -0
- package/connectors/connect-stripe/src/cli/index.ts +206 -0
- package/dist/index.js +75 -33
- package/dist/lib/runner.d.ts +15 -5
- package/package.json +2 -2
- package/connectors/connect-cloudflare/bun.lock +0 -32
- package/connectors/connect-gmail/bin/index.js +0 -7027
- package/connectors/connect-gmail/dist/cli/index.js +0 -7035
- package/connectors/connect-gmail/dist/index.js +0 -2174
- package/dist/server/server.test.d.ts +0 -1
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import type { LinearClient } from './client';
|
|
2
|
+
|
|
3
|
+
// ============================================
|
|
4
|
+
// Bulk Operation Types
|
|
5
|
+
// ============================================
|
|
6
|
+
|
|
7
|
+
export interface BulkOperationOptions {
|
|
8
|
+
/** Maximum concurrent API calls (default: 10) */
|
|
9
|
+
concurrency?: number;
|
|
10
|
+
/** Dry run - don't actually modify */
|
|
11
|
+
dryRun?: boolean;
|
|
12
|
+
/** Progress callback */
|
|
13
|
+
onProgress?: (current: number, total: number, item: unknown) => void;
|
|
14
|
+
/** Error callback */
|
|
15
|
+
onError?: (error: Error, item: unknown) => void;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// --- Issue Bulk Operations ---
|
|
19
|
+
|
|
20
|
+
export interface BulkIssueOptions extends BulkOperationOptions {
|
|
21
|
+
issueIds: string[];
|
|
22
|
+
/** Action to perform */
|
|
23
|
+
action: 'archive' | 'change_state' | 'assign' | 'set_priority' | 'add_to_project';
|
|
24
|
+
/** State ID (for change_state) */
|
|
25
|
+
stateId?: string;
|
|
26
|
+
/** User ID (for assign) */
|
|
27
|
+
assigneeId?: string;
|
|
28
|
+
/** Priority 0-4 (for set_priority) */
|
|
29
|
+
priority?: number;
|
|
30
|
+
/** Project ID (for add_to_project) */
|
|
31
|
+
projectId?: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface BulkIssueResult {
|
|
35
|
+
total: number;
|
|
36
|
+
success: number;
|
|
37
|
+
failed: number;
|
|
38
|
+
errors: Array<{ issueId: string; error: string }>;
|
|
39
|
+
results: Array<{ issueId: string; response: unknown }>;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// --- Label Bulk Operations ---
|
|
43
|
+
|
|
44
|
+
export interface BulkLabelOptions extends BulkOperationOptions {
|
|
45
|
+
/** Issues to apply/remove label from */
|
|
46
|
+
issueIds: string[];
|
|
47
|
+
/** Label ID to add or remove */
|
|
48
|
+
labelId: string;
|
|
49
|
+
/** Action: add or remove label */
|
|
50
|
+
action: 'add' | 'remove';
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface BulkLabelResult {
|
|
54
|
+
total: number;
|
|
55
|
+
success: number;
|
|
56
|
+
failed: number;
|
|
57
|
+
errors: Array<{ issueId: string; error: string }>;
|
|
58
|
+
results: Array<{ issueId: string; response: unknown }>;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// ============================================
|
|
62
|
+
// Bulk Operations API
|
|
63
|
+
// ============================================
|
|
64
|
+
|
|
65
|
+
export class BulkApi {
|
|
66
|
+
private readonly client: LinearClient;
|
|
67
|
+
|
|
68
|
+
constructor(client: LinearClient) {
|
|
69
|
+
this.client = client;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// ============================================
|
|
73
|
+
// Bulk Issue Operations
|
|
74
|
+
// ============================================
|
|
75
|
+
|
|
76
|
+
async issues(options: BulkIssueOptions): Promise<BulkIssueResult> {
|
|
77
|
+
const { issueIds, action, concurrency = 10, dryRun = false, onProgress, onError, stateId, assigneeId, priority, projectId } = options;
|
|
78
|
+
|
|
79
|
+
const result: BulkIssueResult = {
|
|
80
|
+
total: issueIds.length,
|
|
81
|
+
success: 0,
|
|
82
|
+
failed: 0,
|
|
83
|
+
errors: [],
|
|
84
|
+
results: [],
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
if (issueIds.length === 0) return result;
|
|
88
|
+
|
|
89
|
+
const chunks = this.chunkArray(issueIds, concurrency);
|
|
90
|
+
|
|
91
|
+
for (const chunk of chunks) {
|
|
92
|
+
await Promise.all(
|
|
93
|
+
chunk.map(async (issueId) => {
|
|
94
|
+
try {
|
|
95
|
+
if (dryRun) {
|
|
96
|
+
result.success++;
|
|
97
|
+
} else {
|
|
98
|
+
let response: unknown;
|
|
99
|
+
if (action === 'archive') {
|
|
100
|
+
response = await this.archiveIssue(issueId);
|
|
101
|
+
} else if (action === 'change_state') {
|
|
102
|
+
if (!stateId) throw new Error('stateId is required for change_state');
|
|
103
|
+
response = await this.updateIssue(issueId, { stateId });
|
|
104
|
+
} else if (action === 'assign') {
|
|
105
|
+
if (!assigneeId) throw new Error('assigneeId is required for assign');
|
|
106
|
+
response = await this.updateIssue(issueId, { assigneeId });
|
|
107
|
+
} else if (action === 'set_priority') {
|
|
108
|
+
if (priority === undefined) throw new Error('priority is required for set_priority');
|
|
109
|
+
response = await this.updateIssue(issueId, { priority });
|
|
110
|
+
} else if (action === 'add_to_project') {
|
|
111
|
+
if (!projectId) throw new Error('projectId is required for add_to_project');
|
|
112
|
+
response = await this.updateIssue(issueId, { projectId });
|
|
113
|
+
}
|
|
114
|
+
result.success++;
|
|
115
|
+
result.results.push({ issueId, response });
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (onProgress) onProgress(result.success + result.failed, result.total, issueId);
|
|
119
|
+
} catch (err) {
|
|
120
|
+
result.failed++;
|
|
121
|
+
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
122
|
+
result.errors.push({ issueId, error: errorMessage });
|
|
123
|
+
if (onError) onError(err instanceof Error ? err : new Error(errorMessage), issueId);
|
|
124
|
+
}
|
|
125
|
+
})
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return result;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// ============================================
|
|
133
|
+
// Bulk Label Operations
|
|
134
|
+
// ============================================
|
|
135
|
+
|
|
136
|
+
async labels(options: BulkLabelOptions): Promise<BulkLabelResult> {
|
|
137
|
+
const { issueIds, labelId, action, concurrency = 10, dryRun = false, onProgress, onError } = options;
|
|
138
|
+
|
|
139
|
+
const result: BulkLabelResult = {
|
|
140
|
+
total: issueIds.length,
|
|
141
|
+
success: 0,
|
|
142
|
+
failed: 0,
|
|
143
|
+
errors: [],
|
|
144
|
+
results: [],
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
if (issueIds.length === 0) return result;
|
|
148
|
+
|
|
149
|
+
const chunks = this.chunkArray(issueIds, concurrency);
|
|
150
|
+
|
|
151
|
+
for (const chunk of chunks) {
|
|
152
|
+
await Promise.all(
|
|
153
|
+
chunk.map(async (issueId) => {
|
|
154
|
+
try {
|
|
155
|
+
if (dryRun) {
|
|
156
|
+
result.success++;
|
|
157
|
+
} else {
|
|
158
|
+
// Linear doesn't have a direct add/remove label mutation,
|
|
159
|
+
// so we update the issue's labelIds
|
|
160
|
+
const response = await this.updateIssue(issueId, {
|
|
161
|
+
labelIds: action === 'add' ? [labelId] : undefined,
|
|
162
|
+
});
|
|
163
|
+
result.success++;
|
|
164
|
+
result.results.push({ issueId, response });
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (onProgress) onProgress(result.success + result.failed, result.total, issueId);
|
|
168
|
+
} catch (err) {
|
|
169
|
+
result.failed++;
|
|
170
|
+
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
171
|
+
result.errors.push({ issueId, error: errorMessage });
|
|
172
|
+
if (onError) onError(err instanceof Error ? err : new Error(errorMessage), issueId);
|
|
173
|
+
}
|
|
174
|
+
})
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return result;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// ============================================
|
|
182
|
+
// Private Helpers
|
|
183
|
+
// ============================================
|
|
184
|
+
|
|
185
|
+
private async updateIssue(id: string, input: Record<string, unknown>): Promise<unknown> {
|
|
186
|
+
const mutation = `
|
|
187
|
+
mutation IssueUpdate($id: String!, $input: IssueUpdateInput!) {
|
|
188
|
+
issueUpdate(id: $id, input: $input) {
|
|
189
|
+
success
|
|
190
|
+
issue {
|
|
191
|
+
id
|
|
192
|
+
identifier
|
|
193
|
+
title
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
`;
|
|
198
|
+
return this.client.mutate(mutation, { id, input });
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
private async archiveIssue(id: string): Promise<unknown> {
|
|
202
|
+
const mutation = `
|
|
203
|
+
mutation IssueArchive($id: String!) {
|
|
204
|
+
issueArchive(id: $id) {
|
|
205
|
+
success
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
`;
|
|
209
|
+
return this.client.mutate(mutation, { id });
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
private chunkArray<T>(array: T[], size: number): T[][] {
|
|
213
|
+
const chunks: T[][] = [];
|
|
214
|
+
for (let i = 0; i < array.length; i += size) {
|
|
215
|
+
chunks.push(array.slice(i, i + size));
|
|
216
|
+
}
|
|
217
|
+
return chunks;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
@@ -6,6 +6,7 @@ import { TeamsApi } from './teams';
|
|
|
6
6
|
import { UsersApi } from './users';
|
|
7
7
|
import { CommentsApi } from './comments';
|
|
8
8
|
import { StatesApi } from './states';
|
|
9
|
+
import { BulkApi } from './bulk';
|
|
9
10
|
|
|
10
11
|
export { LinearClient } from './client';
|
|
11
12
|
export { IssuesApi } from './issues';
|
|
@@ -14,6 +15,7 @@ export { TeamsApi } from './teams';
|
|
|
14
15
|
export { UsersApi } from './users';
|
|
15
16
|
export { CommentsApi } from './comments';
|
|
16
17
|
export { StatesApi } from './states';
|
|
18
|
+
export { BulkApi } from './bulk';
|
|
17
19
|
|
|
18
20
|
/**
|
|
19
21
|
* Main Linear API class
|
|
@@ -27,6 +29,7 @@ export class Linear {
|
|
|
27
29
|
public readonly users: UsersApi;
|
|
28
30
|
public readonly comments: CommentsApi;
|
|
29
31
|
public readonly states: StatesApi;
|
|
32
|
+
public readonly bulk: BulkApi;
|
|
30
33
|
|
|
31
34
|
constructor(config: LinearConfig) {
|
|
32
35
|
this.client = new LinearClient(config);
|
|
@@ -36,6 +39,7 @@ export class Linear {
|
|
|
36
39
|
this.users = new UsersApi(this.client);
|
|
37
40
|
this.comments = new CommentsApi(this.client);
|
|
38
41
|
this.states = new StatesApi(this.client);
|
|
42
|
+
this.bulk = new BulkApi(this.client);
|
|
39
43
|
}
|
|
40
44
|
|
|
41
45
|
/**
|
|
@@ -557,4 +557,120 @@ usersCmd
|
|
|
557
557
|
}
|
|
558
558
|
});
|
|
559
559
|
|
|
560
|
+
// ============================================
|
|
561
|
+
// Bulk Operations
|
|
562
|
+
// ============================================
|
|
563
|
+
|
|
564
|
+
const bulkCmd = program
|
|
565
|
+
.command('bulk')
|
|
566
|
+
.description('Bulk operations');
|
|
567
|
+
|
|
568
|
+
bulkCmd
|
|
569
|
+
.command('issues')
|
|
570
|
+
.description('Bulk issue operations')
|
|
571
|
+
.option('--ids <ids>', 'Comma-separated issue IDs')
|
|
572
|
+
.option('--action <action>', 'Action: archive/change_state/assign/set_priority/add_to_project')
|
|
573
|
+
.option('--state-id <id>', 'Target state ID (for change_state)')
|
|
574
|
+
.option('--assignee-id <id>', 'Target user ID (for assign)')
|
|
575
|
+
.option('--priority <n>', 'Priority 0-4 (for set_priority)')
|
|
576
|
+
.option('--project-id <id>', 'Target project ID (for add_to_project)')
|
|
577
|
+
.option('--concurrency <n>', 'Max concurrent requests', '10')
|
|
578
|
+
.option('--dry-run', 'Preview without executing')
|
|
579
|
+
.action(async (opts) => {
|
|
580
|
+
try {
|
|
581
|
+
const client = getClient();
|
|
582
|
+
const format = program.opts().format as OutputFormat;
|
|
583
|
+
|
|
584
|
+
if (!opts.ids || !opts.action) {
|
|
585
|
+
error('--ids and --action are required');
|
|
586
|
+
process.exit(1);
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
const issueIds = opts.ids.split(',').map((id: string) => id.trim()).filter(Boolean);
|
|
590
|
+
|
|
591
|
+
info(`Bulk ${opts.action} on ${issueIds.length} issues (concurrency: ${opts.concurrency}, dry-run: ${!!opts.dryRun})`);
|
|
592
|
+
|
|
593
|
+
const result = await client.bulk.issues({
|
|
594
|
+
issueIds,
|
|
595
|
+
action: opts.action,
|
|
596
|
+
stateId: opts.stateId,
|
|
597
|
+
assigneeId: opts.assigneeId,
|
|
598
|
+
priority: opts.priority ? parseInt(opts.priority, 10) : undefined,
|
|
599
|
+
projectId: opts.projectId,
|
|
600
|
+
concurrency: parseInt(opts.concurrency, 10),
|
|
601
|
+
dryRun: opts.dryRun,
|
|
602
|
+
onProgress: (current, total) => {
|
|
603
|
+
process.stdout.write(`\rProgress: ${current}/${total}`);
|
|
604
|
+
},
|
|
605
|
+
});
|
|
606
|
+
|
|
607
|
+
console.log();
|
|
608
|
+
if (format === 'json') {
|
|
609
|
+
print(result, format);
|
|
610
|
+
} else {
|
|
611
|
+
info(`Done: ${result.success} succeeded, ${result.failed} failed out of ${result.total}`);
|
|
612
|
+
if (result.errors.length > 0) {
|
|
613
|
+
error('Failures:');
|
|
614
|
+
result.errors.forEach((e: { issueId: string; error: string }) => {
|
|
615
|
+
console.error(` - ${e.issueId}: ${e.error}`);
|
|
616
|
+
});
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
} catch (e) {
|
|
620
|
+
error((e as Error).message);
|
|
621
|
+
process.exit(1);
|
|
622
|
+
}
|
|
623
|
+
});
|
|
624
|
+
|
|
625
|
+
bulkCmd
|
|
626
|
+
.command('labels')
|
|
627
|
+
.description('Bulk add/remove labels from issues')
|
|
628
|
+
.option('--issue-ids <ids>', 'Comma-separated issue IDs')
|
|
629
|
+
.option('--label-id <id>', 'Label ID to add or remove')
|
|
630
|
+
.option('--action <action>', 'Action: add or remove')
|
|
631
|
+
.option('--concurrency <n>', 'Max concurrent requests', '10')
|
|
632
|
+
.option('--dry-run', 'Preview without executing')
|
|
633
|
+
.action(async (opts) => {
|
|
634
|
+
try {
|
|
635
|
+
const client = getClient();
|
|
636
|
+
const format = program.opts().format as OutputFormat;
|
|
637
|
+
|
|
638
|
+
if (!opts.issueIds || !opts.labelId || !opts.action) {
|
|
639
|
+
error('--issue-ids, --label-id, and --action are required');
|
|
640
|
+
process.exit(1);
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
const issueIds = opts.issueIds.split(',').map((id: string) => id.trim()).filter(Boolean);
|
|
644
|
+
|
|
645
|
+
info(`Bulk ${opts.action} label ${opts.labelId} to ${issueIds.length} issues (concurrency: ${opts.concurrency}, dry-run: ${!!opts.dryRun})`);
|
|
646
|
+
|
|
647
|
+
const result = await client.bulk.labels({
|
|
648
|
+
issueIds,
|
|
649
|
+
labelId: opts.labelId,
|
|
650
|
+
action: opts.action as 'add' | 'remove',
|
|
651
|
+
concurrency: parseInt(opts.concurrency, 10),
|
|
652
|
+
dryRun: opts.dryRun,
|
|
653
|
+
onProgress: (current, total) => {
|
|
654
|
+
process.stdout.write(`\rProgress: ${current}/${total}`);
|
|
655
|
+
},
|
|
656
|
+
});
|
|
657
|
+
|
|
658
|
+
console.log();
|
|
659
|
+
if (format === 'json') {
|
|
660
|
+
print(result, format);
|
|
661
|
+
} else {
|
|
662
|
+
info(`Done: ${result.success} succeeded, ${result.failed} failed out of ${result.total}`);
|
|
663
|
+
if (result.errors.length > 0) {
|
|
664
|
+
error('Failures:');
|
|
665
|
+
result.errors.forEach((e: { issueId: string; error: string }) => {
|
|
666
|
+
console.error(` - ${e.issueId}: ${e.error}`);
|
|
667
|
+
});
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
} catch (e) {
|
|
671
|
+
error((e as Error).message);
|
|
672
|
+
process.exit(1);
|
|
673
|
+
}
|
|
674
|
+
});
|
|
675
|
+
|
|
560
676
|
program.parse();
|
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
import type { ShopifyClient } from './client';
|
|
2
|
+
|
|
3
|
+
// ============================================
|
|
4
|
+
// Bulk Operation Types
|
|
5
|
+
// ============================================
|
|
6
|
+
|
|
7
|
+
export interface BulkOperationOptions {
|
|
8
|
+
/** Maximum concurrent API calls (default: 10) */
|
|
9
|
+
concurrency?: number;
|
|
10
|
+
/** Dry run - don't actually modify */
|
|
11
|
+
dryRun?: boolean;
|
|
12
|
+
/** Progress callback */
|
|
13
|
+
onProgress?: (current: number, total: number, item: unknown) => void;
|
|
14
|
+
/** Error callback */
|
|
15
|
+
onError?: (error: Error, item: unknown) => void;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// --- Product Bulk Operations ---
|
|
19
|
+
|
|
20
|
+
export interface BulkProductOptions extends BulkOperationOptions {
|
|
21
|
+
productIds: number[];
|
|
22
|
+
/** Action to perform */
|
|
23
|
+
action: 'delete' | 'activate' | 'archive';
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface BulkProductResult {
|
|
27
|
+
total: number;
|
|
28
|
+
success: number;
|
|
29
|
+
failed: number;
|
|
30
|
+
errors: Array<{ productId: number; error: string }>;
|
|
31
|
+
results: Array<{ productId: number; response: unknown }>;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// --- Order Bulk Operations ---
|
|
35
|
+
|
|
36
|
+
export interface BulkOrderOptions extends BulkOperationOptions {
|
|
37
|
+
orderIds: number[];
|
|
38
|
+
/** Action to perform */
|
|
39
|
+
action: 'close' | 'open' | 'cancel';
|
|
40
|
+
/** Cancel reason (for cancel action only) */
|
|
41
|
+
cancelReason?: 'customer' | 'inventory' | 'fraud' | 'declined' | 'other';
|
|
42
|
+
/** Send cancel email notification */
|
|
43
|
+
cancelEmail?: boolean;
|
|
44
|
+
/** Restock items on cancel */
|
|
45
|
+
cancelRestock?: boolean;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface BulkOrderResult {
|
|
49
|
+
total: number;
|
|
50
|
+
success: number;
|
|
51
|
+
failed: number;
|
|
52
|
+
errors: Array<{ orderId: number; error: string }>;
|
|
53
|
+
results: Array<{ orderId: number; response: unknown }>;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// --- Inventory Bulk Operations ---
|
|
57
|
+
|
|
58
|
+
export interface BulkInventoryOptions extends BulkOperationOptions {
|
|
59
|
+
/** Items with their target levels per location */
|
|
60
|
+
items: Array<{ inventoryItemId: number; locationId: number; available: number }>;
|
|
61
|
+
/** Action: set to exact value, or adjust by amount */
|
|
62
|
+
action: 'set' | 'adjust';
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface BulkInventoryResult {
|
|
66
|
+
total: number;
|
|
67
|
+
success: number;
|
|
68
|
+
failed: number;
|
|
69
|
+
errors: Array<{ inventoryItemId: number; locationId: number; error: string }>;
|
|
70
|
+
results: Array<{ inventoryItemId: number; locationId: number; response: unknown }>;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// ============================================
|
|
74
|
+
// Bulk Operations API
|
|
75
|
+
// ============================================
|
|
76
|
+
|
|
77
|
+
export class BulkApi {
|
|
78
|
+
private readonly client: ShopifyClient;
|
|
79
|
+
|
|
80
|
+
constructor(client: ShopifyClient) {
|
|
81
|
+
this.client = client;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// ============================================
|
|
85
|
+
// Bulk Product Operations
|
|
86
|
+
// ============================================
|
|
87
|
+
|
|
88
|
+
async products(options: BulkProductOptions): Promise<BulkProductResult> {
|
|
89
|
+
const { productIds, action, concurrency = 10, dryRun = false, onProgress, onError } = options;
|
|
90
|
+
|
|
91
|
+
const result: BulkProductResult = {
|
|
92
|
+
total: productIds.length,
|
|
93
|
+
success: 0,
|
|
94
|
+
failed: 0,
|
|
95
|
+
errors: [],
|
|
96
|
+
results: [],
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
if (productIds.length === 0) return result;
|
|
100
|
+
|
|
101
|
+
const chunks = this.chunkArray(productIds, concurrency);
|
|
102
|
+
|
|
103
|
+
for (const chunk of chunks) {
|
|
104
|
+
await Promise.all(
|
|
105
|
+
chunk.map(async (productId) => {
|
|
106
|
+
try {
|
|
107
|
+
if (dryRun) {
|
|
108
|
+
result.success++;
|
|
109
|
+
} else {
|
|
110
|
+
let response: unknown;
|
|
111
|
+
if (action === 'delete') {
|
|
112
|
+
await this.client.request<void>(`/products/${productId}.json`, { method: 'DELETE' });
|
|
113
|
+
response = { deleted: productId };
|
|
114
|
+
} else {
|
|
115
|
+
response = await this.client.request<{ product: Record<string, unknown> }>(
|
|
116
|
+
`/products/${productId}.json`,
|
|
117
|
+
{
|
|
118
|
+
method: 'PUT',
|
|
119
|
+
body: { product: { id: productId, status: action === 'activate' ? 'active' : 'archived' } },
|
|
120
|
+
}
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
result.success++;
|
|
124
|
+
result.results.push({ productId, response });
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (onProgress) onProgress(result.success + result.failed, result.total, productId);
|
|
128
|
+
} catch (err) {
|
|
129
|
+
result.failed++;
|
|
130
|
+
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
131
|
+
result.errors.push({ productId, error: errorMessage });
|
|
132
|
+
if (onError) onError(err instanceof Error ? err : new Error(errorMessage), productId);
|
|
133
|
+
}
|
|
134
|
+
})
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return result;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// ============================================
|
|
142
|
+
// Bulk Order Operations
|
|
143
|
+
// ============================================
|
|
144
|
+
|
|
145
|
+
async orders(options: BulkOrderOptions): Promise<BulkOrderResult> {
|
|
146
|
+
const { orderIds, action, cancelReason, cancelEmail, cancelRestock, concurrency = 10, dryRun = false, onProgress, onError } = options;
|
|
147
|
+
|
|
148
|
+
const result: BulkOrderResult = {
|
|
149
|
+
total: orderIds.length,
|
|
150
|
+
success: 0,
|
|
151
|
+
failed: 0,
|
|
152
|
+
errors: [],
|
|
153
|
+
results: [],
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
if (orderIds.length === 0) return result;
|
|
157
|
+
|
|
158
|
+
const chunks = this.chunkArray(orderIds, concurrency);
|
|
159
|
+
|
|
160
|
+
for (const chunk of chunks) {
|
|
161
|
+
await Promise.all(
|
|
162
|
+
chunk.map(async (orderId) => {
|
|
163
|
+
try {
|
|
164
|
+
if (dryRun) {
|
|
165
|
+
result.success++;
|
|
166
|
+
} else {
|
|
167
|
+
let response: unknown;
|
|
168
|
+
if (action === 'close') {
|
|
169
|
+
response = await this.client.request<{ order: Record<string, unknown> }>(
|
|
170
|
+
`/orders/${orderId}/close.json`,
|
|
171
|
+
{ method: 'POST' }
|
|
172
|
+
);
|
|
173
|
+
} else if (action === 'open') {
|
|
174
|
+
response = await this.client.request<{ order: Record<string, unknown> }>(
|
|
175
|
+
`/orders/${orderId}/open.json`,
|
|
176
|
+
{ method: 'POST' }
|
|
177
|
+
);
|
|
178
|
+
} else {
|
|
179
|
+
const body: Record<string, unknown> = {};
|
|
180
|
+
if (cancelReason) body.reason = cancelReason;
|
|
181
|
+
if (cancelEmail !== undefined) body.email = cancelEmail;
|
|
182
|
+
if (cancelRestock !== undefined) body.restock = cancelRestock;
|
|
183
|
+
response = await this.client.request<{ order: Record<string, unknown> }>(
|
|
184
|
+
`/orders/${orderId}/cancel.json`,
|
|
185
|
+
{ method: 'POST', body: Object.keys(body).length > 0 ? body : undefined }
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
result.success++;
|
|
189
|
+
result.results.push({ orderId, response });
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (onProgress) onProgress(result.success + result.failed, result.total, orderId);
|
|
193
|
+
} catch (err) {
|
|
194
|
+
result.failed++;
|
|
195
|
+
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
196
|
+
result.errors.push({ orderId, error: errorMessage });
|
|
197
|
+
if (onError) onError(err instanceof Error ? err : new Error(errorMessage), orderId);
|
|
198
|
+
}
|
|
199
|
+
})
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return result;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// ============================================
|
|
207
|
+
// Bulk Inventory Operations
|
|
208
|
+
// ============================================
|
|
209
|
+
|
|
210
|
+
async inventory(options: BulkInventoryOptions): Promise<BulkInventoryResult> {
|
|
211
|
+
const { items, action, concurrency = 10, dryRun = false, onProgress, onError } = options;
|
|
212
|
+
|
|
213
|
+
const result: BulkInventoryResult = {
|
|
214
|
+
total: items.length,
|
|
215
|
+
success: 0,
|
|
216
|
+
failed: 0,
|
|
217
|
+
errors: [],
|
|
218
|
+
results: [],
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
if (items.length === 0) return result;
|
|
222
|
+
|
|
223
|
+
const chunks = this.chunkArray(items, concurrency);
|
|
224
|
+
|
|
225
|
+
for (const chunk of chunks) {
|
|
226
|
+
await Promise.all(
|
|
227
|
+
chunk.map(async (item) => {
|
|
228
|
+
const { inventoryItemId, locationId, available } = item;
|
|
229
|
+
try {
|
|
230
|
+
if (dryRun) {
|
|
231
|
+
result.success++;
|
|
232
|
+
} else {
|
|
233
|
+
let response: unknown;
|
|
234
|
+
if (action === 'set') {
|
|
235
|
+
response = await this.client.request<{ inventory_level: Record<string, unknown> }>(
|
|
236
|
+
'/inventory_levels/set.json',
|
|
237
|
+
{
|
|
238
|
+
method: 'POST',
|
|
239
|
+
body: {
|
|
240
|
+
location_id: locationId,
|
|
241
|
+
inventory_item_id: inventoryItemId,
|
|
242
|
+
available,
|
|
243
|
+
},
|
|
244
|
+
}
|
|
245
|
+
);
|
|
246
|
+
} else {
|
|
247
|
+
response = await this.client.request<{ inventory_level: Record<string, unknown> }>(
|
|
248
|
+
'/inventory_levels/adjust.json',
|
|
249
|
+
{
|
|
250
|
+
method: 'POST',
|
|
251
|
+
body: {
|
|
252
|
+
location_id: locationId,
|
|
253
|
+
inventory_item_id: inventoryItemId,
|
|
254
|
+
available_adjustment: available,
|
|
255
|
+
},
|
|
256
|
+
}
|
|
257
|
+
);
|
|
258
|
+
}
|
|
259
|
+
result.success++;
|
|
260
|
+
result.results.push({ inventoryItemId, locationId, response });
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
if (onProgress) onProgress(result.success + result.failed, result.total, item);
|
|
264
|
+
} catch (err) {
|
|
265
|
+
result.failed++;
|
|
266
|
+
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
267
|
+
result.errors.push({ inventoryItemId, locationId, error: errorMessage });
|
|
268
|
+
if (onError) onError(err instanceof Error ? err : new Error(errorMessage), item);
|
|
269
|
+
}
|
|
270
|
+
})
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
return result;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// ============================================
|
|
278
|
+
// Helper Methods
|
|
279
|
+
// ============================================
|
|
280
|
+
|
|
281
|
+
private chunkArray<T>(array: T[], size: number): T[][] {
|
|
282
|
+
const chunks: T[][] = [];
|
|
283
|
+
for (let i = 0; i < array.length; i += size) {
|
|
284
|
+
chunks.push(array.slice(i, i + size));
|
|
285
|
+
}
|
|
286
|
+
return chunks;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
@@ -6,6 +6,7 @@ import { CustomersApi } from './customers';
|
|
|
6
6
|
import { InventoryApi } from './inventory';
|
|
7
7
|
import { CollectionsApi } from './collections';
|
|
8
8
|
import { ShopApi } from './shop';
|
|
9
|
+
import { BulkApi } from './bulk';
|
|
9
10
|
|
|
10
11
|
/**
|
|
11
12
|
* Main Shopify Connector class
|
|
@@ -21,6 +22,7 @@ export class Shopify {
|
|
|
21
22
|
public readonly inventory: InventoryApi;
|
|
22
23
|
public readonly collections: CollectionsApi;
|
|
23
24
|
public readonly shop: ShopApi;
|
|
25
|
+
public readonly bulk: BulkApi;
|
|
24
26
|
|
|
25
27
|
constructor(config: ShopifyConfig) {
|
|
26
28
|
this.client = new ShopifyClient(config);
|
|
@@ -30,6 +32,7 @@ export class Shopify {
|
|
|
30
32
|
this.inventory = new InventoryApi(this.client);
|
|
31
33
|
this.collections = new CollectionsApi(this.client);
|
|
32
34
|
this.shop = new ShopApi(this.client);
|
|
35
|
+
this.bulk = new BulkApi(this.client);
|
|
33
36
|
}
|
|
34
37
|
|
|
35
38
|
/**
|
|
@@ -87,3 +90,4 @@ export { CustomersApi } from './customers';
|
|
|
87
90
|
export { InventoryApi } from './inventory';
|
|
88
91
|
export { CollectionsApi } from './collections';
|
|
89
92
|
export { ShopApi } from './shop';
|
|
93
|
+
export { BulkApi } from './bulk';
|