@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
|
@@ -4,6 +4,8 @@ import chalk from 'chalk';
|
|
|
4
4
|
import { createServer } from 'http';
|
|
5
5
|
import { GoogleContacts } from '../api';
|
|
6
6
|
import { ContactsApi } from '../api/contacts';
|
|
7
|
+
import { BulkApi } from '../api/bulk';
|
|
8
|
+
import type { ContactSummary } from '../api/bulk';
|
|
7
9
|
import {
|
|
8
10
|
getClientId,
|
|
9
11
|
setClientId,
|
|
@@ -675,5 +677,343 @@ contactsCmd
|
|
|
675
677
|
}
|
|
676
678
|
});
|
|
677
679
|
|
|
680
|
+
// ============================================
|
|
681
|
+
// Groups Commands — CRUD + membership
|
|
682
|
+
// ============================================
|
|
683
|
+
const groupsCmd = program
|
|
684
|
+
.command('groups')
|
|
685
|
+
.description('Manage contact groups (CRUD + membership)');
|
|
686
|
+
|
|
687
|
+
groupsCmd
|
|
688
|
+
.command('list')
|
|
689
|
+
.description('List contact groups')
|
|
690
|
+
.option('-n, --max <number>', 'Maximum results', '100')
|
|
691
|
+
.option('--page-token <token>', 'Page token for pagination')
|
|
692
|
+
.action(async (opts) => {
|
|
693
|
+
try {
|
|
694
|
+
const client = await getClient();
|
|
695
|
+
const result = await client.groups.list({
|
|
696
|
+
pageSize: parseInt(opts.max),
|
|
697
|
+
pageToken: opts.pageToken,
|
|
698
|
+
});
|
|
699
|
+
print(result, getFormat(groupsCmd));
|
|
700
|
+
if ((result as any).nextPageToken) {
|
|
701
|
+
info(`More results available. Use --page-token ${(result as any).nextPageToken}`);
|
|
702
|
+
}
|
|
703
|
+
} catch (err) {
|
|
704
|
+
error(String(err));
|
|
705
|
+
process.exit(1);
|
|
706
|
+
}
|
|
707
|
+
});
|
|
708
|
+
|
|
709
|
+
groupsCmd
|
|
710
|
+
.command('get <groupResourceName>')
|
|
711
|
+
.description('Get a contact group')
|
|
712
|
+
.action(async (groupResourceName: string) => {
|
|
713
|
+
try {
|
|
714
|
+
const client = await getClient();
|
|
715
|
+
const group = await client.groups.get(groupResourceName);
|
|
716
|
+
print(group, getFormat(groupsCmd));
|
|
717
|
+
} catch (err) {
|
|
718
|
+
error(String(err));
|
|
719
|
+
process.exit(1);
|
|
720
|
+
}
|
|
721
|
+
});
|
|
722
|
+
|
|
723
|
+
groupsCmd
|
|
724
|
+
.command('create')
|
|
725
|
+
.description('Create a new contact group')
|
|
726
|
+
.requiredOption('--name <name>', 'Group name')
|
|
727
|
+
.action(async (opts) => {
|
|
728
|
+
try {
|
|
729
|
+
const client = await getClient();
|
|
730
|
+
const group = await client.groups.create({ name: opts.name });
|
|
731
|
+
success(`Group created: ${group.resourceName}`);
|
|
732
|
+
print(group, getFormat(groupsCmd));
|
|
733
|
+
} catch (err) {
|
|
734
|
+
error(String(err));
|
|
735
|
+
process.exit(1);
|
|
736
|
+
}
|
|
737
|
+
});
|
|
738
|
+
|
|
739
|
+
groupsCmd
|
|
740
|
+
.command('update <groupResourceName>')
|
|
741
|
+
.description('Update a contact group')
|
|
742
|
+
.requiredOption('--name <name>', 'New group name')
|
|
743
|
+
.action(async (groupResourceName: string, opts) => {
|
|
744
|
+
try {
|
|
745
|
+
const client = await getClient();
|
|
746
|
+
const group = await client.groups.update(groupResourceName, { name: opts.name });
|
|
747
|
+
success(`Group updated: ${group.resourceName}`);
|
|
748
|
+
print(group, getFormat(groupsCmd));
|
|
749
|
+
} catch (err) {
|
|
750
|
+
error(String(err));
|
|
751
|
+
process.exit(1);
|
|
752
|
+
}
|
|
753
|
+
});
|
|
754
|
+
|
|
755
|
+
groupsCmd
|
|
756
|
+
.command('delete <groupResourceName>')
|
|
757
|
+
.description('Delete a contact group')
|
|
758
|
+
.action(async (groupResourceName: string) => {
|
|
759
|
+
try {
|
|
760
|
+
const client = await getClient();
|
|
761
|
+
await client.groups.delete(groupResourceName);
|
|
762
|
+
success(`Group ${groupResourceName} deleted`);
|
|
763
|
+
} catch (err) {
|
|
764
|
+
error(String(err));
|
|
765
|
+
process.exit(1);
|
|
766
|
+
}
|
|
767
|
+
});
|
|
768
|
+
|
|
769
|
+
groupsCmd
|
|
770
|
+
.command('members <groupResourceName>')
|
|
771
|
+
.description('List members of a contact group')
|
|
772
|
+
.option('-n, --max <number>', 'Maximum results', '100')
|
|
773
|
+
.option('--page-token <token>', 'Page token for pagination')
|
|
774
|
+
.action(async (groupResourceName: string, opts) => {
|
|
775
|
+
try {
|
|
776
|
+
const client = await getClient();
|
|
777
|
+
const result = await client.groups.getMembers(groupResourceName, {
|
|
778
|
+
pageSize: parseInt(opts.max),
|
|
779
|
+
pageToken: opts.pageToken,
|
|
780
|
+
});
|
|
781
|
+
print(result, getFormat(groupsCmd));
|
|
782
|
+
if (result.nextPageToken) {
|
|
783
|
+
info(`More results available. Use --page-token ${result.nextPageToken}`);
|
|
784
|
+
}
|
|
785
|
+
} catch (err) {
|
|
786
|
+
error(String(err));
|
|
787
|
+
process.exit(1);
|
|
788
|
+
}
|
|
789
|
+
});
|
|
790
|
+
|
|
791
|
+
groupsCmd
|
|
792
|
+
.command('add-member <groupResourceName> <contactResourceName>')
|
|
793
|
+
.description('Add a contact to a group')
|
|
794
|
+
.action(async (groupResourceName: string, contactResourceName: string) => {
|
|
795
|
+
try {
|
|
796
|
+
const client = await getClient();
|
|
797
|
+
await client.groups.addMember(groupResourceName, contactResourceName);
|
|
798
|
+
success(`Contact ${contactResourceName} added to group ${groupResourceName}`);
|
|
799
|
+
} catch (err) {
|
|
800
|
+
error(String(err));
|
|
801
|
+
process.exit(1);
|
|
802
|
+
}
|
|
803
|
+
});
|
|
804
|
+
|
|
805
|
+
groupsCmd
|
|
806
|
+
.command('remove-member <groupResourceName> <contactResourceName>')
|
|
807
|
+
.description('Remove a contact from a group')
|
|
808
|
+
.action(async (groupResourceName: string, contactResourceName: string) => {
|
|
809
|
+
try {
|
|
810
|
+
const client = await getClient();
|
|
811
|
+
await client.groups.removeMember(groupResourceName, contactResourceName);
|
|
812
|
+
success(`Contact ${contactResourceName} removed from group ${groupResourceName}`);
|
|
813
|
+
} catch (err) {
|
|
814
|
+
error(String(err));
|
|
815
|
+
process.exit(1);
|
|
816
|
+
}
|
|
817
|
+
});
|
|
818
|
+
|
|
819
|
+
// ============================================
|
|
820
|
+
// Bulk Commands
|
|
821
|
+
// ============================================
|
|
822
|
+
const bulkCmd = program
|
|
823
|
+
.command('bulk')
|
|
824
|
+
.description('Bulk operations on contacts');
|
|
825
|
+
|
|
826
|
+
function makeProgress(total: number) {
|
|
827
|
+
let last = 0;
|
|
828
|
+
return (current: number) => {
|
|
829
|
+
if (current !== last) {
|
|
830
|
+
process.stdout.write(`\r Progress: ${current}/${total}`);
|
|
831
|
+
last = current;
|
|
832
|
+
}
|
|
833
|
+
};
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
bulkCmd
|
|
837
|
+
.command('preview')
|
|
838
|
+
.description('Preview contacts matching a query')
|
|
839
|
+
.option('-q, --query <query>', 'Search query')
|
|
840
|
+
.option('-n, --max <number>', 'Maximum results', '50')
|
|
841
|
+
.action(async (opts) => {
|
|
842
|
+
try {
|
|
843
|
+
const client = await getClient();
|
|
844
|
+
const bulk = new BulkApi(client.getClient());
|
|
845
|
+
const result = await bulk.preview({ query: opts.query, maxResults: parseInt(opts.max) });
|
|
846
|
+
success(`Found ${result.total} contact(s):`);
|
|
847
|
+
print(result.contacts, getFormat(bulkCmd));
|
|
848
|
+
} catch (err) {
|
|
849
|
+
error(String(err));
|
|
850
|
+
process.exit(1);
|
|
851
|
+
}
|
|
852
|
+
});
|
|
853
|
+
|
|
854
|
+
bulkCmd
|
|
855
|
+
.command('delete')
|
|
856
|
+
.description('Bulk delete contacts matching a query')
|
|
857
|
+
.option('-q, --query <query>', 'Search query')
|
|
858
|
+
.option('-n, --max <number>', 'Maximum results', '100')
|
|
859
|
+
.option('--concurrency <number>', 'Max concurrent operations', '10')
|
|
860
|
+
.option('--dry-run', 'Preview without deleting')
|
|
861
|
+
.option('--confirm', 'Confirm deletion')
|
|
862
|
+
.action(async (opts) => {
|
|
863
|
+
if (!opts.confirm && !opts.dryRun) {
|
|
864
|
+
warn('This will delete contacts. Use --dry-run to preview or --confirm to proceed.');
|
|
865
|
+
process.exit(1);
|
|
866
|
+
}
|
|
867
|
+
try {
|
|
868
|
+
const client = await getClient();
|
|
869
|
+
const bulk = new BulkApi(client.getClient());
|
|
870
|
+
const progress = makeProgress(0);
|
|
871
|
+
const result = await bulk.delete({
|
|
872
|
+
query: opts.query,
|
|
873
|
+
maxResults: parseInt(opts.max),
|
|
874
|
+
concurrency: parseInt(opts.concurrency),
|
|
875
|
+
dryRun: opts.dryRun || false,
|
|
876
|
+
onProgress: (cur, total) => { progress(total); progress(cur); },
|
|
877
|
+
onError: (err, contact) => { warn(`Failed: ${contact.displayName} - ${err.message}`); },
|
|
878
|
+
});
|
|
879
|
+
process.stdout.write('\n');
|
|
880
|
+
if (opts.dryRun) {
|
|
881
|
+
info(`Dry run: would delete ${result.success} contact(s)`);
|
|
882
|
+
} else {
|
|
883
|
+
success(`Deleted ${result.success} contact(s), ${result.failed} failed`);
|
|
884
|
+
}
|
|
885
|
+
if (result.errors.length > 0) {
|
|
886
|
+
print(result.errors, 'pretty');
|
|
887
|
+
}
|
|
888
|
+
} catch (err) {
|
|
889
|
+
error(String(err));
|
|
890
|
+
process.exit(1);
|
|
891
|
+
}
|
|
892
|
+
});
|
|
893
|
+
|
|
894
|
+
bulkCmd
|
|
895
|
+
.command('update')
|
|
896
|
+
.description('Bulk update contacts matching a query')
|
|
897
|
+
.option('-q, --query <query>', 'Search query')
|
|
898
|
+
.option('-n, --max <number>', 'Maximum results', '100')
|
|
899
|
+
.option('--concurrency <number>', 'Max concurrent operations', '10')
|
|
900
|
+
.option('--dry-run', 'Preview without updating')
|
|
901
|
+
.option('--given-name <name>', 'New given name')
|
|
902
|
+
.option('--family-name <name>', 'New family name')
|
|
903
|
+
.option('--organization <org>', 'New organization')
|
|
904
|
+
.option('--title <title>', 'New job title')
|
|
905
|
+
.action(async (opts) => {
|
|
906
|
+
if (!opts.givenName && !opts.familyName && !opts.organization && !opts.title) {
|
|
907
|
+
error('At least one update field is required (--given-name, --family-name, --organization, --title)');
|
|
908
|
+
process.exit(1);
|
|
909
|
+
}
|
|
910
|
+
try {
|
|
911
|
+
const client = await getClient();
|
|
912
|
+
const bulk = new BulkApi(client.getClient());
|
|
913
|
+
const result = await bulk.update({
|
|
914
|
+
query: opts.query,
|
|
915
|
+
maxResults: parseInt(opts.max),
|
|
916
|
+
concurrency: parseInt(opts.concurrency),
|
|
917
|
+
dryRun: opts.dryRun || false,
|
|
918
|
+
updates: {
|
|
919
|
+
givenName: opts.givenName,
|
|
920
|
+
familyName: opts.familyName,
|
|
921
|
+
organization: opts.organization || opts.title ? {
|
|
922
|
+
name: opts.organization,
|
|
923
|
+
title: opts.title,
|
|
924
|
+
} : undefined,
|
|
925
|
+
},
|
|
926
|
+
onProgress: (cur, total) => { process.stdout.write(`\r Progress: ${cur}/${total}`); },
|
|
927
|
+
onError: (err, contact) => { warn(`Failed: ${contact.displayName} - ${err.message}`); },
|
|
928
|
+
});
|
|
929
|
+
process.stdout.write('\n');
|
|
930
|
+
if (opts.dryRun) {
|
|
931
|
+
info(`Dry run: would update ${result.success} contact(s)`);
|
|
932
|
+
} else {
|
|
933
|
+
success(`Updated ${result.success} contact(s), ${result.failed} failed`);
|
|
934
|
+
}
|
|
935
|
+
if (result.errors.length > 0) {
|
|
936
|
+
print(result.errors, 'pretty');
|
|
937
|
+
}
|
|
938
|
+
} catch (err) {
|
|
939
|
+
error(String(err));
|
|
940
|
+
process.exit(1);
|
|
941
|
+
}
|
|
942
|
+
});
|
|
943
|
+
|
|
944
|
+
bulkCmd
|
|
945
|
+
.command('add-to-group <groupResourceName>')
|
|
946
|
+
.description('Bulk add contacts to a group')
|
|
947
|
+
.option('-q, --query <query>', 'Search query')
|
|
948
|
+
.option('--contact-names <names>', 'Comma-separated contact resource names')
|
|
949
|
+
.option('-n, --max <number>', 'Maximum results', '100')
|
|
950
|
+
.option('--concurrency <number>', 'Max concurrent operations', '10')
|
|
951
|
+
.option('--dry-run', 'Preview without adding')
|
|
952
|
+
.action(async (groupResourceName: string, opts) => {
|
|
953
|
+
try {
|
|
954
|
+
const client = await getClient();
|
|
955
|
+
const bulk = new BulkApi(client.getClient());
|
|
956
|
+
const contactNames = opts.contactNames ? opts.contactNames.split(',') : undefined;
|
|
957
|
+
const result = await bulk.addToGroup(groupResourceName, {
|
|
958
|
+
contactNames,
|
|
959
|
+
query: opts.query,
|
|
960
|
+
maxResults: parseInt(opts.max),
|
|
961
|
+
concurrency: parseInt(opts.concurrency),
|
|
962
|
+
dryRun: opts.dryRun || false,
|
|
963
|
+
onProgress: (cur, total) => { process.stdout.write(`\r Progress: ${cur}/${total}`); },
|
|
964
|
+
onError: (err, contact) => { warn(`Failed: ${contact.displayName} - ${err.message}`); },
|
|
965
|
+
});
|
|
966
|
+
process.stdout.write('\n');
|
|
967
|
+
if (opts.dryRun) {
|
|
968
|
+
info(`Dry run: would add ${result.success} contact(s) to group`);
|
|
969
|
+
} else {
|
|
970
|
+
success(`Added ${result.success} contact(s) to group, ${result.failed} failed`);
|
|
971
|
+
}
|
|
972
|
+
if (result.errors.length > 0) {
|
|
973
|
+
print(result.errors, 'pretty');
|
|
974
|
+
}
|
|
975
|
+
} catch (err) {
|
|
976
|
+
error(String(err));
|
|
977
|
+
process.exit(1);
|
|
978
|
+
}
|
|
979
|
+
});
|
|
980
|
+
|
|
981
|
+
bulkCmd
|
|
982
|
+
.command('remove-from-group <groupResourceName>')
|
|
983
|
+
.description('Bulk remove contacts from a group')
|
|
984
|
+
.option('-q, --query <query>', 'Search query')
|
|
985
|
+
.option('--contact-names <names>', 'Comma-separated contact resource names')
|
|
986
|
+
.option('-n, --max <number>', 'Maximum results', '100')
|
|
987
|
+
.option('--concurrency <number>', 'Max concurrent operations', '10')
|
|
988
|
+
.option('--dry-run', 'Preview without removing')
|
|
989
|
+
.action(async (groupResourceName: string, opts) => {
|
|
990
|
+
try {
|
|
991
|
+
const client = await getClient();
|
|
992
|
+
const bulk = new BulkApi(client.getClient());
|
|
993
|
+
const contactNames = opts.contactNames ? opts.contactNames.split(',') : undefined;
|
|
994
|
+
const result = await bulk.removeFromGroup(groupResourceName, {
|
|
995
|
+
contactNames,
|
|
996
|
+
query: opts.query,
|
|
997
|
+
maxResults: parseInt(opts.max),
|
|
998
|
+
concurrency: parseInt(opts.concurrency),
|
|
999
|
+
dryRun: opts.dryRun || false,
|
|
1000
|
+
onProgress: (cur, total) => { process.stdout.write(`\r Progress: ${cur}/${total}`); },
|
|
1001
|
+
onError: (err, contact) => { warn(`Failed: ${contact.displayName} - ${err.message}`); },
|
|
1002
|
+
});
|
|
1003
|
+
process.stdout.write('\n');
|
|
1004
|
+
if (opts.dryRun) {
|
|
1005
|
+
info(`Dry run: would remove ${result.success} contact(s) from group`);
|
|
1006
|
+
} else {
|
|
1007
|
+
success(`Removed ${result.success} contact(s) from group, ${result.failed} failed`);
|
|
1008
|
+
}
|
|
1009
|
+
if (result.errors.length > 0) {
|
|
1010
|
+
print(result.errors, 'pretty');
|
|
1011
|
+
}
|
|
1012
|
+
} catch (err) {
|
|
1013
|
+
error(String(err));
|
|
1014
|
+
process.exit(1);
|
|
1015
|
+
}
|
|
1016
|
+
});
|
|
1017
|
+
|
|
678
1018
|
// Parse and execute
|
|
679
1019
|
program.parse();
|
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
import type { GoogleDocsClient } from './client';
|
|
2
|
+
import type { Document, Request, BatchUpdateResponse } from '../types';
|
|
3
|
+
|
|
4
|
+
// ============================================
|
|
5
|
+
// Bulk Operation Types
|
|
6
|
+
// ============================================
|
|
7
|
+
|
|
8
|
+
export interface DocumentSummary {
|
|
9
|
+
documentId: string;
|
|
10
|
+
title: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface BulkOperationOptions {
|
|
14
|
+
/** Maximum concurrent API calls (default: 10) */
|
|
15
|
+
concurrency?: number;
|
|
16
|
+
/** Dry run - don't actually modify */
|
|
17
|
+
dryRun?: boolean;
|
|
18
|
+
/** Progress callback */
|
|
19
|
+
onProgress?: (current: number, total: number, item: unknown) => void;
|
|
20
|
+
/** Error callback */
|
|
21
|
+
onError?: (error: Error, item: unknown) => void;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface BulkCreateDocumentsOptions extends BulkOperationOptions {
|
|
25
|
+
/** Document titles to create */
|
|
26
|
+
titles: string[];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface BulkCreateDocumentsResult {
|
|
30
|
+
total: number;
|
|
31
|
+
success: number;
|
|
32
|
+
failed: number;
|
|
33
|
+
errors: Array<{ title: string; error: string }>;
|
|
34
|
+
createdDocuments: DocumentSummary[];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface FindReplaceEntry {
|
|
38
|
+
documentId: string;
|
|
39
|
+
find: string;
|
|
40
|
+
replace: string;
|
|
41
|
+
matchCase?: boolean;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface BulkFindReplaceOptions extends BulkOperationOptions {
|
|
45
|
+
/** Find/replace entries per document */
|
|
46
|
+
entries: FindReplaceEntry[];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface FindReplaceResult {
|
|
50
|
+
documentId: string;
|
|
51
|
+
occurrencesChanged: number;
|
|
52
|
+
find: string;
|
|
53
|
+
replace: string;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface BulkFindReplaceResult {
|
|
57
|
+
total: number;
|
|
58
|
+
success: number;
|
|
59
|
+
failed: number;
|
|
60
|
+
errors: Array<{ documentId: string; find: string; error: string }>;
|
|
61
|
+
results: FindReplaceResult[];
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface BulkOperationEntry {
|
|
65
|
+
documentId: string;
|
|
66
|
+
requests: Request[];
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export interface BulkBatchUpdateOptions extends BulkOperationOptions {
|
|
70
|
+
/** Batch update entries */
|
|
71
|
+
entries: BulkOperationEntry[];
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export interface BulkBatchUpdateResult {
|
|
75
|
+
total: number;
|
|
76
|
+
success: number;
|
|
77
|
+
failed: number;
|
|
78
|
+
errors: Array<{ documentId: string; error: string }>;
|
|
79
|
+
results: Array<{ documentId: string; response: BatchUpdateResponse }>;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// ============================================
|
|
83
|
+
// Bulk Operations API
|
|
84
|
+
// ============================================
|
|
85
|
+
|
|
86
|
+
export class BulkApi {
|
|
87
|
+
private readonly client: GoogleDocsClient;
|
|
88
|
+
|
|
89
|
+
constructor(client: GoogleDocsClient) {
|
|
90
|
+
this.client = client;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// ============================================
|
|
94
|
+
// Create Documents
|
|
95
|
+
// ============================================
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Bulk create documents from a list of titles
|
|
99
|
+
*/
|
|
100
|
+
async createDocuments(
|
|
101
|
+
options: BulkCreateDocumentsOptions
|
|
102
|
+
): Promise<BulkCreateDocumentsResult> {
|
|
103
|
+
const { titles, concurrency = 10, dryRun = false, onProgress, onError } = options;
|
|
104
|
+
|
|
105
|
+
const result: BulkCreateDocumentsResult = {
|
|
106
|
+
total: titles.length,
|
|
107
|
+
success: 0,
|
|
108
|
+
failed: 0,
|
|
109
|
+
errors: [],
|
|
110
|
+
createdDocuments: [],
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
if (titles.length === 0) return result;
|
|
114
|
+
|
|
115
|
+
const chunks = this.chunkArray(titles, concurrency);
|
|
116
|
+
|
|
117
|
+
for (const chunk of chunks) {
|
|
118
|
+
await Promise.all(
|
|
119
|
+
chunk.map(async (title) => {
|
|
120
|
+
try {
|
|
121
|
+
if (dryRun) {
|
|
122
|
+
result.success++;
|
|
123
|
+
} else {
|
|
124
|
+
const doc = await this.client.post<Document>('/documents', { title });
|
|
125
|
+
result.success++;
|
|
126
|
+
result.createdDocuments.push({
|
|
127
|
+
documentId: doc.documentId,
|
|
128
|
+
title: doc.title,
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (onProgress) onProgress(result.success + result.failed, result.total, title);
|
|
133
|
+
} catch (err) {
|
|
134
|
+
result.failed++;
|
|
135
|
+
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
136
|
+
result.errors.push({ title, error: errorMessage });
|
|
137
|
+
if (onError) onError(err instanceof Error ? err : new Error(errorMessage), title);
|
|
138
|
+
}
|
|
139
|
+
})
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return result;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// ============================================
|
|
147
|
+
// Find/Replace
|
|
148
|
+
// ============================================
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Bulk find and replace across multiple documents
|
|
152
|
+
*/
|
|
153
|
+
async findReplace(
|
|
154
|
+
options: BulkFindReplaceOptions
|
|
155
|
+
): Promise<BulkFindReplaceResult> {
|
|
156
|
+
const { entries, concurrency = 10, dryRun = false, onProgress, onError } = options;
|
|
157
|
+
|
|
158
|
+
const result: BulkFindReplaceResult = {
|
|
159
|
+
total: entries.length,
|
|
160
|
+
success: 0,
|
|
161
|
+
failed: 0,
|
|
162
|
+
errors: [],
|
|
163
|
+
results: [],
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
if (entries.length === 0) return result;
|
|
167
|
+
|
|
168
|
+
const chunks = this.chunkArray(entries, concurrency);
|
|
169
|
+
|
|
170
|
+
for (const chunk of chunks) {
|
|
171
|
+
await Promise.all(
|
|
172
|
+
chunk.map(async (entry) => {
|
|
173
|
+
try {
|
|
174
|
+
if (dryRun) {
|
|
175
|
+
result.success++;
|
|
176
|
+
result.results.push({
|
|
177
|
+
documentId: entry.documentId,
|
|
178
|
+
occurrencesChanged: 0,
|
|
179
|
+
find: entry.find,
|
|
180
|
+
replace: entry.replace,
|
|
181
|
+
});
|
|
182
|
+
} else {
|
|
183
|
+
const requests: Request[] = [
|
|
184
|
+
{
|
|
185
|
+
replaceAllText: {
|
|
186
|
+
containsText: {
|
|
187
|
+
text: entry.find,
|
|
188
|
+
matchCase: entry.matchCase || false,
|
|
189
|
+
},
|
|
190
|
+
replaceText: entry.replace,
|
|
191
|
+
},
|
|
192
|
+
},
|
|
193
|
+
];
|
|
194
|
+
|
|
195
|
+
const response = await this.client.post<BatchUpdateResponse>(
|
|
196
|
+
`/documents/${entry.documentId}:batchUpdate`,
|
|
197
|
+
{ requests }
|
|
198
|
+
);
|
|
199
|
+
|
|
200
|
+
const occurrencesChanged =
|
|
201
|
+
response.replies?.find((r) => r.replaceAllText)?.replaceAllText?.occurrencesChanged || 0;
|
|
202
|
+
|
|
203
|
+
result.success++;
|
|
204
|
+
result.results.push({
|
|
205
|
+
documentId: entry.documentId,
|
|
206
|
+
occurrencesChanged,
|
|
207
|
+
find: entry.find,
|
|
208
|
+
replace: entry.replace,
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (onProgress) onProgress(result.success + result.failed, result.total, entry);
|
|
213
|
+
} catch (err) {
|
|
214
|
+
result.failed++;
|
|
215
|
+
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
216
|
+
result.errors.push({
|
|
217
|
+
documentId: entry.documentId,
|
|
218
|
+
find: entry.find,
|
|
219
|
+
error: errorMessage,
|
|
220
|
+
});
|
|
221
|
+
if (onError) onError(err instanceof Error ? err : new Error(errorMessage), entry);
|
|
222
|
+
}
|
|
223
|
+
})
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return result;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// ============================================
|
|
231
|
+
// Batch Update (generic)
|
|
232
|
+
// ============================================
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Bulk batch updates across multiple documents
|
|
236
|
+
* Each entry specifies a document ID and an array of requests
|
|
237
|
+
*/
|
|
238
|
+
async batchUpdate(
|
|
239
|
+
options: BulkBatchUpdateOptions
|
|
240
|
+
): Promise<BulkBatchUpdateResult> {
|
|
241
|
+
const { entries, concurrency = 10, dryRun = false, onProgress, onError } = options;
|
|
242
|
+
|
|
243
|
+
const result: BulkBatchUpdateResult = {
|
|
244
|
+
total: entries.length,
|
|
245
|
+
success: 0,
|
|
246
|
+
failed: 0,
|
|
247
|
+
errors: [],
|
|
248
|
+
results: [],
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
if (entries.length === 0) return result;
|
|
252
|
+
|
|
253
|
+
const chunks = this.chunkArray(entries, concurrency);
|
|
254
|
+
|
|
255
|
+
for (const chunk of chunks) {
|
|
256
|
+
await Promise.all(
|
|
257
|
+
chunk.map(async (entry) => {
|
|
258
|
+
try {
|
|
259
|
+
if (dryRun) {
|
|
260
|
+
result.success++;
|
|
261
|
+
} else {
|
|
262
|
+
const response = await this.client.post<BatchUpdateResponse>(
|
|
263
|
+
`/documents/${entry.documentId}:batchUpdate`,
|
|
264
|
+
{ requests: entry.requests }
|
|
265
|
+
);
|
|
266
|
+
result.success++;
|
|
267
|
+
result.results.push({
|
|
268
|
+
documentId: entry.documentId,
|
|
269
|
+
response,
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
if (onProgress) onProgress(result.success + result.failed, result.total, entry);
|
|
274
|
+
} catch (err) {
|
|
275
|
+
result.failed++;
|
|
276
|
+
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
277
|
+
result.errors.push({
|
|
278
|
+
documentId: entry.documentId,
|
|
279
|
+
error: errorMessage,
|
|
280
|
+
});
|
|
281
|
+
if (onError) onError(err instanceof Error ? err : new Error(errorMessage), entry);
|
|
282
|
+
}
|
|
283
|
+
})
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
return result;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// ============================================
|
|
291
|
+
// Helper Methods
|
|
292
|
+
// ============================================
|
|
293
|
+
|
|
294
|
+
private chunkArray<T>(array: T[], size: number): T[][] {
|
|
295
|
+
const chunks: T[][] = [];
|
|
296
|
+
for (let i = 0; i < array.length; i += size) {
|
|
297
|
+
chunks.push(array.slice(i, i + size));
|
|
298
|
+
}
|
|
299
|
+
return chunks;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
@@ -3,6 +3,7 @@ import { GoogleDocsClient } from './client';
|
|
|
3
3
|
import { DocumentsApi } from './documents';
|
|
4
4
|
import { ContentApi } from './content';
|
|
5
5
|
import { StylesApi } from './styles';
|
|
6
|
+
import { BulkApi } from './bulk';
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* Google Docs API client
|
|
@@ -15,12 +16,14 @@ export class GoogleDocs {
|
|
|
15
16
|
public readonly documents: DocumentsApi;
|
|
16
17
|
public readonly content: ContentApi;
|
|
17
18
|
public readonly styles: StylesApi;
|
|
19
|
+
public readonly bulk: BulkApi;
|
|
18
20
|
|
|
19
21
|
constructor(config: GoogleDocsConfig) {
|
|
20
22
|
this.client = new GoogleDocsClient(config);
|
|
21
23
|
this.documents = new DocumentsApi(this.client);
|
|
22
24
|
this.content = new ContentApi(this.client);
|
|
23
25
|
this.styles = new StylesApi(this.client);
|
|
26
|
+
this.bulk = new BulkApi(this.client);
|
|
24
27
|
}
|
|
25
28
|
|
|
26
29
|
/**
|
|
@@ -63,3 +66,4 @@ export { GoogleDocsClient } from './client';
|
|
|
63
66
|
export { DocumentsApi } from './documents';
|
|
64
67
|
export { ContentApi } from './content';
|
|
65
68
|
export { StylesApi } from './styles';
|
|
69
|
+
export { BulkApi } from './bulk';
|