@elevasis/core 0.7.1 → 0.8.2
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/dist/test-utils/index.d.ts +3122 -0
- package/dist/test-utils/index.js +386 -0
- package/package.json +6 -1
- package/src/README.md +39 -36
- package/src/__tests__/publish.test.ts +18 -13
- package/src/__tests__/{template-foundations-compatibility.test.ts → template-core-compatibility.test.ts} +99 -99
- package/src/_gen/__tests__/__snapshots__/contracts.md.snap +1135 -1131
- package/src/_gen/__tests__/scaffold-contracts.test.ts +47 -36
- package/src/_gen/scaffold-contracts.ts +45 -45
- package/src/auth/multi-tenancy/credentials/README.md +38 -38
- package/src/auth/multi-tenancy/credentials/index.ts +6 -6
- package/src/auth/multi-tenancy/credentials/server/encryption.ts +39 -39
- package/src/auth/multi-tenancy/credentials/server/service.ts +60 -60
- package/src/auth/multi-tenancy/index.ts +17 -17
- package/src/auth/multi-tenancy/invitations/api-schemas.ts +107 -107
- package/src/auth/multi-tenancy/invitations/index.ts +37 -37
- package/src/auth/multi-tenancy/invitations/invitation.ts +86 -86
- package/src/auth/multi-tenancy/invitations/server/index.ts +25 -25
- package/src/auth/multi-tenancy/invitations/server/transforms.ts +24 -24
- package/src/auth/multi-tenancy/invitations/server/workos.ts +24 -24
- package/src/auth/multi-tenancy/invitations/supabase.ts +50 -50
- package/src/auth/multi-tenancy/memberships/api-schemas.ts +126 -126
- package/src/auth/multi-tenancy/memberships/index.ts +21 -21
- package/src/auth/multi-tenancy/memberships/membership.ts +138 -138
- package/src/auth/multi-tenancy/memberships/server/index.ts +15 -15
- package/src/auth/multi-tenancy/memberships/server/transforms.ts +32 -32
- package/src/auth/multi-tenancy/memberships/server/workos.ts +21 -21
- package/src/auth/multi-tenancy/memberships/supabase.ts +46 -46
- package/src/auth/multi-tenancy/organizations/api-schemas.ts +128 -128
- package/src/auth/multi-tenancy/organizations/index.ts +23 -23
- package/src/auth/multi-tenancy/organizations/organization.ts +24 -24
- package/src/auth/multi-tenancy/organizations/server/index.ts +10 -10
- package/src/auth/multi-tenancy/organizations/server/transforms.ts +35 -35
- package/src/auth/multi-tenancy/organizations/server/workos.ts +20 -20
- package/src/auth/multi-tenancy/types.ts +83 -83
- package/src/auth/multi-tenancy/users/api-schemas.ts +194 -194
- package/src/auth/multi-tenancy/users/index.ts +27 -27
- package/src/auth/multi-tenancy/users/server/index.ts +19 -19
- package/src/auth/multi-tenancy/users/server/transforms.ts +21 -21
- package/src/auth/multi-tenancy/users/server/workos.ts +16 -16
- package/src/auth/multi-tenancy/users/user.ts +65 -65
- package/src/business/README.md +52 -52
- package/src/business/__tests__/entities-published.test.ts +33 -33
- package/src/business/acquisition/api-schemas.ts +759 -759
- package/src/business/acquisition/index.ts +109 -109
- package/src/business/acquisition/types.ts +402 -402
- package/src/business/base-entities.test.ts +481 -481
- package/src/business/base-entities.ts +241 -241
- package/src/business/entities-published.ts +24 -24
- package/src/business/index.ts +15 -15
- package/src/business/pdf/browser/pdfmake-browser.ts +229 -229
- package/src/business/pdf/index.ts +10 -10
- package/src/business/pdf/server/index.ts +21 -21
- package/src/business/pdf/server/themes/default.ts +8 -8
- package/src/business/pdf/server/themes/index.ts +9 -9
- package/src/business/pdf/server/themes/types.ts +8 -8
- package/src/business/pdf/types.ts +272 -272
- package/src/business/projects/index.ts +2 -2
- package/src/business/projects/sse-events.ts +21 -21
- package/src/business/projects/types.ts +89 -89
- package/src/business/sales/api-schemas.ts +75 -75
- package/src/business/seo/__tests__/linking.test.ts +549 -549
- package/src/business/seo/__tests__/types.test.ts +404 -404
- package/src/business/seo/index.ts +2 -2
- package/src/business/seo/linking.ts +281 -281
- package/src/business/seo/types.ts +199 -199
- package/src/commands/queue/index.ts +3 -3
- package/src/commands/queue/schemas.test.ts +593 -593
- package/src/commands/queue/schemas.ts +125 -125
- package/src/commands/queue/sse-events.ts +61 -61
- package/src/commands/queue/types/action.ts +52 -52
- package/src/commands/queue/types/checkpoint.ts +44 -44
- package/src/commands/queue/types/index.ts +7 -7
- package/src/commands/queue/types/task.ts +116 -116
- package/src/commands/queue/types.ts +14 -14
- package/src/content/distribution-metadata.ts +61 -61
- package/src/content/index.ts +10 -10
- package/src/deployments/index.ts +22 -22
- package/src/execution/core/__tests__/archived-logs.test.ts +72 -72
- package/src/execution/core/index.ts +11 -11
- package/src/execution/core/runner-types.ts +80 -80
- package/src/execution/core/server/environment.ts +31 -31
- package/src/execution/core/sse-executions.ts +119 -119
- package/src/execution/core/types.ts +29 -29
- package/src/execution/engine/__tests__/fixtures/test-agents.ts +4 -4
- package/src/execution/engine/__tests__/timeout.test.ts +565 -565
- package/src/execution/engine/agent/__tests__/errors.test.ts +508 -508
- package/src/execution/engine/agent/actions/__tests__/processor.test.ts +531 -531
- package/src/execution/engine/agent/actions/executor.ts +205 -205
- package/src/execution/engine/agent/actions/navigate-knowledge-executor.ts +230 -230
- package/src/execution/engine/agent/actions/processor.ts +116 -116
- package/src/execution/engine/agent/actions/types.ts +70 -70
- package/src/execution/engine/agent/core/agent.ts +810 -810
- package/src/execution/engine/agent/core/types.ts +155 -155
- package/src/execution/engine/agent/errors.ts +251 -251
- package/src/execution/engine/agent/index.ts +78 -78
- package/src/execution/engine/agent/knowledge-map/types.ts +106 -106
- package/src/execution/engine/agent/knowledge-map/utils.ts +101 -101
- package/src/execution/engine/agent/memory/__tests__/manager.test.ts +754 -754
- package/src/execution/engine/agent/memory/domains.ts +99 -99
- package/src/execution/engine/agent/memory/manager.ts +365 -365
- package/src/execution/engine/agent/memory/processor.ts +66 -66
- package/src/execution/engine/agent/memory/types.ts +90 -90
- package/src/execution/engine/agent/memory/utils.ts +134 -134
- package/src/execution/engine/agent/observability/logging.ts +467 -467
- package/src/execution/engine/agent/observability/types.ts +64 -64
- package/src/execution/engine/agent/reasoning/adapters/agent-adapter-helpers.ts +349 -349
- package/src/execution/engine/agent/reasoning/processor.ts +92 -92
- package/src/execution/engine/agent/reasoning/prompt-sections/base-actions.ts +134 -134
- package/src/execution/engine/agent/reasoning/prompt-sections/completion.ts +49 -49
- package/src/execution/engine/agent/reasoning/prompt-sections/knowledge-map.ts +93 -93
- package/src/execution/engine/agent/reasoning/prompt-sections/memory.ts +65 -65
- package/src/execution/engine/agent/reasoning/prompt-sections/tools.ts +44 -44
- package/src/execution/engine/agent/reasoning/request-builder.ts +169 -169
- package/src/execution/engine/agent/reasoning/types.ts +18 -18
- package/src/execution/engine/base/errors.ts +118 -118
- package/src/execution/engine/base/index.ts +2 -2
- package/src/execution/engine/base/logging.ts +31 -31
- package/src/execution/engine/base/serialization.ts +324 -324
- package/src/execution/engine/base/types.ts +126 -126
- package/src/execution/engine/base/utils.ts +41 -41
- package/src/execution/engine/index.ts +434 -434
- package/src/execution/engine/interface/index.ts +1 -1
- package/src/execution/engine/interface/types.ts +62 -62
- package/src/execution/engine/llm/__tests__/model-info.test.ts +50 -50
- package/src/execution/engine/llm/__tests__/model-validation.test.ts +321 -321
- package/src/execution/engine/llm/__tests__/response-schema-validator.test.ts +115 -115
- package/src/execution/engine/llm/adapters/__tests__/adapter-factory.test.ts +375 -375
- package/src/execution/engine/llm/adapters/__tests__/anthropic-adapter.test.ts +463 -463
- package/src/execution/engine/llm/adapters/__tests__/anthropic.integration.test.ts +177 -177
- package/src/execution/engine/llm/adapters/__tests__/google-adapter.test.ts +722 -722
- package/src/execution/engine/llm/adapters/__tests__/google.integration.test.ts +376 -376
- package/src/execution/engine/llm/adapters/__tests__/openai-adapter.test.ts +551 -551
- package/src/execution/engine/llm/adapters/__tests__/openrouter-adapter.test.ts +563 -563
- package/src/execution/engine/llm/adapters/__tests__/openrouter.integration.test.ts +105 -105
- package/src/execution/engine/llm/adapters/__tests__/universal-adapter.test.ts +537 -537
- package/src/execution/engine/llm/adapters/circuit-breaker.ts +147 -147
- package/src/execution/engine/llm/adapters/index.ts +17 -17
- package/src/execution/engine/llm/adapters/mock-adapter.ts +116 -116
- package/src/execution/engine/llm/adapters/server/adapter-factory.ts +130 -130
- package/src/execution/engine/llm/adapters/server/anthropic.ts +137 -137
- package/src/execution/engine/llm/adapters/server/google.ts +283 -283
- package/src/execution/engine/llm/adapters/server/index.ts +12 -12
- package/src/execution/engine/llm/adapters/server/openai.ts +206 -206
- package/src/execution/engine/llm/adapters/server/openrouter.ts +235 -235
- package/src/execution/engine/llm/adapters/universal-adapter.ts +230 -230
- package/src/execution/engine/llm/errors.ts +186 -186
- package/src/execution/engine/llm/model-info.ts +332 -332
- package/src/execution/engine/llm/response-schema-validator.ts +113 -113
- package/src/execution/engine/llm/types.ts +86 -86
- package/src/execution/engine/test-utils/index.ts +6 -6
- package/src/execution/engine/test-utils/mocks.ts +56 -56
- package/src/execution/engine/tools/integration/base-integration-adapter.ts +50 -50
- package/src/execution/engine/tools/integration/index.ts +53 -53
- package/src/execution/engine/tools/integration/server/adapters/anymailfinder/anymailfinder-adapter.ts +73 -73
- package/src/execution/engine/tools/integration/server/adapters/anymailfinder/anymailfinder-tools.ts +209 -209
- package/src/execution/engine/tools/integration/server/adapters/anymailfinder/fetch/find-company-email/index.ts +82 -82
- package/src/execution/engine/tools/integration/server/adapters/anymailfinder/fetch/find-decision-maker-email/index.ts +122 -122
- package/src/execution/engine/tools/integration/server/adapters/anymailfinder/fetch/find-person-email/index.ts +89 -89
- package/src/execution/engine/tools/integration/server/adapters/anymailfinder/fetch/verify-email/index.ts +84 -84
- package/src/execution/engine/tools/integration/server/adapters/anymailfinder/index.ts +16 -16
- package/src/execution/engine/tools/integration/server/adapters/apify/__tests__/apify-run-actor.integration.test.ts +293 -293
- package/src/execution/engine/tools/integration/server/adapters/apify/apify-adapter.ts +100 -100
- package/src/execution/engine/tools/integration/server/adapters/apify/apify-tools.ts +217 -217
- package/src/execution/engine/tools/integration/server/adapters/apify/fetch/get-dataset-items/index.ts +92 -92
- package/src/execution/engine/tools/integration/server/adapters/apify/fetch/run-actor/index.ts +218 -218
- package/src/execution/engine/tools/integration/server/adapters/apify/fetch/start-actor/index.ts +87 -87
- package/src/execution/engine/tools/integration/server/adapters/apify/index.ts +11 -11
- package/src/execution/engine/tools/integration/server/adapters/attio/__tests__/attio-crud.integration.test.ts +361 -361
- package/src/execution/engine/tools/integration/server/adapters/attio/attio-adapter.ts +162 -162
- package/src/execution/engine/tools/integration/server/adapters/attio/attio-tools.ts +594 -594
- package/src/execution/engine/tools/integration/server/adapters/attio/fetch/create-attribute/index.ts +214 -214
- package/src/execution/engine/tools/integration/server/adapters/attio/fetch/create-note/index.ts +152 -152
- package/src/execution/engine/tools/integration/server/adapters/attio/fetch/create-record/index.ts +141 -141
- package/src/execution/engine/tools/integration/server/adapters/attio/fetch/delete-note/index.ts +86 -86
- package/src/execution/engine/tools/integration/server/adapters/attio/fetch/delete-record/index.ts +105 -105
- package/src/execution/engine/tools/integration/server/adapters/attio/fetch/get-record/index.ts +118 -118
- package/src/execution/engine/tools/integration/server/adapters/attio/fetch/list-attributes/index.ts +165 -165
- package/src/execution/engine/tools/integration/server/adapters/attio/fetch/list-notes/index.ts +96 -96
- package/src/execution/engine/tools/integration/server/adapters/attio/fetch/list-objects/index.ts +104 -104
- package/src/execution/engine/tools/integration/server/adapters/attio/fetch/list-records/index.ts +156 -156
- package/src/execution/engine/tools/integration/server/adapters/attio/fetch/update-attribute/index.ts +220 -220
- package/src/execution/engine/tools/integration/server/adapters/attio/fetch/update-record/index.ts +140 -140
- package/src/execution/engine/tools/integration/server/adapters/attio/fetch/utils/types.ts +146 -146
- package/src/execution/engine/tools/integration/server/adapters/attio/index.ts +31 -31
- package/src/execution/engine/tools/integration/server/adapters/gmail/gmail-adapter.ts +210 -210
- package/src/execution/engine/tools/integration/server/adapters/gmail/gmail-tools.ts +104 -104
- package/src/execution/engine/tools/integration/server/adapters/google-sheets/__tests__/google-sheets.integration.test.ts +261 -261
- package/src/execution/engine/tools/integration/server/adapters/google-sheets/google-sheets-adapter.ts +1189 -1189
- package/src/execution/engine/tools/integration/server/adapters/google-sheets/google-sheets-tools.ts +641 -641
- package/src/execution/engine/tools/integration/server/adapters/google-sheets/index.ts +18 -18
- package/src/execution/engine/tools/integration/server/adapters/instantly/fetch/activate-campaign/index.ts +86 -86
- package/src/execution/engine/tools/integration/server/adapters/instantly/fetch/add-to-campaign/__tests__/index.test.ts +289 -289
- package/src/execution/engine/tools/integration/server/adapters/instantly/fetch/add-to-campaign/index.ts +154 -154
- package/src/execution/engine/tools/integration/server/adapters/instantly/fetch/bulk-add-leads/__tests__/index.test.ts +325 -325
- package/src/execution/engine/tools/integration/server/adapters/instantly/fetch/bulk-add-leads/index.ts +153 -153
- package/src/execution/engine/tools/integration/server/adapters/instantly/fetch/bulk-delete-leads/index.ts +84 -84
- package/src/execution/engine/tools/integration/server/adapters/instantly/fetch/create-campaign/index.ts +125 -125
- package/src/execution/engine/tools/integration/server/adapters/instantly/fetch/create-inbox-test/index.ts +107 -107
- package/src/execution/engine/tools/integration/server/adapters/instantly/fetch/delete-campaign/index.ts +85 -85
- package/src/execution/engine/tools/integration/server/adapters/instantly/fetch/get-account-health/index.ts +91 -91
- package/src/execution/engine/tools/integration/server/adapters/instantly/fetch/get-campaign/index.ts +92 -92
- package/src/execution/engine/tools/integration/server/adapters/instantly/fetch/get-campaign-analytics/__tests__/index.test.ts +195 -195
- package/src/execution/engine/tools/integration/server/adapters/instantly/fetch/get-campaign-analytics/index.ts +113 -113
- package/src/execution/engine/tools/integration/server/adapters/instantly/fetch/get-daily-campaign-analytics/index.ts +104 -104
- package/src/execution/engine/tools/integration/server/adapters/instantly/fetch/get-emails/index.ts +155 -155
- package/src/execution/engine/tools/integration/server/adapters/instantly/fetch/get-step-analytics/__tests__/index.test.ts +196 -196
- package/src/execution/engine/tools/integration/server/adapters/instantly/fetch/get-step-analytics/index.ts +102 -102
- package/src/execution/engine/tools/integration/server/adapters/instantly/fetch/list-campaigns/__tests__/index.test.ts +189 -189
- package/src/execution/engine/tools/integration/server/adapters/instantly/fetch/list-campaigns/index.ts +87 -87
- package/src/execution/engine/tools/integration/server/adapters/instantly/fetch/list-leads/index.ts +112 -112
- package/src/execution/engine/tools/integration/server/adapters/instantly/fetch/patch-lead/index.ts +76 -76
- package/src/execution/engine/tools/integration/server/adapters/instantly/fetch/pause-campaign/index.ts +86 -86
- package/src/execution/engine/tools/integration/server/adapters/instantly/fetch/remove-from-subsequence/index.ts +98 -98
- package/src/execution/engine/tools/integration/server/adapters/instantly/fetch/send-reply/index.ts +126 -126
- package/src/execution/engine/tools/integration/server/adapters/instantly/fetch/update-campaign/__tests__/index.test.ts +193 -193
- package/src/execution/engine/tools/integration/server/adapters/instantly/fetch/update-campaign/index.ts +99 -99
- package/src/execution/engine/tools/integration/server/adapters/instantly/fetch/update-interest-status/__tests__/index.test.ts +621 -621
- package/src/execution/engine/tools/integration/server/adapters/instantly/fetch/update-interest-status/index.ts +125 -125
- package/src/execution/engine/tools/integration/server/adapters/instantly/index.ts +29 -29
- package/src/execution/engine/tools/integration/server/adapters/instantly/instantly-adapter.ts +178 -178
- package/src/execution/engine/tools/integration/server/adapters/instantly/instantly-tools.ts +1473 -1473
- package/src/execution/engine/tools/integration/server/adapters/millionverifier/fetch/check-credits/index.ts +59 -59
- package/src/execution/engine/tools/integration/server/adapters/millionverifier/fetch/verify-email/index.ts +102 -102
- package/src/execution/engine/tools/integration/server/adapters/millionverifier/index.ts +17 -17
- package/src/execution/engine/tools/integration/server/adapters/millionverifier/millionverifier-adapter.ts +80 -80
- package/src/execution/engine/tools/integration/server/adapters/millionverifier/millionverifier-tools.ts +102 -102
- package/src/execution/engine/tools/integration/server/adapters/resend/fetch/get-email/index.ts +102 -102
- package/src/execution/engine/tools/integration/server/adapters/resend/fetch/send-email/index.ts +134 -134
- package/src/execution/engine/tools/integration/server/adapters/resend/fetch/utils/types.ts +75 -75
- package/src/execution/engine/tools/integration/server/adapters/resend/index.ts +27 -27
- package/src/execution/engine/tools/integration/server/adapters/resend/resend-adapter.ts +108 -108
- package/src/execution/engine/tools/integration/server/adapters/resend/resend-tools.ts +132 -132
- package/src/execution/engine/tools/integration/server/adapters/signature-api/fetch/create-envelope/index.ts +274 -274
- package/src/execution/engine/tools/integration/server/adapters/signature-api/fetch/download-document/index.ts +230 -230
- package/src/execution/engine/tools/integration/server/adapters/signature-api/fetch/get-envelope/index.ts +133 -133
- package/src/execution/engine/tools/integration/server/adapters/signature-api/fetch/void-envelope/index.ts +90 -90
- package/src/execution/engine/tools/integration/server/adapters/stripe/fetch/utils/types.ts +210 -210
- package/src/execution/engine/tools/integration/server/adapters/stripe/stripe-adapter.ts +517 -517
- package/src/execution/engine/tools/integration/server/adapters/stripe/stripe-tools.ts +309 -309
- package/src/execution/engine/tools/integration/server/adapters/tomba/fetch/domain-search/index.ts +133 -133
- package/src/execution/engine/tools/integration/server/adapters/tomba/fetch/email-finder/index.ts +122 -122
- package/src/execution/engine/tools/integration/server/adapters/tomba/fetch/email-verifier/index.ts +111 -111
- package/src/execution/engine/tools/integration/server/adapters/tomba/index.ts +11 -11
- package/src/execution/engine/tools/integration/server/adapters/tomba/tomba-adapter.ts +78 -78
- package/src/execution/engine/tools/integration/server/adapters/tomba/tomba-tools.ts +222 -222
- package/src/execution/engine/tools/integration/server/index.ts +61 -61
- package/src/execution/engine/tools/integration/service.ts +161 -161
- package/src/execution/engine/tools/integration/tool.ts +253 -253
- package/src/execution/engine/tools/integration/types/anymailfinder.ts +74 -74
- package/src/execution/engine/tools/integration/types/apify.ts +92 -92
- package/src/execution/engine/tools/integration/types/index.ts +19 -19
- package/src/execution/engine/tools/integration/types/instantly.ts +557 -557
- package/src/execution/engine/tools/integration/types/millionverifier.ts +56 -56
- package/src/execution/engine/tools/integration/types/stripe.ts +162 -162
- package/src/execution/engine/tools/integration/types/tomba.ts +94 -94
- package/src/execution/engine/tools/lead-service-types.ts +884 -884
- package/src/execution/engine/tools/llm/index.ts +11 -11
- package/src/execution/engine/tools/llm/server/index.ts +8 -8
- package/src/execution/engine/tools/llm/server/llm-call-tool.ts +118 -118
- package/src/execution/engine/tools/platform/__tests__/pdf.test.ts +441 -441
- package/src/execution/engine/tools/platform/acquisition/company-tools.ts +248 -248
- package/src/execution/engine/tools/platform/acquisition/contact-tools.ts +319 -319
- package/src/execution/engine/tools/platform/acquisition/index.ts +43 -43
- package/src/execution/engine/tools/platform/acquisition/list-tools.ts +148 -148
- package/src/execution/engine/tools/platform/acquisition/types.ts +260 -260
- package/src/execution/engine/tools/platform/email/index.ts +122 -122
- package/src/execution/engine/tools/platform/email/types.ts +96 -96
- package/src/execution/engine/tools/platform/index.ts +157 -157
- package/src/execution/engine/tools/platform/notification.ts +81 -81
- package/src/execution/engine/tools/platform/pdf/index.ts +110 -110
- package/src/execution/engine/tools/platform/pdf/types.ts +77 -77
- package/src/execution/engine/tools/platform/scheduler.ts +87 -87
- package/src/execution/engine/tools/platform/storage/index.ts +370 -370
- package/src/execution/engine/tools/platform/types.ts +148 -148
- package/src/execution/engine/tools/registry.ts +700 -700
- package/src/execution/engine/tools/tool-maps.ts +786 -786
- package/src/execution/engine/tools/types.ts +233 -233
- package/src/execution/engine/workflow/__tests__/errors.test.ts +139 -139
- package/src/execution/engine/workflow/errors.ts +63 -63
- package/src/execution/engine/workflow/helpers/index.ts +11 -11
- package/src/execution/engine/workflow/helpers/server/index.ts +8 -8
- package/src/execution/engine/workflow/helpers/server/llm-call.ts +93 -93
- package/src/execution/engine/workflow/index.ts +19 -19
- package/src/execution/engine/workflow/log-truncate.ts +26 -26
- package/src/execution/engine/workflow/logging.ts +191 -191
- package/src/execution/engine/workflow/types.ts +182 -182
- package/src/execution/engine/workflow/utils.ts +280 -280
- package/src/execution/engine/workflow/workflow.ts +168 -168
- package/src/execution/index.ts +3 -3
- package/src/execution/scheduler/__tests__/api-schemas.test.ts +733 -733
- package/src/execution/scheduler/__tests__/utils.test.ts +1009 -1009
- package/src/execution/scheduler/api-schemas.ts +296 -296
- package/src/execution/scheduler/index.ts +50 -50
- package/src/execution/scheduler/schemas.ts +264 -264
- package/src/execution/scheduler/types.ts +111 -111
- package/src/execution/scheduler/utils.ts +364 -364
- package/src/forms/index.ts +7 -7
- package/src/forms/schemas.ts +69 -69
- package/src/forms/types.ts +70 -70
- package/src/index.ts +71 -60
- package/src/integrations/credentials/__tests__/schemas.test.ts +82 -82
- package/src/integrations/credentials/__tests__/utils.test.ts +144 -144
- package/src/integrations/credentials/api-schemas.ts +143 -143
- package/src/integrations/credentials/index.ts +32 -32
- package/src/integrations/credentials/schemas.ts +164 -164
- package/src/integrations/credentials/utils.ts +59 -59
- package/src/integrations/oauth/__tests__/provider-registry.test.ts +59 -59
- package/src/integrations/oauth/api-schemas.ts +92 -92
- package/src/integrations/oauth/index.ts +19 -19
- package/src/integrations/oauth/provider-registry.ts +61 -61
- package/src/integrations/oauth/server/__tests__/refresh-concurrent.test.ts +183 -183
- package/src/integrations/oauth/server/__tests__/refresh.test.ts +577 -577
- package/src/integrations/oauth/server/credentials.ts +39 -39
- package/src/integrations/oauth/server/refresh.ts +214 -214
- package/src/integrations/oauth/types.ts +34 -34
- package/src/integrations/webhook-endpoints/__tests__/api-schemas.test.ts +318 -318
- package/src/integrations/webhook-endpoints/api-schemas.ts +102 -102
- package/src/integrations/webhook-endpoints/index.ts +28 -28
- package/src/integrations/webhook-endpoints/types.ts +51 -51
- package/src/operations/activities/api-schemas.ts +79 -79
- package/src/operations/activities/index.ts +9 -9
- package/src/operations/activities/sse-events.ts +30 -30
- package/src/operations/activities/types.ts +63 -63
- package/src/operations/debug-logs/client.ts +60 -60
- package/src/operations/debug-logs/debug-logger.ts +83 -83
- package/src/operations/debug-logs/index.ts +8 -8
- package/src/operations/debug-logs/server.ts +19 -19
- package/src/operations/debug-logs/types.ts +33 -33
- package/src/operations/index.ts +50 -50
- package/src/operations/notifications/api-schemas.ts +91 -91
- package/src/operations/notifications/index.ts +3 -3
- package/src/operations/notifications/sse-events.ts +21 -21
- package/src/operations/notifications/types.ts +47 -47
- package/src/operations/observability/__tests__/openrouter-cost-flow.test.ts +297 -297
- package/src/operations/observability/__tests__/utils.test.ts +54 -54
- package/src/operations/observability/ai-usage-collector.ts +64 -64
- package/src/operations/observability/index.ts +13 -13
- package/src/operations/observability/metrics-collector.ts +49 -49
- package/src/operations/observability/schemas.ts +39 -39
- package/src/operations/observability/types.ts +463 -463
- package/src/operations/observability/utils.ts +77 -77
- package/src/operations/sessions/__tests__/manager.test.ts +821 -821
- package/src/operations/sessions/index.ts +26 -26
- package/src/operations/sessions/server/manager.ts +90 -90
- package/src/operations/sessions/server/session.ts +180 -180
- package/src/operations/sessions/types.ts +98 -98
- package/src/operations/triggers/index.ts +12 -12
- package/src/operations/triggers/webhook/definitions/instantly-account-error.ts +44 -44
- package/src/operations/triggers/webhook/definitions/instantly-auto-reply-received.ts +51 -51
- package/src/operations/triggers/webhook/definitions/instantly-campaign-completed.ts +45 -45
- package/src/operations/triggers/webhook/definitions/instantly-email-bounced.ts +49 -49
- package/src/operations/triggers/webhook/definitions/instantly-lead-unsubscribed.ts +45 -45
- package/src/operations/triggers/webhook/definitions/instantly-reply-received.ts +54 -54
- package/src/operations/triggers/webhook/index.ts +35 -35
- package/src/operations/triggers/webhook/types.ts +74 -74
- package/src/organization-model/README.md +97 -97
- package/src/organization-model/__tests__/defaults.test.ts +175 -175
- package/src/organization-model/__tests__/domains/customers.test.ts +295 -295
- package/src/organization-model/__tests__/domains/goals.test.ts +479 -479
- package/src/organization-model/__tests__/domains/identity.test.ts +279 -279
- package/src/organization-model/__tests__/domains/navigation.test.ts +212 -212
- package/src/organization-model/__tests__/domains/offerings.test.ts +419 -419
- package/src/organization-model/__tests__/domains/operations.test.ts +203 -203
- package/src/organization-model/__tests__/domains/resource-mappings.test.ts +362 -362
- package/src/organization-model/__tests__/domains/roles.test.ts +347 -347
- package/src/organization-model/__tests__/domains/statuses.test.ts +243 -243
- package/src/organization-model/__tests__/foundation.test.ts +105 -105
- package/src/organization-model/__tests__/graph.test.ts +894 -894
- package/src/organization-model/__tests__/resolve.test.ts +690 -690
- package/src/organization-model/__tests__/schema.test.ts +407 -407
- package/src/organization-model/contracts.ts +14 -14
- package/src/organization-model/defaults.ts +148 -148
- package/src/organization-model/domains/branding.ts +22 -22
- package/src/organization-model/domains/customers.ts +75 -75
- package/src/organization-model/domains/features.ts +22 -22
- package/src/organization-model/domains/goals.ts +80 -80
- package/src/organization-model/domains/identity.ts +94 -94
- package/src/organization-model/domains/navigation.ts +391 -391
- package/src/organization-model/domains/offerings.ts +66 -66
- package/src/organization-model/domains/operations.ts +85 -85
- package/src/organization-model/domains/projects.ts +48 -48
- package/src/organization-model/domains/prospecting.ts +33 -33
- package/src/organization-model/domains/roles.ts +55 -55
- package/src/organization-model/domains/sales.ts +94 -94
- package/src/organization-model/domains/shared.ts +62 -62
- package/src/organization-model/domains/statuses.ts +130 -130
- package/src/organization-model/foundation.ts +97 -97
- package/src/organization-model/graph/build.ts +399 -399
- package/src/organization-model/graph/index.ts +4 -4
- package/src/organization-model/graph/schema.ts +48 -48
- package/src/organization-model/graph/types.ts +40 -40
- package/src/organization-model/index.ts +13 -13
- package/src/organization-model/organization-graph.mdx +272 -272
- package/src/organization-model/organization-model.mdx +320 -320
- package/src/organization-model/published.ts +85 -85
- package/src/organization-model/resolve.ts +66 -66
- package/src/organization-model/schema.ts +287 -287
- package/src/organization-model/types.ts +46 -46
- package/src/platform/api/index.ts +1 -1
- package/src/platform/api/types.ts +35 -35
- package/src/platform/constants/http.ts +37 -37
- package/src/platform/constants/index.ts +5 -5
- package/src/platform/constants/limits.ts +32 -32
- package/src/platform/constants/resilience.ts +51 -51
- package/src/platform/constants/timeouts.ts +20 -20
- package/src/platform/constants/versions.ts +3 -3
- package/src/platform/registry/__tests__/resource-registry-static.test.ts +347 -347
- package/src/platform/registry/__tests__/resource-registry.integration.test.ts +1028 -1028
- package/src/platform/registry/__tests__/resource-registry.list-executable.test.ts +393 -393
- package/src/platform/registry/__tests__/resource-registry.test.ts +2005 -2005
- package/src/platform/registry/__tests__/serialization.test.ts +1127 -1127
- package/src/platform/registry/command-view.ts +180 -180
- package/src/platform/registry/domains.ts +165 -165
- package/src/platform/registry/index.ts +93 -93
- package/src/platform/registry/reserved.ts +24 -24
- package/src/platform/registry/resource-metadata.ts +59 -59
- package/src/platform/registry/resource-registry.command-queue-groups.test.ts +129 -129
- package/src/platform/registry/resource-registry.ts +876 -876
- package/src/platform/registry/serialization.ts +273 -273
- package/src/platform/registry/serialized-types.ts +231 -231
- package/src/platform/registry/stats-types.ts +66 -66
- package/src/platform/registry/types.ts +404 -404
- package/src/platform/registry/validation.ts +513 -513
- package/src/platform/resilience/__tests__/rate-limiter.test.ts +471 -471
- package/src/platform/resilience/circuit-breaker.ts +164 -164
- package/src/platform/resilience/errors.ts +68 -68
- package/src/platform/resilience/http-error-mapper.ts +129 -129
- package/src/platform/resilience/index.ts +93 -93
- package/src/platform/resilience/rate-limiter-types.ts +46 -46
- package/src/platform/resilience/rate-limiter.ts +140 -140
- package/src/platform/resilience/retry.ts +89 -89
- package/src/platform/resilience/timeout.ts +63 -63
- package/src/platform/sse/events.ts +37 -37
- package/src/platform/sse/index.ts +7 -7
- package/src/platform/utils/__tests__/validation.test.ts +1083 -1083
- package/src/platform/utils/currency.ts +96 -96
- package/src/platform/utils/debounce.ts +52 -52
- package/src/platform/utils/error.ts +41 -41
- package/src/platform/utils/hmac.test.ts +97 -97
- package/src/platform/utils/index.ts +32 -32
- package/src/platform/utils/server/betterstack-logger.ts +210 -210
- package/src/platform/utils/server/hmac.ts +44 -44
- package/src/platform/utils/server/unsubscribe.ts +111 -111
- package/src/platform/utils/token-counter.ts +96 -96
- package/src/platform/utils/validation.ts +425 -425
- package/src/projects/api-schemas.ts +268 -268
- package/src/published.ts +1 -1
- package/src/reference/_generated/contracts.md +607 -607
- package/src/reference/glossary.md +105 -105
- package/src/requests/__tests__/api-schemas.test.ts +277 -277
- package/src/requests/api-schemas.ts +83 -83
- package/src/requests/index.ts +1 -1
- package/src/scaffold-registry/__tests__/index.test.ts +17 -0
- package/src/scaffold-registry/__tests__/schema.test.ts +329 -230
- package/src/scaffold-registry/index.ts +205 -189
- package/src/scaffold-registry/schema.ts +196 -128
- package/src/server.ts +272 -272
- package/src/supabase/database.types.ts +2719 -2719
- package/src/supabase/helpers.ts +20 -20
- package/src/supabase/index.ts +52 -52
- package/src/supabase/server/client.ts +58 -58
- package/src/test-utils/README.md +30 -138
- package/src/test-utils/browser-mocks.ts +54 -54
- package/src/test-utils/fixtures/api-keys.ts +52 -52
- package/src/test-utils/fixtures/index.ts +4 -4
- package/src/test-utils/fixtures/memberships.ts +80 -80
- package/src/test-utils/fixtures/organizations.ts +69 -69
- package/src/test-utils/fixtures/users.ts +79 -79
- package/src/test-utils/index.ts +7 -8
- package/src/test-utils/mocks/index.ts +2 -2
- package/src/test-utils/mocks/supabase.ts +142 -142
- package/src/test-utils/mocks/workos.ts +108 -108
- package/src/test-utils/published.ts +4 -0
- package/src/test-utils/rls/RLSTestContext.ts +554 -554
- package/src/test-utils/rls/index.ts +1 -1
|
@@ -1,1189 +1,1189 @@
|
|
|
1
|
-
import type { sheets_v4 } from '@googleapis/sheets'
|
|
2
|
-
import type { BaseIntegrationAdapter } from '../../../base-integration-adapter'
|
|
3
|
-
import { ToolingError } from '../../../../types'
|
|
4
|
-
import type { ExecutionContext } from '../../../../../base/types'
|
|
5
|
-
import { getOAuthCredentials } from '../../../../../../../integrations/oauth/server/credentials'
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Google Sheets credentials format
|
|
9
|
-
* Stored in credentials table, encrypted
|
|
10
|
-
*/
|
|
11
|
-
interface GoogleSheetsCredentials {
|
|
12
|
-
accessToken: string
|
|
13
|
-
refreshToken?: string
|
|
14
|
-
expiresAt?: string
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* Sheet info returned from metadata calls
|
|
19
|
-
*/
|
|
20
|
-
interface SheetInfo {
|
|
21
|
-
sheetId: number
|
|
22
|
-
title: string
|
|
23
|
-
index: number
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* Read sheet parameters
|
|
28
|
-
*/
|
|
29
|
-
interface ReadSheetParams {
|
|
30
|
-
spreadsheetId: string
|
|
31
|
-
range: string
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* Read sheet result
|
|
36
|
-
*/
|
|
37
|
-
interface ReadSheetResult {
|
|
38
|
-
values: string[][]
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* Write sheet parameters
|
|
43
|
-
*/
|
|
44
|
-
interface WriteSheetParams {
|
|
45
|
-
spreadsheetId: string
|
|
46
|
-
range: string
|
|
47
|
-
values: string[][]
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* Write sheet result
|
|
52
|
-
*/
|
|
53
|
-
interface WriteSheetResult {
|
|
54
|
-
updatedCells: number
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
/**
|
|
58
|
-
* Append rows parameters
|
|
59
|
-
*/
|
|
60
|
-
interface AppendRowsParams {
|
|
61
|
-
spreadsheetId: string
|
|
62
|
-
range: string
|
|
63
|
-
values: string[][]
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* Append rows result
|
|
68
|
-
*/
|
|
69
|
-
interface AppendRowsResult {
|
|
70
|
-
updatedRows: number
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
/**
|
|
74
|
-
* Clear range parameters
|
|
75
|
-
*/
|
|
76
|
-
interface ClearRangeParams {
|
|
77
|
-
spreadsheetId: string
|
|
78
|
-
range: string
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
/**
|
|
82
|
-
* Clear range result
|
|
83
|
-
*/
|
|
84
|
-
interface ClearRangeResult {
|
|
85
|
-
clearedRange: string
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
/**
|
|
89
|
-
* Get spreadsheet metadata parameters
|
|
90
|
-
*/
|
|
91
|
-
interface GetSpreadsheetMetadataParams {
|
|
92
|
-
spreadsheetId: string
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
/**
|
|
96
|
-
* Get spreadsheet metadata result
|
|
97
|
-
*/
|
|
98
|
-
interface GetSpreadsheetMetadataResult {
|
|
99
|
-
title: string
|
|
100
|
-
sheets: SheetInfo[]
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
/**
|
|
104
|
-
* Batch update parameters
|
|
105
|
-
*/
|
|
106
|
-
interface BatchUpdateParams {
|
|
107
|
-
spreadsheetId: string
|
|
108
|
-
data: Array<{ range: string; values: string[][] }>
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
/**
|
|
112
|
-
* Batch update result
|
|
113
|
-
*/
|
|
114
|
-
interface BatchUpdateResult {
|
|
115
|
-
totalUpdatedCells: number
|
|
116
|
-
totalUpdatedRows: number
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
// ============================================
|
|
120
|
-
// Workflow-friendly method interfaces
|
|
121
|
-
// ============================================
|
|
122
|
-
|
|
123
|
-
/**
|
|
124
|
-
* Get headers parameters
|
|
125
|
-
*/
|
|
126
|
-
interface GetHeadersParams {
|
|
127
|
-
spreadsheetId: string
|
|
128
|
-
sheetName: string
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
/**
|
|
132
|
-
* Get headers result
|
|
133
|
-
*/
|
|
134
|
-
interface GetHeadersResult {
|
|
135
|
-
headers: string[]
|
|
136
|
-
columnCount: number
|
|
137
|
-
columnMap: Record<string, string> // header name -> column letter (e.g., { Name: 'A', Id: 'B' })
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
/**
|
|
141
|
-
* Get last row parameters
|
|
142
|
-
*/
|
|
143
|
-
interface GetLastRowParams {
|
|
144
|
-
spreadsheetId: string
|
|
145
|
-
sheetName: string
|
|
146
|
-
column?: string // Optional column to check (default: A)
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
/**
|
|
150
|
-
* Get last row result
|
|
151
|
-
*/
|
|
152
|
-
interface GetLastRowResult {
|
|
153
|
-
lastRow: number
|
|
154
|
-
isEmpty: boolean
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
/**
|
|
158
|
-
* Get row by value parameters
|
|
159
|
-
*/
|
|
160
|
-
interface GetRowByValueParams {
|
|
161
|
-
spreadsheetId: string
|
|
162
|
-
sheetName: string
|
|
163
|
-
searchColumn: string // Column letter (e.g., 'A', 'B')
|
|
164
|
-
searchValue: string
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
/**
|
|
168
|
-
* Get row by value result
|
|
169
|
-
*/
|
|
170
|
-
interface GetRowByValueResult {
|
|
171
|
-
found: boolean
|
|
172
|
-
rowNumber: number | null
|
|
173
|
-
rowData: string[] | null
|
|
174
|
-
headers: string[]
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
/**
|
|
178
|
-
* Update row by value parameters
|
|
179
|
-
*/
|
|
180
|
-
interface UpdateRowByValueParams {
|
|
181
|
-
spreadsheetId: string
|
|
182
|
-
sheetName: string
|
|
183
|
-
searchColumn: string
|
|
184
|
-
searchValue: string
|
|
185
|
-
updates: Record<string, string> // Column header -> new value
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
/**
|
|
189
|
-
* Update row by value result
|
|
190
|
-
*/
|
|
191
|
-
interface UpdateRowByValueResult {
|
|
192
|
-
found: boolean
|
|
193
|
-
rowNumber: number | null
|
|
194
|
-
updatedCells: number
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
/**
|
|
198
|
-
* Upsert row parameters
|
|
199
|
-
*/
|
|
200
|
-
interface UpsertRowParams {
|
|
201
|
-
spreadsheetId: string
|
|
202
|
-
sheetName: string
|
|
203
|
-
keyColumn: string // Column to match on (e.g., 'A' or header name)
|
|
204
|
-
keyValue: string
|
|
205
|
-
rowData: Record<string, string> // Column header -> value
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
/**
|
|
209
|
-
* Upsert row result
|
|
210
|
-
*/
|
|
211
|
-
interface UpsertRowResult {
|
|
212
|
-
action: 'inserted' | 'updated'
|
|
213
|
-
rowNumber: number
|
|
214
|
-
updatedCells: number
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
/**
|
|
218
|
-
* Filter rows parameters
|
|
219
|
-
*/
|
|
220
|
-
interface FilterRowsParams {
|
|
221
|
-
spreadsheetId: string
|
|
222
|
-
sheetName: string
|
|
223
|
-
filterColumn: string
|
|
224
|
-
filterValue: string
|
|
225
|
-
operator?: 'equals' | 'contains' | 'startsWith' | 'endsWith'
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
/**
|
|
229
|
-
* Filter rows result
|
|
230
|
-
*/
|
|
231
|
-
interface FilterRowsResult {
|
|
232
|
-
rows: Array<{
|
|
233
|
-
rowNumber: number
|
|
234
|
-
data: string[]
|
|
235
|
-
}>
|
|
236
|
-
matchCount: number
|
|
237
|
-
headers: string[]
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
/**
|
|
241
|
-
* Delete row by value parameters
|
|
242
|
-
*/
|
|
243
|
-
interface DeleteRowByValueParams {
|
|
244
|
-
spreadsheetId: string
|
|
245
|
-
sheetName: string
|
|
246
|
-
searchColumn: string
|
|
247
|
-
searchValue: string
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
/**
|
|
251
|
-
* Delete row by value result
|
|
252
|
-
*/
|
|
253
|
-
interface DeleteRowByValueResult {
|
|
254
|
-
found: boolean
|
|
255
|
-
deletedRowNumber: number | null
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
/**
|
|
259
|
-
* Google Sheets adapter for spreadsheet operations via Google Sheets API v4
|
|
260
|
-
*
|
|
261
|
-
* Core methods:
|
|
262
|
-
* - readSheet: Read cell range from spreadsheet
|
|
263
|
-
* - writeSheet: Write to cell range (overwrites existing data)
|
|
264
|
-
* - appendRows: Append rows to end of table
|
|
265
|
-
* - clearRange: Clear values from range (preserves formatting)
|
|
266
|
-
* - getSpreadsheetMetadata: Get spreadsheet title and sheet list
|
|
267
|
-
* - batchUpdate: Update multiple ranges in single request
|
|
268
|
-
*
|
|
269
|
-
* Workflow-friendly methods:
|
|
270
|
-
* - getHeaders: Get column headers from first row
|
|
271
|
-
* - getLastRow: Find the last row with data
|
|
272
|
-
* - getRowByValue: Find a row by searching a column
|
|
273
|
-
* - updateRowByValue: Update a row found by search
|
|
274
|
-
* - upsertRow: Insert or update a row based on key column
|
|
275
|
-
* - filterRows: Get all rows matching a condition
|
|
276
|
-
* - deleteRowByValue: Delete a row found by search
|
|
277
|
-
*
|
|
278
|
-
* @example
|
|
279
|
-
* const adapter = new GoogleSheetsAdapter()
|
|
280
|
-
* const result = await adapter.call('readSheet', {
|
|
281
|
-
* spreadsheetId: '1BxiMVs0XRA5nFMdKvBdBZjgmUUqptlbs74OgvE2upms',
|
|
282
|
-
* range: 'Sheet1!A1:C10'
|
|
283
|
-
* }, { accessToken: 'ya29...' })
|
|
284
|
-
*/
|
|
285
|
-
export class GoogleSheetsAdapter implements BaseIntegrationAdapter {
|
|
286
|
-
readonly name = 'google-sheets'
|
|
287
|
-
|
|
288
|
-
/**
|
|
289
|
-
* Call Google Sheets API method
|
|
290
|
-
* @param method - Method name (e.g., 'readSheet', 'writeSheet')
|
|
291
|
-
* @param params - Method parameters
|
|
292
|
-
* @param credentials - OAuth2 access token
|
|
293
|
-
* @param context - Execution context for logging and tracing
|
|
294
|
-
*/
|
|
295
|
-
async call(
|
|
296
|
-
method: string,
|
|
297
|
-
params: unknown,
|
|
298
|
-
credentials: Record<string, unknown>,
|
|
299
|
-
context?: ExecutionContext
|
|
300
|
-
): Promise<unknown> {
|
|
301
|
-
// Validate credentials format
|
|
302
|
-
if (!this.validateCredentials(credentials)) {
|
|
303
|
-
throw new ToolingError('credentials_invalid', 'Invalid Google Sheets credentials', {
|
|
304
|
-
integration: 'google-sheets',
|
|
305
|
-
method
|
|
306
|
-
})
|
|
307
|
-
}
|
|
308
|
-
const sheetsCreds = credentials as unknown as GoogleSheetsCredentials
|
|
309
|
-
|
|
310
|
-
// Create authenticated Google Sheets client
|
|
311
|
-
const sheets = await this.createClient(sheetsCreds)
|
|
312
|
-
|
|
313
|
-
// Route to method handler
|
|
314
|
-
switch (method) {
|
|
315
|
-
// Core methods
|
|
316
|
-
case 'readSheet':
|
|
317
|
-
return this.readSheet(sheets, params as ReadSheetParams, context)
|
|
318
|
-
case 'writeSheet':
|
|
319
|
-
return this.writeSheet(sheets, params as WriteSheetParams, context)
|
|
320
|
-
case 'appendRows':
|
|
321
|
-
return this.appendRows(sheets, params as AppendRowsParams, context)
|
|
322
|
-
case 'clearRange':
|
|
323
|
-
return this.clearRange(sheets, params as ClearRangeParams, context)
|
|
324
|
-
case 'getSpreadsheetMetadata':
|
|
325
|
-
return this.getSpreadsheetMetadata(sheets, params as GetSpreadsheetMetadataParams, context)
|
|
326
|
-
case 'batchUpdate':
|
|
327
|
-
return this.batchUpdate(sheets, params as BatchUpdateParams, context)
|
|
328
|
-
// Workflow-friendly methods
|
|
329
|
-
case 'getHeaders':
|
|
330
|
-
return this.getHeaders(sheets, params as GetHeadersParams, context)
|
|
331
|
-
case 'getLastRow':
|
|
332
|
-
return this.getLastRow(sheets, params as GetLastRowParams, context)
|
|
333
|
-
case 'getRowByValue':
|
|
334
|
-
return this.getRowByValue(sheets, params as GetRowByValueParams, context)
|
|
335
|
-
case 'updateRowByValue':
|
|
336
|
-
return this.updateRowByValue(sheets, params as UpdateRowByValueParams, context)
|
|
337
|
-
case 'upsertRow':
|
|
338
|
-
return this.upsertRow(sheets, params as UpsertRowParams, context)
|
|
339
|
-
case 'filterRows':
|
|
340
|
-
return this.filterRows(sheets, params as FilterRowsParams, context)
|
|
341
|
-
case 'deleteRowByValue':
|
|
342
|
-
return this.deleteRowByValue(sheets, params as DeleteRowByValueParams, context)
|
|
343
|
-
default:
|
|
344
|
-
throw new ToolingError('method_not_found', `Unknown method: ${method}`, {
|
|
345
|
-
integration: 'google-sheets',
|
|
346
|
-
method
|
|
347
|
-
})
|
|
348
|
-
}
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
/**
|
|
352
|
-
* Create authenticated Google Sheets client (lazy loads @googleapis/sheets SDK)
|
|
353
|
-
* CRITICAL: Use dynamic import to prevent OOM and module-level crashes
|
|
354
|
-
*/
|
|
355
|
-
private async createClient(creds: GoogleSheetsCredentials): Promise<sheets_v4.Sheets> {
|
|
356
|
-
const { sheets } = await import('@googleapis/sheets')
|
|
357
|
-
const { OAuth2Client } = await import('google-auth-library')
|
|
358
|
-
|
|
359
|
-
// Get client credentials from environment for token refresh
|
|
360
|
-
const { clientId, clientSecret } = getOAuthCredentials('google-sheets')
|
|
361
|
-
|
|
362
|
-
const oauth2Client = new OAuth2Client(clientId, clientSecret)
|
|
363
|
-
oauth2Client.setCredentials({
|
|
364
|
-
access_token: creds.accessToken,
|
|
365
|
-
refresh_token: creds.refreshToken
|
|
366
|
-
})
|
|
367
|
-
|
|
368
|
-
return sheets({ version: 'v4', auth: oauth2Client })
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
/**
|
|
372
|
-
* Read data from a spreadsheet range
|
|
373
|
-
*/
|
|
374
|
-
private async readSheet(
|
|
375
|
-
sheets: sheets_v4.Sheets,
|
|
376
|
-
params: ReadSheetParams,
|
|
377
|
-
context?: ExecutionContext
|
|
378
|
-
): Promise<ReadSheetResult> {
|
|
379
|
-
// Validate params
|
|
380
|
-
if (!params.spreadsheetId) {
|
|
381
|
-
throw new ToolingError('validation_error', 'Missing required field: spreadsheetId', { params })
|
|
382
|
-
}
|
|
383
|
-
if (!params.range) {
|
|
384
|
-
throw new ToolingError('validation_error', 'Missing required field: range', { params })
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
try {
|
|
388
|
-
const response = await sheets.spreadsheets.values.get({
|
|
389
|
-
spreadsheetId: params.spreadsheetId,
|
|
390
|
-
range: params.range
|
|
391
|
-
})
|
|
392
|
-
|
|
393
|
-
if (context?.logger) {
|
|
394
|
-
context.logger.info(
|
|
395
|
-
`[GoogleSheetsAdapter] Sheet read: organizationId=${context.organizationId} executionId=${context.executionId} spreadsheetId=${params.spreadsheetId} range=${params.range} rowCount=${response.data.values?.length || 0}`
|
|
396
|
-
)
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
return {
|
|
400
|
-
values: (response.data.values as string[][]) || []
|
|
401
|
-
}
|
|
402
|
-
} catch (error: any) {
|
|
403
|
-
throw this.handleApiError(error, params.spreadsheetId)
|
|
404
|
-
}
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
/**
|
|
408
|
-
* Write data to a spreadsheet range (overwrites existing data)
|
|
409
|
-
*/
|
|
410
|
-
private async writeSheet(
|
|
411
|
-
sheets: sheets_v4.Sheets,
|
|
412
|
-
params: WriteSheetParams,
|
|
413
|
-
context?: ExecutionContext
|
|
414
|
-
): Promise<WriteSheetResult> {
|
|
415
|
-
// Validate params
|
|
416
|
-
if (!params.spreadsheetId) {
|
|
417
|
-
throw new ToolingError('validation_error', 'Missing required field: spreadsheetId', { params })
|
|
418
|
-
}
|
|
419
|
-
if (!params.range) {
|
|
420
|
-
throw new ToolingError('validation_error', 'Missing required field: range', { params })
|
|
421
|
-
}
|
|
422
|
-
if (!params.values || !Array.isArray(params.values)) {
|
|
423
|
-
throw new ToolingError('validation_error', 'Missing required field: values (must be 2D array)', { params })
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
try {
|
|
427
|
-
const response = await sheets.spreadsheets.values.update({
|
|
428
|
-
spreadsheetId: params.spreadsheetId,
|
|
429
|
-
range: params.range,
|
|
430
|
-
valueInputOption: 'USER_ENTERED',
|
|
431
|
-
requestBody: { values: params.values }
|
|
432
|
-
})
|
|
433
|
-
|
|
434
|
-
if (context?.logger) {
|
|
435
|
-
context.logger.info(
|
|
436
|
-
`[GoogleSheetsAdapter] Sheet written: organizationId=${context.organizationId} executionId=${context.executionId} spreadsheetId=${params.spreadsheetId} range=${params.range} updatedCells=${response.data.updatedCells}`
|
|
437
|
-
)
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
return {
|
|
441
|
-
updatedCells: response.data.updatedCells || 0
|
|
442
|
-
}
|
|
443
|
-
} catch (error: any) {
|
|
444
|
-
throw this.handleApiError(error, params.spreadsheetId)
|
|
445
|
-
}
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
/**
|
|
449
|
-
* Append rows to end of a table
|
|
450
|
-
*/
|
|
451
|
-
private async appendRows(
|
|
452
|
-
sheets: sheets_v4.Sheets,
|
|
453
|
-
params: AppendRowsParams,
|
|
454
|
-
context?: ExecutionContext
|
|
455
|
-
): Promise<AppendRowsResult> {
|
|
456
|
-
// Validate params
|
|
457
|
-
if (!params.spreadsheetId) {
|
|
458
|
-
throw new ToolingError('validation_error', 'Missing required field: spreadsheetId', { params })
|
|
459
|
-
}
|
|
460
|
-
if (!params.range) {
|
|
461
|
-
throw new ToolingError('validation_error', 'Missing required field: range', { params })
|
|
462
|
-
}
|
|
463
|
-
if (!params.values || !Array.isArray(params.values)) {
|
|
464
|
-
throw new ToolingError('validation_error', 'Missing required field: values (must be 2D array)', { params })
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
try {
|
|
468
|
-
const response = await sheets.spreadsheets.values.append({
|
|
469
|
-
spreadsheetId: params.spreadsheetId,
|
|
470
|
-
range: params.range,
|
|
471
|
-
valueInputOption: 'USER_ENTERED',
|
|
472
|
-
insertDataOption: 'INSERT_ROWS',
|
|
473
|
-
requestBody: { values: params.values }
|
|
474
|
-
})
|
|
475
|
-
|
|
476
|
-
if (context?.logger) {
|
|
477
|
-
context.logger.info(
|
|
478
|
-
`[GoogleSheetsAdapter] Rows appended: organizationId=${context.organizationId} executionId=${context.executionId} spreadsheetId=${params.spreadsheetId} range=${params.range} updatedRows=${response.data.updates?.updatedRows}`
|
|
479
|
-
)
|
|
480
|
-
}
|
|
481
|
-
|
|
482
|
-
return {
|
|
483
|
-
updatedRows: response.data.updates?.updatedRows || 0
|
|
484
|
-
}
|
|
485
|
-
} catch (error: any) {
|
|
486
|
-
throw this.handleApiError(error, params.spreadsheetId)
|
|
487
|
-
}
|
|
488
|
-
}
|
|
489
|
-
|
|
490
|
-
/**
|
|
491
|
-
* Clear values from a range (preserves formatting)
|
|
492
|
-
*/
|
|
493
|
-
private async clearRange(
|
|
494
|
-
sheets: sheets_v4.Sheets,
|
|
495
|
-
params: ClearRangeParams,
|
|
496
|
-
context?: ExecutionContext
|
|
497
|
-
): Promise<ClearRangeResult> {
|
|
498
|
-
// Validate params
|
|
499
|
-
if (!params.spreadsheetId) {
|
|
500
|
-
throw new ToolingError('validation_error', 'Missing required field: spreadsheetId', { params })
|
|
501
|
-
}
|
|
502
|
-
if (!params.range) {
|
|
503
|
-
throw new ToolingError('validation_error', 'Missing required field: range', { params })
|
|
504
|
-
}
|
|
505
|
-
|
|
506
|
-
try {
|
|
507
|
-
const response = await sheets.spreadsheets.values.clear({
|
|
508
|
-
spreadsheetId: params.spreadsheetId,
|
|
509
|
-
range: params.range,
|
|
510
|
-
requestBody: {} // Must be empty object, not undefined
|
|
511
|
-
})
|
|
512
|
-
|
|
513
|
-
if (context?.logger) {
|
|
514
|
-
context.logger.info(
|
|
515
|
-
`[GoogleSheetsAdapter] Range cleared: organizationId=${context.organizationId} executionId=${context.executionId} spreadsheetId=${params.spreadsheetId} clearedRange=${response.data.clearedRange}`
|
|
516
|
-
)
|
|
517
|
-
}
|
|
518
|
-
|
|
519
|
-
return {
|
|
520
|
-
clearedRange: response.data.clearedRange || params.range
|
|
521
|
-
}
|
|
522
|
-
} catch (error: any) {
|
|
523
|
-
throw this.handleApiError(error, params.spreadsheetId)
|
|
524
|
-
}
|
|
525
|
-
}
|
|
526
|
-
|
|
527
|
-
/**
|
|
528
|
-
* Get spreadsheet metadata (title, sheets)
|
|
529
|
-
*/
|
|
530
|
-
private async getSpreadsheetMetadata(
|
|
531
|
-
sheets: sheets_v4.Sheets,
|
|
532
|
-
params: GetSpreadsheetMetadataParams,
|
|
533
|
-
context?: ExecutionContext
|
|
534
|
-
): Promise<GetSpreadsheetMetadataResult> {
|
|
535
|
-
// Validate params
|
|
536
|
-
if (!params.spreadsheetId) {
|
|
537
|
-
throw new ToolingError('validation_error', 'Missing required field: spreadsheetId', { params })
|
|
538
|
-
}
|
|
539
|
-
|
|
540
|
-
try {
|
|
541
|
-
const response = await sheets.spreadsheets.get({
|
|
542
|
-
spreadsheetId: params.spreadsheetId,
|
|
543
|
-
fields: 'properties.title,sheets.properties'
|
|
544
|
-
})
|
|
545
|
-
|
|
546
|
-
if (context?.logger) {
|
|
547
|
-
context.logger.info(
|
|
548
|
-
`[GoogleSheetsAdapter] Metadata retrieved: organizationId=${context.organizationId} executionId=${context.executionId} spreadsheetId=${params.spreadsheetId} title=${response.data.properties?.title} sheetCount=${response.data.sheets?.length}`
|
|
549
|
-
)
|
|
550
|
-
}
|
|
551
|
-
|
|
552
|
-
return {
|
|
553
|
-
title: response.data.properties?.title || '',
|
|
554
|
-
sheets:
|
|
555
|
-
response.data.sheets?.map((s) => ({
|
|
556
|
-
sheetId: s.properties?.sheetId ?? 0,
|
|
557
|
-
title: s.properties?.title ?? '',
|
|
558
|
-
index: s.properties?.index ?? 0
|
|
559
|
-
})) || []
|
|
560
|
-
}
|
|
561
|
-
} catch (error: any) {
|
|
562
|
-
throw this.handleApiError(error, params.spreadsheetId)
|
|
563
|
-
}
|
|
564
|
-
}
|
|
565
|
-
|
|
566
|
-
/**
|
|
567
|
-
* Batch update multiple ranges in a single request
|
|
568
|
-
*/
|
|
569
|
-
private async batchUpdate(
|
|
570
|
-
sheets: sheets_v4.Sheets,
|
|
571
|
-
params: BatchUpdateParams,
|
|
572
|
-
context?: ExecutionContext
|
|
573
|
-
): Promise<BatchUpdateResult> {
|
|
574
|
-
// Validate params
|
|
575
|
-
if (!params.spreadsheetId) {
|
|
576
|
-
throw new ToolingError('validation_error', 'Missing required field: spreadsheetId', { params })
|
|
577
|
-
}
|
|
578
|
-
if (!params.data || !Array.isArray(params.data) || params.data.length === 0) {
|
|
579
|
-
throw new ToolingError('validation_error', 'Missing required field: data (must be non-empty array)', { params })
|
|
580
|
-
}
|
|
581
|
-
|
|
582
|
-
try {
|
|
583
|
-
const response = await sheets.spreadsheets.values.batchUpdate({
|
|
584
|
-
spreadsheetId: params.spreadsheetId,
|
|
585
|
-
requestBody: {
|
|
586
|
-
valueInputOption: 'USER_ENTERED',
|
|
587
|
-
data: params.data.map((d) => ({ range: d.range, values: d.values })),
|
|
588
|
-
includeValuesInResponse: false
|
|
589
|
-
}
|
|
590
|
-
})
|
|
591
|
-
|
|
592
|
-
if (context?.logger) {
|
|
593
|
-
context.logger.info(
|
|
594
|
-
`[GoogleSheetsAdapter] Batch update completed: organizationId=${context.organizationId} executionId=${context.executionId} spreadsheetId=${params.spreadsheetId} totalUpdatedCells=${response.data.totalUpdatedCells} totalUpdatedRows=${response.data.totalUpdatedRows}`
|
|
595
|
-
)
|
|
596
|
-
}
|
|
597
|
-
|
|
598
|
-
return {
|
|
599
|
-
totalUpdatedCells: response.data.totalUpdatedCells || 0,
|
|
600
|
-
totalUpdatedRows: response.data.totalUpdatedRows || 0
|
|
601
|
-
}
|
|
602
|
-
} catch (error: any) {
|
|
603
|
-
throw this.handleApiError(error, params.spreadsheetId)
|
|
604
|
-
}
|
|
605
|
-
}
|
|
606
|
-
|
|
607
|
-
// ============================================
|
|
608
|
-
// Workflow-friendly methods
|
|
609
|
-
// ============================================
|
|
610
|
-
|
|
611
|
-
/**
|
|
612
|
-
* Get column headers from first row
|
|
613
|
-
*/
|
|
614
|
-
private async getHeaders(
|
|
615
|
-
sheets: sheets_v4.Sheets,
|
|
616
|
-
params: GetHeadersParams,
|
|
617
|
-
context?: ExecutionContext
|
|
618
|
-
): Promise<GetHeadersResult> {
|
|
619
|
-
if (!params.spreadsheetId) {
|
|
620
|
-
throw new ToolingError('validation_error', 'Missing required field: spreadsheetId', { params })
|
|
621
|
-
}
|
|
622
|
-
if (!params.sheetName) {
|
|
623
|
-
throw new ToolingError('validation_error', 'Missing required field: sheetName', { params })
|
|
624
|
-
}
|
|
625
|
-
|
|
626
|
-
try {
|
|
627
|
-
const response = await sheets.spreadsheets.values.get({
|
|
628
|
-
spreadsheetId: params.spreadsheetId,
|
|
629
|
-
range: `'${params.sheetName}'!1:1`
|
|
630
|
-
})
|
|
631
|
-
|
|
632
|
-
const headers = (response.data.values?.[0] as string[]) || []
|
|
633
|
-
|
|
634
|
-
// Build column map: header name -> column letter
|
|
635
|
-
const columnMap: Record<string, string> = {}
|
|
636
|
-
for (let i = 0; i < headers.length; i++) {
|
|
637
|
-
columnMap[headers[i]] = this.indexToColumnLetter(i)
|
|
638
|
-
}
|
|
639
|
-
|
|
640
|
-
if (context?.logger) {
|
|
641
|
-
context.logger.info(
|
|
642
|
-
`[GoogleSheetsAdapter] Headers retrieved: organizationId=${context.organizationId} executionId=${context.executionId} spreadsheetId=${params.spreadsheetId} sheetName=${params.sheetName} columnCount=${headers.length}`
|
|
643
|
-
)
|
|
644
|
-
}
|
|
645
|
-
|
|
646
|
-
return {
|
|
647
|
-
headers,
|
|
648
|
-
columnCount: headers.length,
|
|
649
|
-
columnMap
|
|
650
|
-
}
|
|
651
|
-
} catch (error: any) {
|
|
652
|
-
throw this.handleApiError(error, params.spreadsheetId)
|
|
653
|
-
}
|
|
654
|
-
}
|
|
655
|
-
|
|
656
|
-
/**
|
|
657
|
-
* Find the last row with data in a column
|
|
658
|
-
*/
|
|
659
|
-
private async getLastRow(
|
|
660
|
-
sheets: sheets_v4.Sheets,
|
|
661
|
-
params: GetLastRowParams,
|
|
662
|
-
context?: ExecutionContext
|
|
663
|
-
): Promise<GetLastRowResult> {
|
|
664
|
-
if (!params.spreadsheetId) {
|
|
665
|
-
throw new ToolingError('validation_error', 'Missing required field: spreadsheetId', { params })
|
|
666
|
-
}
|
|
667
|
-
if (!params.sheetName) {
|
|
668
|
-
throw new ToolingError('validation_error', 'Missing required field: sheetName', { params })
|
|
669
|
-
}
|
|
670
|
-
|
|
671
|
-
const column = params.column || 'A'
|
|
672
|
-
|
|
673
|
-
try {
|
|
674
|
-
const response = await sheets.spreadsheets.values.get({
|
|
675
|
-
spreadsheetId: params.spreadsheetId,
|
|
676
|
-
range: `'${params.sheetName}'!${column}:${column}`
|
|
677
|
-
})
|
|
678
|
-
|
|
679
|
-
const values = response.data.values || []
|
|
680
|
-
const lastRow = values.length
|
|
681
|
-
|
|
682
|
-
if (context?.logger) {
|
|
683
|
-
context.logger.info(
|
|
684
|
-
`[GoogleSheetsAdapter] Last row found: organizationId=${context.organizationId} executionId=${context.executionId} spreadsheetId=${params.spreadsheetId} sheetName=${params.sheetName} column=${column} lastRow=${lastRow}`
|
|
685
|
-
)
|
|
686
|
-
}
|
|
687
|
-
|
|
688
|
-
return {
|
|
689
|
-
lastRow,
|
|
690
|
-
isEmpty: lastRow === 0
|
|
691
|
-
}
|
|
692
|
-
} catch (error: any) {
|
|
693
|
-
throw this.handleApiError(error, params.spreadsheetId)
|
|
694
|
-
}
|
|
695
|
-
}
|
|
696
|
-
|
|
697
|
-
/**
|
|
698
|
-
* Find a row by searching for a value in a column
|
|
699
|
-
*/
|
|
700
|
-
private async getRowByValue(
|
|
701
|
-
sheets: sheets_v4.Sheets,
|
|
702
|
-
params: GetRowByValueParams,
|
|
703
|
-
context?: ExecutionContext
|
|
704
|
-
): Promise<GetRowByValueResult> {
|
|
705
|
-
if (!params.spreadsheetId) {
|
|
706
|
-
throw new ToolingError('validation_error', 'Missing required field: spreadsheetId', { params })
|
|
707
|
-
}
|
|
708
|
-
if (!params.sheetName) {
|
|
709
|
-
throw new ToolingError('validation_error', 'Missing required field: sheetName', { params })
|
|
710
|
-
}
|
|
711
|
-
if (!params.searchColumn) {
|
|
712
|
-
throw new ToolingError('validation_error', 'Missing required field: searchColumn', { params })
|
|
713
|
-
}
|
|
714
|
-
if (params.searchValue === undefined || params.searchValue === null) {
|
|
715
|
-
throw new ToolingError('validation_error', 'Missing required field: searchValue', { params })
|
|
716
|
-
}
|
|
717
|
-
|
|
718
|
-
try {
|
|
719
|
-
// Get headers first
|
|
720
|
-
const headersResponse = await sheets.spreadsheets.values.get({
|
|
721
|
-
spreadsheetId: params.spreadsheetId,
|
|
722
|
-
range: `'${params.sheetName}'!1:1`
|
|
723
|
-
})
|
|
724
|
-
const headers = (headersResponse.data.values?.[0] as string[]) || []
|
|
725
|
-
|
|
726
|
-
// Get all data in search column
|
|
727
|
-
const columnResponse = await sheets.spreadsheets.values.get({
|
|
728
|
-
spreadsheetId: params.spreadsheetId,
|
|
729
|
-
range: `'${params.sheetName}'!${params.searchColumn}:${params.searchColumn}`
|
|
730
|
-
})
|
|
731
|
-
const columnValues = columnResponse.data.values || []
|
|
732
|
-
|
|
733
|
-
// Find matching row (skip header row)
|
|
734
|
-
let rowNumber: number | null = null
|
|
735
|
-
for (let i = 1; i < columnValues.length; i++) {
|
|
736
|
-
if (columnValues[i]?.[0] === params.searchValue) {
|
|
737
|
-
rowNumber = i + 1 // 1-indexed
|
|
738
|
-
break
|
|
739
|
-
}
|
|
740
|
-
}
|
|
741
|
-
|
|
742
|
-
if (rowNumber === null) {
|
|
743
|
-
if (context?.logger) {
|
|
744
|
-
context.logger.info(
|
|
745
|
-
`[GoogleSheetsAdapter] Row not found: organizationId=${context.organizationId} executionId=${context.executionId} spreadsheetId=${params.spreadsheetId} sheetName=${params.sheetName} searchColumn=${params.searchColumn} searchValue=${params.searchValue}`
|
|
746
|
-
)
|
|
747
|
-
}
|
|
748
|
-
return { found: false, rowNumber: null, rowData: null, headers }
|
|
749
|
-
}
|
|
750
|
-
|
|
751
|
-
// Get the full row data
|
|
752
|
-
const rowResponse = await sheets.spreadsheets.values.get({
|
|
753
|
-
spreadsheetId: params.spreadsheetId,
|
|
754
|
-
range: `'${params.sheetName}'!${rowNumber}:${rowNumber}`
|
|
755
|
-
})
|
|
756
|
-
const rowData = (rowResponse.data.values?.[0] as string[]) || []
|
|
757
|
-
|
|
758
|
-
if (context?.logger) {
|
|
759
|
-
context.logger.info(
|
|
760
|
-
`[GoogleSheetsAdapter] Row found: organizationId=${context.organizationId} executionId=${context.executionId} spreadsheetId=${params.spreadsheetId} sheetName=${params.sheetName} rowNumber=${rowNumber}`
|
|
761
|
-
)
|
|
762
|
-
}
|
|
763
|
-
|
|
764
|
-
return { found: true, rowNumber, rowData, headers }
|
|
765
|
-
} catch (error: any) {
|
|
766
|
-
throw this.handleApiError(error, params.spreadsheetId)
|
|
767
|
-
}
|
|
768
|
-
}
|
|
769
|
-
|
|
770
|
-
/**
|
|
771
|
-
* Update a row found by searching for a value
|
|
772
|
-
*/
|
|
773
|
-
private async updateRowByValue(
|
|
774
|
-
sheets: sheets_v4.Sheets,
|
|
775
|
-
params: UpdateRowByValueParams,
|
|
776
|
-
context?: ExecutionContext
|
|
777
|
-
): Promise<UpdateRowByValueResult> {
|
|
778
|
-
if (!params.spreadsheetId) {
|
|
779
|
-
throw new ToolingError('validation_error', 'Missing required field: spreadsheetId', { params })
|
|
780
|
-
}
|
|
781
|
-
if (!params.sheetName) {
|
|
782
|
-
throw new ToolingError('validation_error', 'Missing required field: sheetName', { params })
|
|
783
|
-
}
|
|
784
|
-
if (!params.searchColumn) {
|
|
785
|
-
throw new ToolingError('validation_error', 'Missing required field: searchColumn', { params })
|
|
786
|
-
}
|
|
787
|
-
if (params.searchValue === undefined || params.searchValue === null) {
|
|
788
|
-
throw new ToolingError('validation_error', 'Missing required field: searchValue', { params })
|
|
789
|
-
}
|
|
790
|
-
if (!params.updates || Object.keys(params.updates).length === 0) {
|
|
791
|
-
throw new ToolingError('validation_error', 'Missing required field: updates', { params })
|
|
792
|
-
}
|
|
793
|
-
|
|
794
|
-
try {
|
|
795
|
-
// Find the row first
|
|
796
|
-
const findResult = await this.getRowByValue(
|
|
797
|
-
sheets,
|
|
798
|
-
{
|
|
799
|
-
spreadsheetId: params.spreadsheetId,
|
|
800
|
-
sheetName: params.sheetName,
|
|
801
|
-
searchColumn: params.searchColumn,
|
|
802
|
-
searchValue: params.searchValue
|
|
803
|
-
},
|
|
804
|
-
context
|
|
805
|
-
)
|
|
806
|
-
|
|
807
|
-
if (!findResult.found || findResult.rowNumber === null) {
|
|
808
|
-
return { found: false, rowNumber: null, updatedCells: 0 }
|
|
809
|
-
}
|
|
810
|
-
|
|
811
|
-
// Build the row data with updates
|
|
812
|
-
const headers = findResult.headers
|
|
813
|
-
const rowData = findResult.rowData || []
|
|
814
|
-
const newRowData = [...rowData]
|
|
815
|
-
|
|
816
|
-
// Extend array if needed
|
|
817
|
-
while (newRowData.length < headers.length) {
|
|
818
|
-
newRowData.push('')
|
|
819
|
-
}
|
|
820
|
-
|
|
821
|
-
// Apply updates by header name
|
|
822
|
-
for (const [headerName, value] of Object.entries(params.updates)) {
|
|
823
|
-
const colIndex = headers.indexOf(headerName)
|
|
824
|
-
if (colIndex >= 0) {
|
|
825
|
-
newRowData[colIndex] = value
|
|
826
|
-
}
|
|
827
|
-
}
|
|
828
|
-
|
|
829
|
-
// Write back the row
|
|
830
|
-
const response = await sheets.spreadsheets.values.update({
|
|
831
|
-
spreadsheetId: params.spreadsheetId,
|
|
832
|
-
range: `'${params.sheetName}'!${findResult.rowNumber}:${findResult.rowNumber}`,
|
|
833
|
-
valueInputOption: 'USER_ENTERED',
|
|
834
|
-
requestBody: { values: [newRowData] }
|
|
835
|
-
})
|
|
836
|
-
|
|
837
|
-
if (context?.logger) {
|
|
838
|
-
context.logger.info(
|
|
839
|
-
`[GoogleSheetsAdapter] Row updated: organizationId=${context.organizationId} executionId=${context.executionId} spreadsheetId=${params.spreadsheetId} sheetName=${params.sheetName} rowNumber=${findResult.rowNumber} updatedCells=${response.data.updatedCells}`
|
|
840
|
-
)
|
|
841
|
-
}
|
|
842
|
-
|
|
843
|
-
return {
|
|
844
|
-
found: true,
|
|
845
|
-
rowNumber: findResult.rowNumber,
|
|
846
|
-
updatedCells: response.data.updatedCells || 0
|
|
847
|
-
}
|
|
848
|
-
} catch (error: any) {
|
|
849
|
-
throw this.handleApiError(error, params.spreadsheetId)
|
|
850
|
-
}
|
|
851
|
-
}
|
|
852
|
-
|
|
853
|
-
/**
|
|
854
|
-
* Insert or update a row based on key column match
|
|
855
|
-
*/
|
|
856
|
-
private async upsertRow(
|
|
857
|
-
sheets: sheets_v4.Sheets,
|
|
858
|
-
params: UpsertRowParams,
|
|
859
|
-
context?: ExecutionContext
|
|
860
|
-
): Promise<UpsertRowResult> {
|
|
861
|
-
if (!params.spreadsheetId) {
|
|
862
|
-
throw new ToolingError('validation_error', 'Missing required field: spreadsheetId', { params })
|
|
863
|
-
}
|
|
864
|
-
if (!params.sheetName) {
|
|
865
|
-
throw new ToolingError('validation_error', 'Missing required field: sheetName', { params })
|
|
866
|
-
}
|
|
867
|
-
if (!params.keyColumn) {
|
|
868
|
-
throw new ToolingError('validation_error', 'Missing required field: keyColumn', { params })
|
|
869
|
-
}
|
|
870
|
-
if (params.keyValue === undefined || params.keyValue === null) {
|
|
871
|
-
throw new ToolingError('validation_error', 'Missing required field: keyValue', { params })
|
|
872
|
-
}
|
|
873
|
-
if (!params.rowData || Object.keys(params.rowData).length === 0) {
|
|
874
|
-
throw new ToolingError('validation_error', 'Missing required field: rowData', { params })
|
|
875
|
-
}
|
|
876
|
-
|
|
877
|
-
try {
|
|
878
|
-
// Try to find existing row
|
|
879
|
-
const findResult = await this.getRowByValue(
|
|
880
|
-
sheets,
|
|
881
|
-
{
|
|
882
|
-
spreadsheetId: params.spreadsheetId,
|
|
883
|
-
sheetName: params.sheetName,
|
|
884
|
-
searchColumn: params.keyColumn,
|
|
885
|
-
searchValue: params.keyValue
|
|
886
|
-
},
|
|
887
|
-
context
|
|
888
|
-
)
|
|
889
|
-
|
|
890
|
-
const headers = findResult.headers
|
|
891
|
-
|
|
892
|
-
if (findResult.found && findResult.rowNumber !== null) {
|
|
893
|
-
// Update existing row
|
|
894
|
-
const updateResult = await this.updateRowByValue(
|
|
895
|
-
sheets,
|
|
896
|
-
{
|
|
897
|
-
spreadsheetId: params.spreadsheetId,
|
|
898
|
-
sheetName: params.sheetName,
|
|
899
|
-
searchColumn: params.keyColumn,
|
|
900
|
-
searchValue: params.keyValue,
|
|
901
|
-
updates: params.rowData
|
|
902
|
-
},
|
|
903
|
-
context
|
|
904
|
-
)
|
|
905
|
-
|
|
906
|
-
return {
|
|
907
|
-
action: 'updated',
|
|
908
|
-
rowNumber: findResult.rowNumber,
|
|
909
|
-
updatedCells: updateResult.updatedCells
|
|
910
|
-
}
|
|
911
|
-
} else {
|
|
912
|
-
// Insert new row - build row array from headers
|
|
913
|
-
const newRow: string[] = headers.map((h) => params.rowData[h] || '')
|
|
914
|
-
|
|
915
|
-
// Get last row to know where to append
|
|
916
|
-
const lastRowResult = await this.getLastRow(
|
|
917
|
-
sheets,
|
|
918
|
-
{
|
|
919
|
-
spreadsheetId: params.spreadsheetId,
|
|
920
|
-
sheetName: params.sheetName,
|
|
921
|
-
column: 'A'
|
|
922
|
-
},
|
|
923
|
-
context
|
|
924
|
-
)
|
|
925
|
-
|
|
926
|
-
const newRowNumber = lastRowResult.lastRow + 1
|
|
927
|
-
|
|
928
|
-
const response = await sheets.spreadsheets.values.update({
|
|
929
|
-
spreadsheetId: params.spreadsheetId,
|
|
930
|
-
range: `'${params.sheetName}'!${newRowNumber}:${newRowNumber}`,
|
|
931
|
-
valueInputOption: 'USER_ENTERED',
|
|
932
|
-
requestBody: { values: [newRow] }
|
|
933
|
-
})
|
|
934
|
-
|
|
935
|
-
if (context?.logger) {
|
|
936
|
-
context.logger.info(
|
|
937
|
-
`[GoogleSheetsAdapter] Row inserted: organizationId=${context.organizationId} executionId=${context.executionId} spreadsheetId=${params.spreadsheetId} sheetName=${params.sheetName} rowNumber=${newRowNumber}`
|
|
938
|
-
)
|
|
939
|
-
}
|
|
940
|
-
|
|
941
|
-
return {
|
|
942
|
-
action: 'inserted',
|
|
943
|
-
rowNumber: newRowNumber,
|
|
944
|
-
updatedCells: response.data.updatedCells || 0
|
|
945
|
-
}
|
|
946
|
-
}
|
|
947
|
-
} catch (error: any) {
|
|
948
|
-
throw this.handleApiError(error, params.spreadsheetId)
|
|
949
|
-
}
|
|
950
|
-
}
|
|
951
|
-
|
|
952
|
-
/**
|
|
953
|
-
* Get all rows matching a filter condition
|
|
954
|
-
*/
|
|
955
|
-
private async filterRows(
|
|
956
|
-
sheets: sheets_v4.Sheets,
|
|
957
|
-
params: FilterRowsParams,
|
|
958
|
-
context?: ExecutionContext
|
|
959
|
-
): Promise<FilterRowsResult> {
|
|
960
|
-
if (!params.spreadsheetId) {
|
|
961
|
-
throw new ToolingError('validation_error', 'Missing required field: spreadsheetId', { params })
|
|
962
|
-
}
|
|
963
|
-
if (!params.sheetName) {
|
|
964
|
-
throw new ToolingError('validation_error', 'Missing required field: sheetName', { params })
|
|
965
|
-
}
|
|
966
|
-
if (!params.filterColumn) {
|
|
967
|
-
throw new ToolingError('validation_error', 'Missing required field: filterColumn', { params })
|
|
968
|
-
}
|
|
969
|
-
if (params.filterValue === undefined || params.filterValue === null) {
|
|
970
|
-
throw new ToolingError('validation_error', 'Missing required field: filterValue', { params })
|
|
971
|
-
}
|
|
972
|
-
|
|
973
|
-
const operator = params.operator || 'equals'
|
|
974
|
-
|
|
975
|
-
try {
|
|
976
|
-
// Get all data
|
|
977
|
-
const response = await sheets.spreadsheets.values.get({
|
|
978
|
-
spreadsheetId: params.spreadsheetId,
|
|
979
|
-
range: `'${params.sheetName}'`
|
|
980
|
-
})
|
|
981
|
-
|
|
982
|
-
const allData = (response.data.values as string[][]) || []
|
|
983
|
-
if (allData.length === 0) {
|
|
984
|
-
return { rows: [], matchCount: 0, headers: [] }
|
|
985
|
-
}
|
|
986
|
-
|
|
987
|
-
const headers = allData[0]
|
|
988
|
-
const colIndex = this.columnLetterToIndex(params.filterColumn)
|
|
989
|
-
|
|
990
|
-
// Filter rows (skip header)
|
|
991
|
-
const matchingRows: Array<{ rowNumber: number; data: string[] }> = []
|
|
992
|
-
|
|
993
|
-
for (let i = 1; i < allData.length; i++) {
|
|
994
|
-
const cellValue = allData[i]?.[colIndex] || ''
|
|
995
|
-
let matches = false
|
|
996
|
-
|
|
997
|
-
switch (operator) {
|
|
998
|
-
case 'equals':
|
|
999
|
-
matches = cellValue === params.filterValue
|
|
1000
|
-
break
|
|
1001
|
-
case 'contains':
|
|
1002
|
-
matches = cellValue.includes(params.filterValue)
|
|
1003
|
-
break
|
|
1004
|
-
case 'startsWith':
|
|
1005
|
-
matches = cellValue.startsWith(params.filterValue)
|
|
1006
|
-
break
|
|
1007
|
-
case 'endsWith':
|
|
1008
|
-
matches = cellValue.endsWith(params.filterValue)
|
|
1009
|
-
break
|
|
1010
|
-
}
|
|
1011
|
-
|
|
1012
|
-
if (matches) {
|
|
1013
|
-
matchingRows.push({
|
|
1014
|
-
rowNumber: i + 1, // 1-indexed
|
|
1015
|
-
data: allData[i]
|
|
1016
|
-
})
|
|
1017
|
-
}
|
|
1018
|
-
}
|
|
1019
|
-
|
|
1020
|
-
if (context?.logger) {
|
|
1021
|
-
context.logger.info(
|
|
1022
|
-
`[GoogleSheetsAdapter] Filter completed: organizationId=${context.organizationId} executionId=${context.executionId} spreadsheetId=${params.spreadsheetId} sheetName=${params.sheetName} matchCount=${matchingRows.length}`
|
|
1023
|
-
)
|
|
1024
|
-
}
|
|
1025
|
-
|
|
1026
|
-
return {
|
|
1027
|
-
rows: matchingRows,
|
|
1028
|
-
matchCount: matchingRows.length,
|
|
1029
|
-
headers
|
|
1030
|
-
}
|
|
1031
|
-
} catch (error: any) {
|
|
1032
|
-
throw this.handleApiError(error, params.spreadsheetId)
|
|
1033
|
-
}
|
|
1034
|
-
}
|
|
1035
|
-
|
|
1036
|
-
/**
|
|
1037
|
-
* Delete a row by searching for a value
|
|
1038
|
-
*/
|
|
1039
|
-
private async deleteRowByValue(
|
|
1040
|
-
sheets: sheets_v4.Sheets,
|
|
1041
|
-
params: DeleteRowByValueParams,
|
|
1042
|
-
context?: ExecutionContext
|
|
1043
|
-
): Promise<DeleteRowByValueResult> {
|
|
1044
|
-
if (!params.spreadsheetId) {
|
|
1045
|
-
throw new ToolingError('validation_error', 'Missing required field: spreadsheetId', { params })
|
|
1046
|
-
}
|
|
1047
|
-
if (!params.sheetName) {
|
|
1048
|
-
throw new ToolingError('validation_error', 'Missing required field: sheetName', { params })
|
|
1049
|
-
}
|
|
1050
|
-
if (!params.searchColumn) {
|
|
1051
|
-
throw new ToolingError('validation_error', 'Missing required field: searchColumn', { params })
|
|
1052
|
-
}
|
|
1053
|
-
if (params.searchValue === undefined || params.searchValue === null) {
|
|
1054
|
-
throw new ToolingError('validation_error', 'Missing required field: searchValue', { params })
|
|
1055
|
-
}
|
|
1056
|
-
|
|
1057
|
-
try {
|
|
1058
|
-
// Find the row first
|
|
1059
|
-
const findResult = await this.getRowByValue(
|
|
1060
|
-
sheets,
|
|
1061
|
-
{
|
|
1062
|
-
spreadsheetId: params.spreadsheetId,
|
|
1063
|
-
sheetName: params.sheetName,
|
|
1064
|
-
searchColumn: params.searchColumn,
|
|
1065
|
-
searchValue: params.searchValue
|
|
1066
|
-
},
|
|
1067
|
-
context
|
|
1068
|
-
)
|
|
1069
|
-
|
|
1070
|
-
if (!findResult.found || findResult.rowNumber === null) {
|
|
1071
|
-
return { found: false, deletedRowNumber: null }
|
|
1072
|
-
}
|
|
1073
|
-
|
|
1074
|
-
// Get sheet ID for the delete request
|
|
1075
|
-
const metadataResponse = await sheets.spreadsheets.get({
|
|
1076
|
-
spreadsheetId: params.spreadsheetId,
|
|
1077
|
-
fields: 'sheets.properties'
|
|
1078
|
-
})
|
|
1079
|
-
|
|
1080
|
-
const sheetInfo = metadataResponse.data.sheets?.find((s) => s.properties?.title === params.sheetName)
|
|
1081
|
-
|
|
1082
|
-
if (!sheetInfo?.properties || sheetInfo.properties.sheetId === undefined) {
|
|
1083
|
-
throw new ToolingError('api_error', `Sheet not found: ${params.sheetName}`, {
|
|
1084
|
-
spreadsheetId: params.spreadsheetId,
|
|
1085
|
-
sheetName: params.sheetName
|
|
1086
|
-
})
|
|
1087
|
-
}
|
|
1088
|
-
|
|
1089
|
-
// Delete the row using batchUpdate
|
|
1090
|
-
await sheets.spreadsheets.batchUpdate({
|
|
1091
|
-
spreadsheetId: params.spreadsheetId,
|
|
1092
|
-
requestBody: {
|
|
1093
|
-
requests: [
|
|
1094
|
-
{
|
|
1095
|
-
deleteDimension: {
|
|
1096
|
-
range: {
|
|
1097
|
-
sheetId: sheetInfo.properties.sheetId,
|
|
1098
|
-
dimension: 'ROWS',
|
|
1099
|
-
startIndex: findResult.rowNumber - 1, // 0-indexed
|
|
1100
|
-
endIndex: findResult.rowNumber // exclusive
|
|
1101
|
-
}
|
|
1102
|
-
}
|
|
1103
|
-
}
|
|
1104
|
-
]
|
|
1105
|
-
}
|
|
1106
|
-
})
|
|
1107
|
-
|
|
1108
|
-
if (context?.logger) {
|
|
1109
|
-
context.logger.info(
|
|
1110
|
-
`[GoogleSheetsAdapter] Row deleted: organizationId=${context.organizationId} executionId=${context.executionId} spreadsheetId=${params.spreadsheetId} sheetName=${params.sheetName} deletedRowNumber=${findResult.rowNumber}`
|
|
1111
|
-
)
|
|
1112
|
-
}
|
|
1113
|
-
|
|
1114
|
-
return { found: true, deletedRowNumber: findResult.rowNumber }
|
|
1115
|
-
} catch (error: any) {
|
|
1116
|
-
throw this.handleApiError(error, params.spreadsheetId)
|
|
1117
|
-
}
|
|
1118
|
-
}
|
|
1119
|
-
|
|
1120
|
-
/**
|
|
1121
|
-
* Convert column letter to 0-based index (A=0, B=1, ..., Z=25, AA=26, etc.)
|
|
1122
|
-
*/
|
|
1123
|
-
private columnLetterToIndex(letter: string): number {
|
|
1124
|
-
let index = 0
|
|
1125
|
-
for (let i = 0; i < letter.length; i++) {
|
|
1126
|
-
index = index * 26 + (letter.charCodeAt(i) - 64)
|
|
1127
|
-
}
|
|
1128
|
-
return index - 1
|
|
1129
|
-
}
|
|
1130
|
-
|
|
1131
|
-
/**
|
|
1132
|
-
* Convert 0-based index to column letter (0=A, 1=B, ..., 25=Z, 26=AA, etc.)
|
|
1133
|
-
*/
|
|
1134
|
-
private indexToColumnLetter(index: number): string {
|
|
1135
|
-
let letter = ''
|
|
1136
|
-
let temp = index
|
|
1137
|
-
while (temp >= 0) {
|
|
1138
|
-
letter = String.fromCharCode((temp % 26) + 65) + letter
|
|
1139
|
-
temp = Math.floor(temp / 26) - 1
|
|
1140
|
-
}
|
|
1141
|
-
return letter
|
|
1142
|
-
}
|
|
1143
|
-
|
|
1144
|
-
/**
|
|
1145
|
-
* Handle Google Sheets API errors with meaningful messages
|
|
1146
|
-
*/
|
|
1147
|
-
private handleApiError(error: any, spreadsheetId: string): ToolingError {
|
|
1148
|
-
const code = error.code || error.status || error.response?.status
|
|
1149
|
-
|
|
1150
|
-
if (code === 404) {
|
|
1151
|
-
return new ToolingError('api_error', `Spreadsheet not found: ${spreadsheetId}`, {
|
|
1152
|
-
spreadsheetId,
|
|
1153
|
-
statusCode: 404
|
|
1154
|
-
})
|
|
1155
|
-
}
|
|
1156
|
-
if (code === 403) {
|
|
1157
|
-
return new ToolingError('permission_denied', 'Access denied. Check sharing permissions.', {
|
|
1158
|
-
spreadsheetId,
|
|
1159
|
-
statusCode: 403
|
|
1160
|
-
})
|
|
1161
|
-
}
|
|
1162
|
-
if (code === 401) {
|
|
1163
|
-
return new ToolingError('credentials_invalid', 'Authentication expired. Token refresh required.', {
|
|
1164
|
-
spreadsheetId,
|
|
1165
|
-
statusCode: 401
|
|
1166
|
-
})
|
|
1167
|
-
}
|
|
1168
|
-
if (code === 429) {
|
|
1169
|
-
return new ToolingError('rate_limit_exceeded', 'Rate limited. Retry after delay.', {
|
|
1170
|
-
spreadsheetId,
|
|
1171
|
-
statusCode: 429
|
|
1172
|
-
})
|
|
1173
|
-
}
|
|
1174
|
-
|
|
1175
|
-
return new ToolingError('api_error', `Google Sheets API error: ${error.message}`, {
|
|
1176
|
-
spreadsheetId,
|
|
1177
|
-
statusCode: code,
|
|
1178
|
-
details: error
|
|
1179
|
-
})
|
|
1180
|
-
}
|
|
1181
|
-
|
|
1182
|
-
/**
|
|
1183
|
-
* Validate credentials structure
|
|
1184
|
-
* Required by BaseIntegrationAdapter interface
|
|
1185
|
-
*/
|
|
1186
|
-
validateCredentials(creds: Record<string, unknown>): boolean {
|
|
1187
|
-
return typeof creds.accessToken === 'string' && creds.accessToken.length > 0
|
|
1188
|
-
}
|
|
1189
|
-
}
|
|
1
|
+
import type { sheets_v4 } from '@googleapis/sheets'
|
|
2
|
+
import type { BaseIntegrationAdapter } from '../../../base-integration-adapter'
|
|
3
|
+
import { ToolingError } from '../../../../types'
|
|
4
|
+
import type { ExecutionContext } from '../../../../../base/types'
|
|
5
|
+
import { getOAuthCredentials } from '../../../../../../../integrations/oauth/server/credentials'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Google Sheets credentials format
|
|
9
|
+
* Stored in credentials table, encrypted
|
|
10
|
+
*/
|
|
11
|
+
interface GoogleSheetsCredentials {
|
|
12
|
+
accessToken: string
|
|
13
|
+
refreshToken?: string
|
|
14
|
+
expiresAt?: string
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Sheet info returned from metadata calls
|
|
19
|
+
*/
|
|
20
|
+
interface SheetInfo {
|
|
21
|
+
sheetId: number
|
|
22
|
+
title: string
|
|
23
|
+
index: number
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Read sheet parameters
|
|
28
|
+
*/
|
|
29
|
+
interface ReadSheetParams {
|
|
30
|
+
spreadsheetId: string
|
|
31
|
+
range: string
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Read sheet result
|
|
36
|
+
*/
|
|
37
|
+
interface ReadSheetResult {
|
|
38
|
+
values: string[][]
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Write sheet parameters
|
|
43
|
+
*/
|
|
44
|
+
interface WriteSheetParams {
|
|
45
|
+
spreadsheetId: string
|
|
46
|
+
range: string
|
|
47
|
+
values: string[][]
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Write sheet result
|
|
52
|
+
*/
|
|
53
|
+
interface WriteSheetResult {
|
|
54
|
+
updatedCells: number
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Append rows parameters
|
|
59
|
+
*/
|
|
60
|
+
interface AppendRowsParams {
|
|
61
|
+
spreadsheetId: string
|
|
62
|
+
range: string
|
|
63
|
+
values: string[][]
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Append rows result
|
|
68
|
+
*/
|
|
69
|
+
interface AppendRowsResult {
|
|
70
|
+
updatedRows: number
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Clear range parameters
|
|
75
|
+
*/
|
|
76
|
+
interface ClearRangeParams {
|
|
77
|
+
spreadsheetId: string
|
|
78
|
+
range: string
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Clear range result
|
|
83
|
+
*/
|
|
84
|
+
interface ClearRangeResult {
|
|
85
|
+
clearedRange: string
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Get spreadsheet metadata parameters
|
|
90
|
+
*/
|
|
91
|
+
interface GetSpreadsheetMetadataParams {
|
|
92
|
+
spreadsheetId: string
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Get spreadsheet metadata result
|
|
97
|
+
*/
|
|
98
|
+
interface GetSpreadsheetMetadataResult {
|
|
99
|
+
title: string
|
|
100
|
+
sheets: SheetInfo[]
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Batch update parameters
|
|
105
|
+
*/
|
|
106
|
+
interface BatchUpdateParams {
|
|
107
|
+
spreadsheetId: string
|
|
108
|
+
data: Array<{ range: string; values: string[][] }>
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Batch update result
|
|
113
|
+
*/
|
|
114
|
+
interface BatchUpdateResult {
|
|
115
|
+
totalUpdatedCells: number
|
|
116
|
+
totalUpdatedRows: number
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// ============================================
|
|
120
|
+
// Workflow-friendly method interfaces
|
|
121
|
+
// ============================================
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Get headers parameters
|
|
125
|
+
*/
|
|
126
|
+
interface GetHeadersParams {
|
|
127
|
+
spreadsheetId: string
|
|
128
|
+
sheetName: string
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Get headers result
|
|
133
|
+
*/
|
|
134
|
+
interface GetHeadersResult {
|
|
135
|
+
headers: string[]
|
|
136
|
+
columnCount: number
|
|
137
|
+
columnMap: Record<string, string> // header name -> column letter (e.g., { Name: 'A', Id: 'B' })
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Get last row parameters
|
|
142
|
+
*/
|
|
143
|
+
interface GetLastRowParams {
|
|
144
|
+
spreadsheetId: string
|
|
145
|
+
sheetName: string
|
|
146
|
+
column?: string // Optional column to check (default: A)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Get last row result
|
|
151
|
+
*/
|
|
152
|
+
interface GetLastRowResult {
|
|
153
|
+
lastRow: number
|
|
154
|
+
isEmpty: boolean
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Get row by value parameters
|
|
159
|
+
*/
|
|
160
|
+
interface GetRowByValueParams {
|
|
161
|
+
spreadsheetId: string
|
|
162
|
+
sheetName: string
|
|
163
|
+
searchColumn: string // Column letter (e.g., 'A', 'B')
|
|
164
|
+
searchValue: string
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Get row by value result
|
|
169
|
+
*/
|
|
170
|
+
interface GetRowByValueResult {
|
|
171
|
+
found: boolean
|
|
172
|
+
rowNumber: number | null
|
|
173
|
+
rowData: string[] | null
|
|
174
|
+
headers: string[]
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Update row by value parameters
|
|
179
|
+
*/
|
|
180
|
+
interface UpdateRowByValueParams {
|
|
181
|
+
spreadsheetId: string
|
|
182
|
+
sheetName: string
|
|
183
|
+
searchColumn: string
|
|
184
|
+
searchValue: string
|
|
185
|
+
updates: Record<string, string> // Column header -> new value
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Update row by value result
|
|
190
|
+
*/
|
|
191
|
+
interface UpdateRowByValueResult {
|
|
192
|
+
found: boolean
|
|
193
|
+
rowNumber: number | null
|
|
194
|
+
updatedCells: number
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Upsert row parameters
|
|
199
|
+
*/
|
|
200
|
+
interface UpsertRowParams {
|
|
201
|
+
spreadsheetId: string
|
|
202
|
+
sheetName: string
|
|
203
|
+
keyColumn: string // Column to match on (e.g., 'A' or header name)
|
|
204
|
+
keyValue: string
|
|
205
|
+
rowData: Record<string, string> // Column header -> value
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Upsert row result
|
|
210
|
+
*/
|
|
211
|
+
interface UpsertRowResult {
|
|
212
|
+
action: 'inserted' | 'updated'
|
|
213
|
+
rowNumber: number
|
|
214
|
+
updatedCells: number
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Filter rows parameters
|
|
219
|
+
*/
|
|
220
|
+
interface FilterRowsParams {
|
|
221
|
+
spreadsheetId: string
|
|
222
|
+
sheetName: string
|
|
223
|
+
filterColumn: string
|
|
224
|
+
filterValue: string
|
|
225
|
+
operator?: 'equals' | 'contains' | 'startsWith' | 'endsWith'
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Filter rows result
|
|
230
|
+
*/
|
|
231
|
+
interface FilterRowsResult {
|
|
232
|
+
rows: Array<{
|
|
233
|
+
rowNumber: number
|
|
234
|
+
data: string[]
|
|
235
|
+
}>
|
|
236
|
+
matchCount: number
|
|
237
|
+
headers: string[]
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Delete row by value parameters
|
|
242
|
+
*/
|
|
243
|
+
interface DeleteRowByValueParams {
|
|
244
|
+
spreadsheetId: string
|
|
245
|
+
sheetName: string
|
|
246
|
+
searchColumn: string
|
|
247
|
+
searchValue: string
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Delete row by value result
|
|
252
|
+
*/
|
|
253
|
+
interface DeleteRowByValueResult {
|
|
254
|
+
found: boolean
|
|
255
|
+
deletedRowNumber: number | null
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Google Sheets adapter for spreadsheet operations via Google Sheets API v4
|
|
260
|
+
*
|
|
261
|
+
* Core methods:
|
|
262
|
+
* - readSheet: Read cell range from spreadsheet
|
|
263
|
+
* - writeSheet: Write to cell range (overwrites existing data)
|
|
264
|
+
* - appendRows: Append rows to end of table
|
|
265
|
+
* - clearRange: Clear values from range (preserves formatting)
|
|
266
|
+
* - getSpreadsheetMetadata: Get spreadsheet title and sheet list
|
|
267
|
+
* - batchUpdate: Update multiple ranges in single request
|
|
268
|
+
*
|
|
269
|
+
* Workflow-friendly methods:
|
|
270
|
+
* - getHeaders: Get column headers from first row
|
|
271
|
+
* - getLastRow: Find the last row with data
|
|
272
|
+
* - getRowByValue: Find a row by searching a column
|
|
273
|
+
* - updateRowByValue: Update a row found by search
|
|
274
|
+
* - upsertRow: Insert or update a row based on key column
|
|
275
|
+
* - filterRows: Get all rows matching a condition
|
|
276
|
+
* - deleteRowByValue: Delete a row found by search
|
|
277
|
+
*
|
|
278
|
+
* @example
|
|
279
|
+
* const adapter = new GoogleSheetsAdapter()
|
|
280
|
+
* const result = await adapter.call('readSheet', {
|
|
281
|
+
* spreadsheetId: '1BxiMVs0XRA5nFMdKvBdBZjgmUUqptlbs74OgvE2upms',
|
|
282
|
+
* range: 'Sheet1!A1:C10'
|
|
283
|
+
* }, { accessToken: 'ya29...' })
|
|
284
|
+
*/
|
|
285
|
+
export class GoogleSheetsAdapter implements BaseIntegrationAdapter {
|
|
286
|
+
readonly name = 'google-sheets'
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Call Google Sheets API method
|
|
290
|
+
* @param method - Method name (e.g., 'readSheet', 'writeSheet')
|
|
291
|
+
* @param params - Method parameters
|
|
292
|
+
* @param credentials - OAuth2 access token
|
|
293
|
+
* @param context - Execution context for logging and tracing
|
|
294
|
+
*/
|
|
295
|
+
async call(
|
|
296
|
+
method: string,
|
|
297
|
+
params: unknown,
|
|
298
|
+
credentials: Record<string, unknown>,
|
|
299
|
+
context?: ExecutionContext
|
|
300
|
+
): Promise<unknown> {
|
|
301
|
+
// Validate credentials format
|
|
302
|
+
if (!this.validateCredentials(credentials)) {
|
|
303
|
+
throw new ToolingError('credentials_invalid', 'Invalid Google Sheets credentials', {
|
|
304
|
+
integration: 'google-sheets',
|
|
305
|
+
method
|
|
306
|
+
})
|
|
307
|
+
}
|
|
308
|
+
const sheetsCreds = credentials as unknown as GoogleSheetsCredentials
|
|
309
|
+
|
|
310
|
+
// Create authenticated Google Sheets client
|
|
311
|
+
const sheets = await this.createClient(sheetsCreds)
|
|
312
|
+
|
|
313
|
+
// Route to method handler
|
|
314
|
+
switch (method) {
|
|
315
|
+
// Core methods
|
|
316
|
+
case 'readSheet':
|
|
317
|
+
return this.readSheet(sheets, params as ReadSheetParams, context)
|
|
318
|
+
case 'writeSheet':
|
|
319
|
+
return this.writeSheet(sheets, params as WriteSheetParams, context)
|
|
320
|
+
case 'appendRows':
|
|
321
|
+
return this.appendRows(sheets, params as AppendRowsParams, context)
|
|
322
|
+
case 'clearRange':
|
|
323
|
+
return this.clearRange(sheets, params as ClearRangeParams, context)
|
|
324
|
+
case 'getSpreadsheetMetadata':
|
|
325
|
+
return this.getSpreadsheetMetadata(sheets, params as GetSpreadsheetMetadataParams, context)
|
|
326
|
+
case 'batchUpdate':
|
|
327
|
+
return this.batchUpdate(sheets, params as BatchUpdateParams, context)
|
|
328
|
+
// Workflow-friendly methods
|
|
329
|
+
case 'getHeaders':
|
|
330
|
+
return this.getHeaders(sheets, params as GetHeadersParams, context)
|
|
331
|
+
case 'getLastRow':
|
|
332
|
+
return this.getLastRow(sheets, params as GetLastRowParams, context)
|
|
333
|
+
case 'getRowByValue':
|
|
334
|
+
return this.getRowByValue(sheets, params as GetRowByValueParams, context)
|
|
335
|
+
case 'updateRowByValue':
|
|
336
|
+
return this.updateRowByValue(sheets, params as UpdateRowByValueParams, context)
|
|
337
|
+
case 'upsertRow':
|
|
338
|
+
return this.upsertRow(sheets, params as UpsertRowParams, context)
|
|
339
|
+
case 'filterRows':
|
|
340
|
+
return this.filterRows(sheets, params as FilterRowsParams, context)
|
|
341
|
+
case 'deleteRowByValue':
|
|
342
|
+
return this.deleteRowByValue(sheets, params as DeleteRowByValueParams, context)
|
|
343
|
+
default:
|
|
344
|
+
throw new ToolingError('method_not_found', `Unknown method: ${method}`, {
|
|
345
|
+
integration: 'google-sheets',
|
|
346
|
+
method
|
|
347
|
+
})
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* Create authenticated Google Sheets client (lazy loads @googleapis/sheets SDK)
|
|
353
|
+
* CRITICAL: Use dynamic import to prevent OOM and module-level crashes
|
|
354
|
+
*/
|
|
355
|
+
private async createClient(creds: GoogleSheetsCredentials): Promise<sheets_v4.Sheets> {
|
|
356
|
+
const { sheets } = await import('@googleapis/sheets')
|
|
357
|
+
const { OAuth2Client } = await import('google-auth-library')
|
|
358
|
+
|
|
359
|
+
// Get client credentials from environment for token refresh
|
|
360
|
+
const { clientId, clientSecret } = getOAuthCredentials('google-sheets')
|
|
361
|
+
|
|
362
|
+
const oauth2Client = new OAuth2Client(clientId, clientSecret)
|
|
363
|
+
oauth2Client.setCredentials({
|
|
364
|
+
access_token: creds.accessToken,
|
|
365
|
+
refresh_token: creds.refreshToken
|
|
366
|
+
})
|
|
367
|
+
|
|
368
|
+
return sheets({ version: 'v4', auth: oauth2Client })
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* Read data from a spreadsheet range
|
|
373
|
+
*/
|
|
374
|
+
private async readSheet(
|
|
375
|
+
sheets: sheets_v4.Sheets,
|
|
376
|
+
params: ReadSheetParams,
|
|
377
|
+
context?: ExecutionContext
|
|
378
|
+
): Promise<ReadSheetResult> {
|
|
379
|
+
// Validate params
|
|
380
|
+
if (!params.spreadsheetId) {
|
|
381
|
+
throw new ToolingError('validation_error', 'Missing required field: spreadsheetId', { params })
|
|
382
|
+
}
|
|
383
|
+
if (!params.range) {
|
|
384
|
+
throw new ToolingError('validation_error', 'Missing required field: range', { params })
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
try {
|
|
388
|
+
const response = await sheets.spreadsheets.values.get({
|
|
389
|
+
spreadsheetId: params.spreadsheetId,
|
|
390
|
+
range: params.range
|
|
391
|
+
})
|
|
392
|
+
|
|
393
|
+
if (context?.logger) {
|
|
394
|
+
context.logger.info(
|
|
395
|
+
`[GoogleSheetsAdapter] Sheet read: organizationId=${context.organizationId} executionId=${context.executionId} spreadsheetId=${params.spreadsheetId} range=${params.range} rowCount=${response.data.values?.length || 0}`
|
|
396
|
+
)
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
return {
|
|
400
|
+
values: (response.data.values as string[][]) || []
|
|
401
|
+
}
|
|
402
|
+
} catch (error: any) {
|
|
403
|
+
throw this.handleApiError(error, params.spreadsheetId)
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* Write data to a spreadsheet range (overwrites existing data)
|
|
409
|
+
*/
|
|
410
|
+
private async writeSheet(
|
|
411
|
+
sheets: sheets_v4.Sheets,
|
|
412
|
+
params: WriteSheetParams,
|
|
413
|
+
context?: ExecutionContext
|
|
414
|
+
): Promise<WriteSheetResult> {
|
|
415
|
+
// Validate params
|
|
416
|
+
if (!params.spreadsheetId) {
|
|
417
|
+
throw new ToolingError('validation_error', 'Missing required field: spreadsheetId', { params })
|
|
418
|
+
}
|
|
419
|
+
if (!params.range) {
|
|
420
|
+
throw new ToolingError('validation_error', 'Missing required field: range', { params })
|
|
421
|
+
}
|
|
422
|
+
if (!params.values || !Array.isArray(params.values)) {
|
|
423
|
+
throw new ToolingError('validation_error', 'Missing required field: values (must be 2D array)', { params })
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
try {
|
|
427
|
+
const response = await sheets.spreadsheets.values.update({
|
|
428
|
+
spreadsheetId: params.spreadsheetId,
|
|
429
|
+
range: params.range,
|
|
430
|
+
valueInputOption: 'USER_ENTERED',
|
|
431
|
+
requestBody: { values: params.values }
|
|
432
|
+
})
|
|
433
|
+
|
|
434
|
+
if (context?.logger) {
|
|
435
|
+
context.logger.info(
|
|
436
|
+
`[GoogleSheetsAdapter] Sheet written: organizationId=${context.organizationId} executionId=${context.executionId} spreadsheetId=${params.spreadsheetId} range=${params.range} updatedCells=${response.data.updatedCells}`
|
|
437
|
+
)
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
return {
|
|
441
|
+
updatedCells: response.data.updatedCells || 0
|
|
442
|
+
}
|
|
443
|
+
} catch (error: any) {
|
|
444
|
+
throw this.handleApiError(error, params.spreadsheetId)
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
/**
|
|
449
|
+
* Append rows to end of a table
|
|
450
|
+
*/
|
|
451
|
+
private async appendRows(
|
|
452
|
+
sheets: sheets_v4.Sheets,
|
|
453
|
+
params: AppendRowsParams,
|
|
454
|
+
context?: ExecutionContext
|
|
455
|
+
): Promise<AppendRowsResult> {
|
|
456
|
+
// Validate params
|
|
457
|
+
if (!params.spreadsheetId) {
|
|
458
|
+
throw new ToolingError('validation_error', 'Missing required field: spreadsheetId', { params })
|
|
459
|
+
}
|
|
460
|
+
if (!params.range) {
|
|
461
|
+
throw new ToolingError('validation_error', 'Missing required field: range', { params })
|
|
462
|
+
}
|
|
463
|
+
if (!params.values || !Array.isArray(params.values)) {
|
|
464
|
+
throw new ToolingError('validation_error', 'Missing required field: values (must be 2D array)', { params })
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
try {
|
|
468
|
+
const response = await sheets.spreadsheets.values.append({
|
|
469
|
+
spreadsheetId: params.spreadsheetId,
|
|
470
|
+
range: params.range,
|
|
471
|
+
valueInputOption: 'USER_ENTERED',
|
|
472
|
+
insertDataOption: 'INSERT_ROWS',
|
|
473
|
+
requestBody: { values: params.values }
|
|
474
|
+
})
|
|
475
|
+
|
|
476
|
+
if (context?.logger) {
|
|
477
|
+
context.logger.info(
|
|
478
|
+
`[GoogleSheetsAdapter] Rows appended: organizationId=${context.organizationId} executionId=${context.executionId} spreadsheetId=${params.spreadsheetId} range=${params.range} updatedRows=${response.data.updates?.updatedRows}`
|
|
479
|
+
)
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
return {
|
|
483
|
+
updatedRows: response.data.updates?.updatedRows || 0
|
|
484
|
+
}
|
|
485
|
+
} catch (error: any) {
|
|
486
|
+
throw this.handleApiError(error, params.spreadsheetId)
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
/**
|
|
491
|
+
* Clear values from a range (preserves formatting)
|
|
492
|
+
*/
|
|
493
|
+
private async clearRange(
|
|
494
|
+
sheets: sheets_v4.Sheets,
|
|
495
|
+
params: ClearRangeParams,
|
|
496
|
+
context?: ExecutionContext
|
|
497
|
+
): Promise<ClearRangeResult> {
|
|
498
|
+
// Validate params
|
|
499
|
+
if (!params.spreadsheetId) {
|
|
500
|
+
throw new ToolingError('validation_error', 'Missing required field: spreadsheetId', { params })
|
|
501
|
+
}
|
|
502
|
+
if (!params.range) {
|
|
503
|
+
throw new ToolingError('validation_error', 'Missing required field: range', { params })
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
try {
|
|
507
|
+
const response = await sheets.spreadsheets.values.clear({
|
|
508
|
+
spreadsheetId: params.spreadsheetId,
|
|
509
|
+
range: params.range,
|
|
510
|
+
requestBody: {} // Must be empty object, not undefined
|
|
511
|
+
})
|
|
512
|
+
|
|
513
|
+
if (context?.logger) {
|
|
514
|
+
context.logger.info(
|
|
515
|
+
`[GoogleSheetsAdapter] Range cleared: organizationId=${context.organizationId} executionId=${context.executionId} spreadsheetId=${params.spreadsheetId} clearedRange=${response.data.clearedRange}`
|
|
516
|
+
)
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
return {
|
|
520
|
+
clearedRange: response.data.clearedRange || params.range
|
|
521
|
+
}
|
|
522
|
+
} catch (error: any) {
|
|
523
|
+
throw this.handleApiError(error, params.spreadsheetId)
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
/**
|
|
528
|
+
* Get spreadsheet metadata (title, sheets)
|
|
529
|
+
*/
|
|
530
|
+
private async getSpreadsheetMetadata(
|
|
531
|
+
sheets: sheets_v4.Sheets,
|
|
532
|
+
params: GetSpreadsheetMetadataParams,
|
|
533
|
+
context?: ExecutionContext
|
|
534
|
+
): Promise<GetSpreadsheetMetadataResult> {
|
|
535
|
+
// Validate params
|
|
536
|
+
if (!params.spreadsheetId) {
|
|
537
|
+
throw new ToolingError('validation_error', 'Missing required field: spreadsheetId', { params })
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
try {
|
|
541
|
+
const response = await sheets.spreadsheets.get({
|
|
542
|
+
spreadsheetId: params.spreadsheetId,
|
|
543
|
+
fields: 'properties.title,sheets.properties'
|
|
544
|
+
})
|
|
545
|
+
|
|
546
|
+
if (context?.logger) {
|
|
547
|
+
context.logger.info(
|
|
548
|
+
`[GoogleSheetsAdapter] Metadata retrieved: organizationId=${context.organizationId} executionId=${context.executionId} spreadsheetId=${params.spreadsheetId} title=${response.data.properties?.title} sheetCount=${response.data.sheets?.length}`
|
|
549
|
+
)
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
return {
|
|
553
|
+
title: response.data.properties?.title || '',
|
|
554
|
+
sheets:
|
|
555
|
+
response.data.sheets?.map((s) => ({
|
|
556
|
+
sheetId: s.properties?.sheetId ?? 0,
|
|
557
|
+
title: s.properties?.title ?? '',
|
|
558
|
+
index: s.properties?.index ?? 0
|
|
559
|
+
})) || []
|
|
560
|
+
}
|
|
561
|
+
} catch (error: any) {
|
|
562
|
+
throw this.handleApiError(error, params.spreadsheetId)
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
/**
|
|
567
|
+
* Batch update multiple ranges in a single request
|
|
568
|
+
*/
|
|
569
|
+
private async batchUpdate(
|
|
570
|
+
sheets: sheets_v4.Sheets,
|
|
571
|
+
params: BatchUpdateParams,
|
|
572
|
+
context?: ExecutionContext
|
|
573
|
+
): Promise<BatchUpdateResult> {
|
|
574
|
+
// Validate params
|
|
575
|
+
if (!params.spreadsheetId) {
|
|
576
|
+
throw new ToolingError('validation_error', 'Missing required field: spreadsheetId', { params })
|
|
577
|
+
}
|
|
578
|
+
if (!params.data || !Array.isArray(params.data) || params.data.length === 0) {
|
|
579
|
+
throw new ToolingError('validation_error', 'Missing required field: data (must be non-empty array)', { params })
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
try {
|
|
583
|
+
const response = await sheets.spreadsheets.values.batchUpdate({
|
|
584
|
+
spreadsheetId: params.spreadsheetId,
|
|
585
|
+
requestBody: {
|
|
586
|
+
valueInputOption: 'USER_ENTERED',
|
|
587
|
+
data: params.data.map((d) => ({ range: d.range, values: d.values })),
|
|
588
|
+
includeValuesInResponse: false
|
|
589
|
+
}
|
|
590
|
+
})
|
|
591
|
+
|
|
592
|
+
if (context?.logger) {
|
|
593
|
+
context.logger.info(
|
|
594
|
+
`[GoogleSheetsAdapter] Batch update completed: organizationId=${context.organizationId} executionId=${context.executionId} spreadsheetId=${params.spreadsheetId} totalUpdatedCells=${response.data.totalUpdatedCells} totalUpdatedRows=${response.data.totalUpdatedRows}`
|
|
595
|
+
)
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
return {
|
|
599
|
+
totalUpdatedCells: response.data.totalUpdatedCells || 0,
|
|
600
|
+
totalUpdatedRows: response.data.totalUpdatedRows || 0
|
|
601
|
+
}
|
|
602
|
+
} catch (error: any) {
|
|
603
|
+
throw this.handleApiError(error, params.spreadsheetId)
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
// ============================================
|
|
608
|
+
// Workflow-friendly methods
|
|
609
|
+
// ============================================
|
|
610
|
+
|
|
611
|
+
/**
|
|
612
|
+
* Get column headers from first row
|
|
613
|
+
*/
|
|
614
|
+
private async getHeaders(
|
|
615
|
+
sheets: sheets_v4.Sheets,
|
|
616
|
+
params: GetHeadersParams,
|
|
617
|
+
context?: ExecutionContext
|
|
618
|
+
): Promise<GetHeadersResult> {
|
|
619
|
+
if (!params.spreadsheetId) {
|
|
620
|
+
throw new ToolingError('validation_error', 'Missing required field: spreadsheetId', { params })
|
|
621
|
+
}
|
|
622
|
+
if (!params.sheetName) {
|
|
623
|
+
throw new ToolingError('validation_error', 'Missing required field: sheetName', { params })
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
try {
|
|
627
|
+
const response = await sheets.spreadsheets.values.get({
|
|
628
|
+
spreadsheetId: params.spreadsheetId,
|
|
629
|
+
range: `'${params.sheetName}'!1:1`
|
|
630
|
+
})
|
|
631
|
+
|
|
632
|
+
const headers = (response.data.values?.[0] as string[]) || []
|
|
633
|
+
|
|
634
|
+
// Build column map: header name -> column letter
|
|
635
|
+
const columnMap: Record<string, string> = {}
|
|
636
|
+
for (let i = 0; i < headers.length; i++) {
|
|
637
|
+
columnMap[headers[i]] = this.indexToColumnLetter(i)
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
if (context?.logger) {
|
|
641
|
+
context.logger.info(
|
|
642
|
+
`[GoogleSheetsAdapter] Headers retrieved: organizationId=${context.organizationId} executionId=${context.executionId} spreadsheetId=${params.spreadsheetId} sheetName=${params.sheetName} columnCount=${headers.length}`
|
|
643
|
+
)
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
return {
|
|
647
|
+
headers,
|
|
648
|
+
columnCount: headers.length,
|
|
649
|
+
columnMap
|
|
650
|
+
}
|
|
651
|
+
} catch (error: any) {
|
|
652
|
+
throw this.handleApiError(error, params.spreadsheetId)
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
/**
|
|
657
|
+
* Find the last row with data in a column
|
|
658
|
+
*/
|
|
659
|
+
private async getLastRow(
|
|
660
|
+
sheets: sheets_v4.Sheets,
|
|
661
|
+
params: GetLastRowParams,
|
|
662
|
+
context?: ExecutionContext
|
|
663
|
+
): Promise<GetLastRowResult> {
|
|
664
|
+
if (!params.spreadsheetId) {
|
|
665
|
+
throw new ToolingError('validation_error', 'Missing required field: spreadsheetId', { params })
|
|
666
|
+
}
|
|
667
|
+
if (!params.sheetName) {
|
|
668
|
+
throw new ToolingError('validation_error', 'Missing required field: sheetName', { params })
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
const column = params.column || 'A'
|
|
672
|
+
|
|
673
|
+
try {
|
|
674
|
+
const response = await sheets.spreadsheets.values.get({
|
|
675
|
+
spreadsheetId: params.spreadsheetId,
|
|
676
|
+
range: `'${params.sheetName}'!${column}:${column}`
|
|
677
|
+
})
|
|
678
|
+
|
|
679
|
+
const values = response.data.values || []
|
|
680
|
+
const lastRow = values.length
|
|
681
|
+
|
|
682
|
+
if (context?.logger) {
|
|
683
|
+
context.logger.info(
|
|
684
|
+
`[GoogleSheetsAdapter] Last row found: organizationId=${context.organizationId} executionId=${context.executionId} spreadsheetId=${params.spreadsheetId} sheetName=${params.sheetName} column=${column} lastRow=${lastRow}`
|
|
685
|
+
)
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
return {
|
|
689
|
+
lastRow,
|
|
690
|
+
isEmpty: lastRow === 0
|
|
691
|
+
}
|
|
692
|
+
} catch (error: any) {
|
|
693
|
+
throw this.handleApiError(error, params.spreadsheetId)
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
/**
|
|
698
|
+
* Find a row by searching for a value in a column
|
|
699
|
+
*/
|
|
700
|
+
private async getRowByValue(
|
|
701
|
+
sheets: sheets_v4.Sheets,
|
|
702
|
+
params: GetRowByValueParams,
|
|
703
|
+
context?: ExecutionContext
|
|
704
|
+
): Promise<GetRowByValueResult> {
|
|
705
|
+
if (!params.spreadsheetId) {
|
|
706
|
+
throw new ToolingError('validation_error', 'Missing required field: spreadsheetId', { params })
|
|
707
|
+
}
|
|
708
|
+
if (!params.sheetName) {
|
|
709
|
+
throw new ToolingError('validation_error', 'Missing required field: sheetName', { params })
|
|
710
|
+
}
|
|
711
|
+
if (!params.searchColumn) {
|
|
712
|
+
throw new ToolingError('validation_error', 'Missing required field: searchColumn', { params })
|
|
713
|
+
}
|
|
714
|
+
if (params.searchValue === undefined || params.searchValue === null) {
|
|
715
|
+
throw new ToolingError('validation_error', 'Missing required field: searchValue', { params })
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
try {
|
|
719
|
+
// Get headers first
|
|
720
|
+
const headersResponse = await sheets.spreadsheets.values.get({
|
|
721
|
+
spreadsheetId: params.spreadsheetId,
|
|
722
|
+
range: `'${params.sheetName}'!1:1`
|
|
723
|
+
})
|
|
724
|
+
const headers = (headersResponse.data.values?.[0] as string[]) || []
|
|
725
|
+
|
|
726
|
+
// Get all data in search column
|
|
727
|
+
const columnResponse = await sheets.spreadsheets.values.get({
|
|
728
|
+
spreadsheetId: params.spreadsheetId,
|
|
729
|
+
range: `'${params.sheetName}'!${params.searchColumn}:${params.searchColumn}`
|
|
730
|
+
})
|
|
731
|
+
const columnValues = columnResponse.data.values || []
|
|
732
|
+
|
|
733
|
+
// Find matching row (skip header row)
|
|
734
|
+
let rowNumber: number | null = null
|
|
735
|
+
for (let i = 1; i < columnValues.length; i++) {
|
|
736
|
+
if (columnValues[i]?.[0] === params.searchValue) {
|
|
737
|
+
rowNumber = i + 1 // 1-indexed
|
|
738
|
+
break
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
if (rowNumber === null) {
|
|
743
|
+
if (context?.logger) {
|
|
744
|
+
context.logger.info(
|
|
745
|
+
`[GoogleSheetsAdapter] Row not found: organizationId=${context.organizationId} executionId=${context.executionId} spreadsheetId=${params.spreadsheetId} sheetName=${params.sheetName} searchColumn=${params.searchColumn} searchValue=${params.searchValue}`
|
|
746
|
+
)
|
|
747
|
+
}
|
|
748
|
+
return { found: false, rowNumber: null, rowData: null, headers }
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
// Get the full row data
|
|
752
|
+
const rowResponse = await sheets.spreadsheets.values.get({
|
|
753
|
+
spreadsheetId: params.spreadsheetId,
|
|
754
|
+
range: `'${params.sheetName}'!${rowNumber}:${rowNumber}`
|
|
755
|
+
})
|
|
756
|
+
const rowData = (rowResponse.data.values?.[0] as string[]) || []
|
|
757
|
+
|
|
758
|
+
if (context?.logger) {
|
|
759
|
+
context.logger.info(
|
|
760
|
+
`[GoogleSheetsAdapter] Row found: organizationId=${context.organizationId} executionId=${context.executionId} spreadsheetId=${params.spreadsheetId} sheetName=${params.sheetName} rowNumber=${rowNumber}`
|
|
761
|
+
)
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
return { found: true, rowNumber, rowData, headers }
|
|
765
|
+
} catch (error: any) {
|
|
766
|
+
throw this.handleApiError(error, params.spreadsheetId)
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
/**
|
|
771
|
+
* Update a row found by searching for a value
|
|
772
|
+
*/
|
|
773
|
+
private async updateRowByValue(
|
|
774
|
+
sheets: sheets_v4.Sheets,
|
|
775
|
+
params: UpdateRowByValueParams,
|
|
776
|
+
context?: ExecutionContext
|
|
777
|
+
): Promise<UpdateRowByValueResult> {
|
|
778
|
+
if (!params.spreadsheetId) {
|
|
779
|
+
throw new ToolingError('validation_error', 'Missing required field: spreadsheetId', { params })
|
|
780
|
+
}
|
|
781
|
+
if (!params.sheetName) {
|
|
782
|
+
throw new ToolingError('validation_error', 'Missing required field: sheetName', { params })
|
|
783
|
+
}
|
|
784
|
+
if (!params.searchColumn) {
|
|
785
|
+
throw new ToolingError('validation_error', 'Missing required field: searchColumn', { params })
|
|
786
|
+
}
|
|
787
|
+
if (params.searchValue === undefined || params.searchValue === null) {
|
|
788
|
+
throw new ToolingError('validation_error', 'Missing required field: searchValue', { params })
|
|
789
|
+
}
|
|
790
|
+
if (!params.updates || Object.keys(params.updates).length === 0) {
|
|
791
|
+
throw new ToolingError('validation_error', 'Missing required field: updates', { params })
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
try {
|
|
795
|
+
// Find the row first
|
|
796
|
+
const findResult = await this.getRowByValue(
|
|
797
|
+
sheets,
|
|
798
|
+
{
|
|
799
|
+
spreadsheetId: params.spreadsheetId,
|
|
800
|
+
sheetName: params.sheetName,
|
|
801
|
+
searchColumn: params.searchColumn,
|
|
802
|
+
searchValue: params.searchValue
|
|
803
|
+
},
|
|
804
|
+
context
|
|
805
|
+
)
|
|
806
|
+
|
|
807
|
+
if (!findResult.found || findResult.rowNumber === null) {
|
|
808
|
+
return { found: false, rowNumber: null, updatedCells: 0 }
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
// Build the row data with updates
|
|
812
|
+
const headers = findResult.headers
|
|
813
|
+
const rowData = findResult.rowData || []
|
|
814
|
+
const newRowData = [...rowData]
|
|
815
|
+
|
|
816
|
+
// Extend array if needed
|
|
817
|
+
while (newRowData.length < headers.length) {
|
|
818
|
+
newRowData.push('')
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
// Apply updates by header name
|
|
822
|
+
for (const [headerName, value] of Object.entries(params.updates)) {
|
|
823
|
+
const colIndex = headers.indexOf(headerName)
|
|
824
|
+
if (colIndex >= 0) {
|
|
825
|
+
newRowData[colIndex] = value
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
// Write back the row
|
|
830
|
+
const response = await sheets.spreadsheets.values.update({
|
|
831
|
+
spreadsheetId: params.spreadsheetId,
|
|
832
|
+
range: `'${params.sheetName}'!${findResult.rowNumber}:${findResult.rowNumber}`,
|
|
833
|
+
valueInputOption: 'USER_ENTERED',
|
|
834
|
+
requestBody: { values: [newRowData] }
|
|
835
|
+
})
|
|
836
|
+
|
|
837
|
+
if (context?.logger) {
|
|
838
|
+
context.logger.info(
|
|
839
|
+
`[GoogleSheetsAdapter] Row updated: organizationId=${context.organizationId} executionId=${context.executionId} spreadsheetId=${params.spreadsheetId} sheetName=${params.sheetName} rowNumber=${findResult.rowNumber} updatedCells=${response.data.updatedCells}`
|
|
840
|
+
)
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
return {
|
|
844
|
+
found: true,
|
|
845
|
+
rowNumber: findResult.rowNumber,
|
|
846
|
+
updatedCells: response.data.updatedCells || 0
|
|
847
|
+
}
|
|
848
|
+
} catch (error: any) {
|
|
849
|
+
throw this.handleApiError(error, params.spreadsheetId)
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
/**
|
|
854
|
+
* Insert or update a row based on key column match
|
|
855
|
+
*/
|
|
856
|
+
private async upsertRow(
|
|
857
|
+
sheets: sheets_v4.Sheets,
|
|
858
|
+
params: UpsertRowParams,
|
|
859
|
+
context?: ExecutionContext
|
|
860
|
+
): Promise<UpsertRowResult> {
|
|
861
|
+
if (!params.spreadsheetId) {
|
|
862
|
+
throw new ToolingError('validation_error', 'Missing required field: spreadsheetId', { params })
|
|
863
|
+
}
|
|
864
|
+
if (!params.sheetName) {
|
|
865
|
+
throw new ToolingError('validation_error', 'Missing required field: sheetName', { params })
|
|
866
|
+
}
|
|
867
|
+
if (!params.keyColumn) {
|
|
868
|
+
throw new ToolingError('validation_error', 'Missing required field: keyColumn', { params })
|
|
869
|
+
}
|
|
870
|
+
if (params.keyValue === undefined || params.keyValue === null) {
|
|
871
|
+
throw new ToolingError('validation_error', 'Missing required field: keyValue', { params })
|
|
872
|
+
}
|
|
873
|
+
if (!params.rowData || Object.keys(params.rowData).length === 0) {
|
|
874
|
+
throw new ToolingError('validation_error', 'Missing required field: rowData', { params })
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
try {
|
|
878
|
+
// Try to find existing row
|
|
879
|
+
const findResult = await this.getRowByValue(
|
|
880
|
+
sheets,
|
|
881
|
+
{
|
|
882
|
+
spreadsheetId: params.spreadsheetId,
|
|
883
|
+
sheetName: params.sheetName,
|
|
884
|
+
searchColumn: params.keyColumn,
|
|
885
|
+
searchValue: params.keyValue
|
|
886
|
+
},
|
|
887
|
+
context
|
|
888
|
+
)
|
|
889
|
+
|
|
890
|
+
const headers = findResult.headers
|
|
891
|
+
|
|
892
|
+
if (findResult.found && findResult.rowNumber !== null) {
|
|
893
|
+
// Update existing row
|
|
894
|
+
const updateResult = await this.updateRowByValue(
|
|
895
|
+
sheets,
|
|
896
|
+
{
|
|
897
|
+
spreadsheetId: params.spreadsheetId,
|
|
898
|
+
sheetName: params.sheetName,
|
|
899
|
+
searchColumn: params.keyColumn,
|
|
900
|
+
searchValue: params.keyValue,
|
|
901
|
+
updates: params.rowData
|
|
902
|
+
},
|
|
903
|
+
context
|
|
904
|
+
)
|
|
905
|
+
|
|
906
|
+
return {
|
|
907
|
+
action: 'updated',
|
|
908
|
+
rowNumber: findResult.rowNumber,
|
|
909
|
+
updatedCells: updateResult.updatedCells
|
|
910
|
+
}
|
|
911
|
+
} else {
|
|
912
|
+
// Insert new row - build row array from headers
|
|
913
|
+
const newRow: string[] = headers.map((h) => params.rowData[h] || '')
|
|
914
|
+
|
|
915
|
+
// Get last row to know where to append
|
|
916
|
+
const lastRowResult = await this.getLastRow(
|
|
917
|
+
sheets,
|
|
918
|
+
{
|
|
919
|
+
spreadsheetId: params.spreadsheetId,
|
|
920
|
+
sheetName: params.sheetName,
|
|
921
|
+
column: 'A'
|
|
922
|
+
},
|
|
923
|
+
context
|
|
924
|
+
)
|
|
925
|
+
|
|
926
|
+
const newRowNumber = lastRowResult.lastRow + 1
|
|
927
|
+
|
|
928
|
+
const response = await sheets.spreadsheets.values.update({
|
|
929
|
+
spreadsheetId: params.spreadsheetId,
|
|
930
|
+
range: `'${params.sheetName}'!${newRowNumber}:${newRowNumber}`,
|
|
931
|
+
valueInputOption: 'USER_ENTERED',
|
|
932
|
+
requestBody: { values: [newRow] }
|
|
933
|
+
})
|
|
934
|
+
|
|
935
|
+
if (context?.logger) {
|
|
936
|
+
context.logger.info(
|
|
937
|
+
`[GoogleSheetsAdapter] Row inserted: organizationId=${context.organizationId} executionId=${context.executionId} spreadsheetId=${params.spreadsheetId} sheetName=${params.sheetName} rowNumber=${newRowNumber}`
|
|
938
|
+
)
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
return {
|
|
942
|
+
action: 'inserted',
|
|
943
|
+
rowNumber: newRowNumber,
|
|
944
|
+
updatedCells: response.data.updatedCells || 0
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
} catch (error: any) {
|
|
948
|
+
throw this.handleApiError(error, params.spreadsheetId)
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
/**
|
|
953
|
+
* Get all rows matching a filter condition
|
|
954
|
+
*/
|
|
955
|
+
private async filterRows(
|
|
956
|
+
sheets: sheets_v4.Sheets,
|
|
957
|
+
params: FilterRowsParams,
|
|
958
|
+
context?: ExecutionContext
|
|
959
|
+
): Promise<FilterRowsResult> {
|
|
960
|
+
if (!params.spreadsheetId) {
|
|
961
|
+
throw new ToolingError('validation_error', 'Missing required field: spreadsheetId', { params })
|
|
962
|
+
}
|
|
963
|
+
if (!params.sheetName) {
|
|
964
|
+
throw new ToolingError('validation_error', 'Missing required field: sheetName', { params })
|
|
965
|
+
}
|
|
966
|
+
if (!params.filterColumn) {
|
|
967
|
+
throw new ToolingError('validation_error', 'Missing required field: filterColumn', { params })
|
|
968
|
+
}
|
|
969
|
+
if (params.filterValue === undefined || params.filterValue === null) {
|
|
970
|
+
throw new ToolingError('validation_error', 'Missing required field: filterValue', { params })
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
const operator = params.operator || 'equals'
|
|
974
|
+
|
|
975
|
+
try {
|
|
976
|
+
// Get all data
|
|
977
|
+
const response = await sheets.spreadsheets.values.get({
|
|
978
|
+
spreadsheetId: params.spreadsheetId,
|
|
979
|
+
range: `'${params.sheetName}'`
|
|
980
|
+
})
|
|
981
|
+
|
|
982
|
+
const allData = (response.data.values as string[][]) || []
|
|
983
|
+
if (allData.length === 0) {
|
|
984
|
+
return { rows: [], matchCount: 0, headers: [] }
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
const headers = allData[0]
|
|
988
|
+
const colIndex = this.columnLetterToIndex(params.filterColumn)
|
|
989
|
+
|
|
990
|
+
// Filter rows (skip header)
|
|
991
|
+
const matchingRows: Array<{ rowNumber: number; data: string[] }> = []
|
|
992
|
+
|
|
993
|
+
for (let i = 1; i < allData.length; i++) {
|
|
994
|
+
const cellValue = allData[i]?.[colIndex] || ''
|
|
995
|
+
let matches = false
|
|
996
|
+
|
|
997
|
+
switch (operator) {
|
|
998
|
+
case 'equals':
|
|
999
|
+
matches = cellValue === params.filterValue
|
|
1000
|
+
break
|
|
1001
|
+
case 'contains':
|
|
1002
|
+
matches = cellValue.includes(params.filterValue)
|
|
1003
|
+
break
|
|
1004
|
+
case 'startsWith':
|
|
1005
|
+
matches = cellValue.startsWith(params.filterValue)
|
|
1006
|
+
break
|
|
1007
|
+
case 'endsWith':
|
|
1008
|
+
matches = cellValue.endsWith(params.filterValue)
|
|
1009
|
+
break
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
if (matches) {
|
|
1013
|
+
matchingRows.push({
|
|
1014
|
+
rowNumber: i + 1, // 1-indexed
|
|
1015
|
+
data: allData[i]
|
|
1016
|
+
})
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
if (context?.logger) {
|
|
1021
|
+
context.logger.info(
|
|
1022
|
+
`[GoogleSheetsAdapter] Filter completed: organizationId=${context.organizationId} executionId=${context.executionId} spreadsheetId=${params.spreadsheetId} sheetName=${params.sheetName} matchCount=${matchingRows.length}`
|
|
1023
|
+
)
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
return {
|
|
1027
|
+
rows: matchingRows,
|
|
1028
|
+
matchCount: matchingRows.length,
|
|
1029
|
+
headers
|
|
1030
|
+
}
|
|
1031
|
+
} catch (error: any) {
|
|
1032
|
+
throw this.handleApiError(error, params.spreadsheetId)
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
/**
|
|
1037
|
+
* Delete a row by searching for a value
|
|
1038
|
+
*/
|
|
1039
|
+
private async deleteRowByValue(
|
|
1040
|
+
sheets: sheets_v4.Sheets,
|
|
1041
|
+
params: DeleteRowByValueParams,
|
|
1042
|
+
context?: ExecutionContext
|
|
1043
|
+
): Promise<DeleteRowByValueResult> {
|
|
1044
|
+
if (!params.spreadsheetId) {
|
|
1045
|
+
throw new ToolingError('validation_error', 'Missing required field: spreadsheetId', { params })
|
|
1046
|
+
}
|
|
1047
|
+
if (!params.sheetName) {
|
|
1048
|
+
throw new ToolingError('validation_error', 'Missing required field: sheetName', { params })
|
|
1049
|
+
}
|
|
1050
|
+
if (!params.searchColumn) {
|
|
1051
|
+
throw new ToolingError('validation_error', 'Missing required field: searchColumn', { params })
|
|
1052
|
+
}
|
|
1053
|
+
if (params.searchValue === undefined || params.searchValue === null) {
|
|
1054
|
+
throw new ToolingError('validation_error', 'Missing required field: searchValue', { params })
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
try {
|
|
1058
|
+
// Find the row first
|
|
1059
|
+
const findResult = await this.getRowByValue(
|
|
1060
|
+
sheets,
|
|
1061
|
+
{
|
|
1062
|
+
spreadsheetId: params.spreadsheetId,
|
|
1063
|
+
sheetName: params.sheetName,
|
|
1064
|
+
searchColumn: params.searchColumn,
|
|
1065
|
+
searchValue: params.searchValue
|
|
1066
|
+
},
|
|
1067
|
+
context
|
|
1068
|
+
)
|
|
1069
|
+
|
|
1070
|
+
if (!findResult.found || findResult.rowNumber === null) {
|
|
1071
|
+
return { found: false, deletedRowNumber: null }
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
// Get sheet ID for the delete request
|
|
1075
|
+
const metadataResponse = await sheets.spreadsheets.get({
|
|
1076
|
+
spreadsheetId: params.spreadsheetId,
|
|
1077
|
+
fields: 'sheets.properties'
|
|
1078
|
+
})
|
|
1079
|
+
|
|
1080
|
+
const sheetInfo = metadataResponse.data.sheets?.find((s) => s.properties?.title === params.sheetName)
|
|
1081
|
+
|
|
1082
|
+
if (!sheetInfo?.properties || sheetInfo.properties.sheetId === undefined) {
|
|
1083
|
+
throw new ToolingError('api_error', `Sheet not found: ${params.sheetName}`, {
|
|
1084
|
+
spreadsheetId: params.spreadsheetId,
|
|
1085
|
+
sheetName: params.sheetName
|
|
1086
|
+
})
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
// Delete the row using batchUpdate
|
|
1090
|
+
await sheets.spreadsheets.batchUpdate({
|
|
1091
|
+
spreadsheetId: params.spreadsheetId,
|
|
1092
|
+
requestBody: {
|
|
1093
|
+
requests: [
|
|
1094
|
+
{
|
|
1095
|
+
deleteDimension: {
|
|
1096
|
+
range: {
|
|
1097
|
+
sheetId: sheetInfo.properties.sheetId,
|
|
1098
|
+
dimension: 'ROWS',
|
|
1099
|
+
startIndex: findResult.rowNumber - 1, // 0-indexed
|
|
1100
|
+
endIndex: findResult.rowNumber // exclusive
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
]
|
|
1105
|
+
}
|
|
1106
|
+
})
|
|
1107
|
+
|
|
1108
|
+
if (context?.logger) {
|
|
1109
|
+
context.logger.info(
|
|
1110
|
+
`[GoogleSheetsAdapter] Row deleted: organizationId=${context.organizationId} executionId=${context.executionId} spreadsheetId=${params.spreadsheetId} sheetName=${params.sheetName} deletedRowNumber=${findResult.rowNumber}`
|
|
1111
|
+
)
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
return { found: true, deletedRowNumber: findResult.rowNumber }
|
|
1115
|
+
} catch (error: any) {
|
|
1116
|
+
throw this.handleApiError(error, params.spreadsheetId)
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
/**
|
|
1121
|
+
* Convert column letter to 0-based index (A=0, B=1, ..., Z=25, AA=26, etc.)
|
|
1122
|
+
*/
|
|
1123
|
+
private columnLetterToIndex(letter: string): number {
|
|
1124
|
+
let index = 0
|
|
1125
|
+
for (let i = 0; i < letter.length; i++) {
|
|
1126
|
+
index = index * 26 + (letter.charCodeAt(i) - 64)
|
|
1127
|
+
}
|
|
1128
|
+
return index - 1
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
/**
|
|
1132
|
+
* Convert 0-based index to column letter (0=A, 1=B, ..., 25=Z, 26=AA, etc.)
|
|
1133
|
+
*/
|
|
1134
|
+
private indexToColumnLetter(index: number): string {
|
|
1135
|
+
let letter = ''
|
|
1136
|
+
let temp = index
|
|
1137
|
+
while (temp >= 0) {
|
|
1138
|
+
letter = String.fromCharCode((temp % 26) + 65) + letter
|
|
1139
|
+
temp = Math.floor(temp / 26) - 1
|
|
1140
|
+
}
|
|
1141
|
+
return letter
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
/**
|
|
1145
|
+
* Handle Google Sheets API errors with meaningful messages
|
|
1146
|
+
*/
|
|
1147
|
+
private handleApiError(error: any, spreadsheetId: string): ToolingError {
|
|
1148
|
+
const code = error.code || error.status || error.response?.status
|
|
1149
|
+
|
|
1150
|
+
if (code === 404) {
|
|
1151
|
+
return new ToolingError('api_error', `Spreadsheet not found: ${spreadsheetId}`, {
|
|
1152
|
+
spreadsheetId,
|
|
1153
|
+
statusCode: 404
|
|
1154
|
+
})
|
|
1155
|
+
}
|
|
1156
|
+
if (code === 403) {
|
|
1157
|
+
return new ToolingError('permission_denied', 'Access denied. Check sharing permissions.', {
|
|
1158
|
+
spreadsheetId,
|
|
1159
|
+
statusCode: 403
|
|
1160
|
+
})
|
|
1161
|
+
}
|
|
1162
|
+
if (code === 401) {
|
|
1163
|
+
return new ToolingError('credentials_invalid', 'Authentication expired. Token refresh required.', {
|
|
1164
|
+
spreadsheetId,
|
|
1165
|
+
statusCode: 401
|
|
1166
|
+
})
|
|
1167
|
+
}
|
|
1168
|
+
if (code === 429) {
|
|
1169
|
+
return new ToolingError('rate_limit_exceeded', 'Rate limited. Retry after delay.', {
|
|
1170
|
+
spreadsheetId,
|
|
1171
|
+
statusCode: 429
|
|
1172
|
+
})
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
return new ToolingError('api_error', `Google Sheets API error: ${error.message}`, {
|
|
1176
|
+
spreadsheetId,
|
|
1177
|
+
statusCode: code,
|
|
1178
|
+
details: error
|
|
1179
|
+
})
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
/**
|
|
1183
|
+
* Validate credentials structure
|
|
1184
|
+
* Required by BaseIntegrationAdapter interface
|
|
1185
|
+
*/
|
|
1186
|
+
validateCredentials(creds: Record<string, unknown>): boolean {
|
|
1187
|
+
return typeof creds.accessToken === 'string' && creds.accessToken.length > 0
|
|
1188
|
+
}
|
|
1189
|
+
}
|