@cloudstreamsoftware/claude-tools 1.0.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +152 -37
- package/agents/INDEX.md +183 -0
- package/agents/architect.md +247 -0
- package/agents/build-error-resolver.md +555 -0
- package/agents/catalyst-deployer.md +132 -0
- package/agents/code-reviewer.md +121 -0
- package/agents/compliance-auditor.md +148 -0
- package/agents/creator-architect.md +395 -0
- package/agents/deluge-reviewer.md +98 -0
- package/agents/doc-updater.md +471 -0
- package/agents/e2e-runner.md +711 -0
- package/agents/planner.md +122 -0
- package/agents/refactor-cleaner.md +309 -0
- package/agents/security-reviewer.md +582 -0
- package/agents/tdd-guide.md +302 -0
- package/bin/cloudstream-setup.js +16 -6
- package/config/versions.json +63 -0
- package/dist/hooks/hooks.json +209 -0
- package/dist/index.js +47 -0
- package/dist/lib/asset-value.js +609 -0
- package/dist/lib/client-manager.js +300 -0
- package/dist/lib/command-matcher.js +242 -0
- package/dist/lib/cross-session-patterns.js +754 -0
- package/dist/lib/intent-classifier.js +1075 -0
- package/dist/lib/package-manager.js +374 -0
- package/dist/lib/recommendation-engine.js +597 -0
- package/dist/lib/session-memory.js +489 -0
- package/dist/lib/skill-effectiveness.js +486 -0
- package/dist/lib/skill-matcher.js +595 -0
- package/dist/lib/tutorial-metrics.js +242 -0
- package/dist/lib/tutorial-progress.js +209 -0
- package/dist/lib/tutorial-renderer.js +431 -0
- package/dist/lib/utils.js +380 -0
- package/dist/lib/verify-formatter.js +143 -0
- package/dist/lib/workflow-state.js +249 -0
- package/hooks/hooks.json +209 -0
- package/package.json +5 -1
- package/scripts/aggregate-sessions.js +290 -0
- package/scripts/branch-name-validator.js +291 -0
- package/scripts/build.js +101 -0
- package/scripts/commands/client-switch.js +231 -0
- package/scripts/deprecate-skill.js +610 -0
- package/scripts/diagnose.js +324 -0
- package/scripts/doc-freshness.js +168 -0
- package/scripts/generate-weekly-digest.js +393 -0
- package/scripts/health-check.js +270 -0
- package/scripts/hooks/credential-check.js +101 -0
- package/scripts/hooks/evaluate-session.js +81 -0
- package/scripts/hooks/pre-compact.js +66 -0
- package/scripts/hooks/prompt-analyzer.js +276 -0
- package/scripts/hooks/prompt-router.js +422 -0
- package/scripts/hooks/quality-gate-enforcer.js +371 -0
- package/scripts/hooks/session-end.js +156 -0
- package/scripts/hooks/session-start.js +195 -0
- package/scripts/hooks/skill-injector.js +333 -0
- package/scripts/hooks/suggest-compact.js +58 -0
- package/scripts/lib/asset-value.js +609 -0
- package/scripts/lib/client-manager.js +300 -0
- package/scripts/lib/command-matcher.js +242 -0
- package/scripts/lib/cross-session-patterns.js +754 -0
- package/scripts/lib/intent-classifier.js +1075 -0
- package/scripts/lib/package-manager.js +374 -0
- package/scripts/lib/recommendation-engine.js +597 -0
- package/scripts/lib/session-memory.js +489 -0
- package/scripts/lib/skill-effectiveness.js +486 -0
- package/scripts/lib/skill-matcher.js +595 -0
- package/scripts/lib/tutorial-metrics.js +242 -0
- package/scripts/lib/tutorial-progress.js +209 -0
- package/scripts/lib/tutorial-renderer.js +431 -0
- package/scripts/lib/utils.js +380 -0
- package/scripts/lib/verify-formatter.js +143 -0
- package/scripts/lib/workflow-state.js +249 -0
- package/scripts/onboard.js +363 -0
- package/scripts/quarterly-report.js +692 -0
- package/scripts/setup-package-manager.js +204 -0
- package/scripts/sync-upstream.js +391 -0
- package/scripts/test.js +108 -0
- package/scripts/tutorial-runner.js +351 -0
- package/scripts/validate-all.js +201 -0
- package/scripts/verifiers/agents.js +245 -0
- package/scripts/verifiers/config.js +186 -0
- package/scripts/verifiers/environment.js +123 -0
- package/scripts/verifiers/hooks.js +188 -0
- package/scripts/verifiers/index.js +38 -0
- package/scripts/verifiers/persistence.js +140 -0
- package/scripts/verifiers/plugin.js +215 -0
- package/scripts/verifiers/skills.js +209 -0
- package/scripts/verify-setup.js +164 -0
- package/skills/INDEX.md +157 -0
- package/skills/backend-patterns/SKILL.md +586 -0
- package/skills/backend-patterns/catalyst-patterns.md +128 -0
- package/skills/bigquery-patterns/SKILL.md +27 -0
- package/skills/bigquery-patterns/performance-optimization.md +518 -0
- package/skills/bigquery-patterns/query-patterns.md +372 -0
- package/skills/bigquery-patterns/schema-design.md +78 -0
- package/skills/cloudstream-project-template/SKILL.md +20 -0
- package/skills/cloudstream-project-template/structure.md +65 -0
- package/skills/coding-standards/SKILL.md +524 -0
- package/skills/coding-standards/deluge-standards.md +83 -0
- package/skills/compliance-patterns/SKILL.md +28 -0
- package/skills/compliance-patterns/hipaa/audit-requirements.md +251 -0
- package/skills/compliance-patterns/hipaa/baa-process.md +298 -0
- package/skills/compliance-patterns/hipaa/data-archival-strategy.md +387 -0
- package/skills/compliance-patterns/hipaa/phi-handling.md +52 -0
- package/skills/compliance-patterns/pci-dss/saq-a-requirements.md +307 -0
- package/skills/compliance-patterns/pci-dss/tokenization-patterns.md +382 -0
- package/skills/compliance-patterns/pci-dss/zoho-checkout-patterns.md +56 -0
- package/skills/compliance-patterns/soc2/access-controls.md +344 -0
- package/skills/compliance-patterns/soc2/audit-logging.md +458 -0
- package/skills/compliance-patterns/soc2/change-management.md +403 -0
- package/skills/compliance-patterns/soc2/deluge-execution-logging.md +407 -0
- package/skills/consultancy-workflows/SKILL.md +19 -0
- package/skills/consultancy-workflows/client-isolation.md +21 -0
- package/skills/consultancy-workflows/documentation-automation.md +454 -0
- package/skills/consultancy-workflows/handoff-procedures.md +257 -0
- package/skills/consultancy-workflows/knowledge-capture.md +513 -0
- package/skills/consultancy-workflows/time-tracking.md +26 -0
- package/skills/continuous-learning/SKILL.md +84 -0
- package/skills/continuous-learning/config.json +18 -0
- package/skills/continuous-learning/evaluate-session.sh +60 -0
- package/skills/continuous-learning-v2/SKILL.md +126 -0
- package/skills/continuous-learning-v2/config.json +61 -0
- package/skills/frontend-patterns/SKILL.md +635 -0
- package/skills/frontend-patterns/zoho-widget-patterns.md +103 -0
- package/skills/gcp-data-engineering/SKILL.md +36 -0
- package/skills/gcp-data-engineering/bigquery/performance-optimization.md +337 -0
- package/skills/gcp-data-engineering/dataflow/error-handling.md +496 -0
- package/skills/gcp-data-engineering/dataflow/pipeline-patterns.md +444 -0
- package/skills/gcp-data-engineering/dbt/model-organization.md +63 -0
- package/skills/gcp-data-engineering/dbt/testing-patterns.md +503 -0
- package/skills/gcp-data-engineering/medallion-architecture/bronze-layer.md +60 -0
- package/skills/gcp-data-engineering/medallion-architecture/gold-layer.md +311 -0
- package/skills/gcp-data-engineering/medallion-architecture/layer-transitions.md +517 -0
- package/skills/gcp-data-engineering/medallion-architecture/silver-layer.md +305 -0
- package/skills/gcp-data-engineering/zoho-to-gcp/data-extraction.md +543 -0
- package/skills/gcp-data-engineering/zoho-to-gcp/real-time-vs-batch.md +337 -0
- package/skills/security-review/SKILL.md +498 -0
- package/skills/security-review/compliance-checklist.md +53 -0
- package/skills/strategic-compact/SKILL.md +67 -0
- package/skills/tdd-workflow/SKILL.md +413 -0
- package/skills/tdd-workflow/zoho-testing.md +124 -0
- package/skills/tutorial/SKILL.md +249 -0
- package/skills/tutorial/docs/ACCESSIBILITY.md +169 -0
- package/skills/tutorial/lessons/00-philosophy-and-workflow.md +198 -0
- package/skills/tutorial/lessons/01-basics.md +81 -0
- package/skills/tutorial/lessons/02-training.md +86 -0
- package/skills/tutorial/lessons/03-commands.md +109 -0
- package/skills/tutorial/lessons/04-workflows.md +115 -0
- package/skills/tutorial/lessons/05-compliance.md +116 -0
- package/skills/tutorial/lessons/06-zoho.md +121 -0
- package/skills/tutorial/lessons/07-hooks-system.md +277 -0
- package/skills/tutorial/lessons/08-mcp-servers.md +316 -0
- package/skills/tutorial/lessons/09-client-management.md +215 -0
- package/skills/tutorial/lessons/10-testing-e2e.md +260 -0
- package/skills/tutorial/lessons/11-skills-deep-dive.md +272 -0
- package/skills/tutorial/lessons/12-rules-system.md +326 -0
- package/skills/tutorial/lessons/13-golden-standard-graduation.md +213 -0
- package/skills/tutorial/lessons/14-fork-setup-and-sync.md +312 -0
- package/skills/tutorial/lessons/15-living-examples-system.md +221 -0
- package/skills/tutorial/tracks/accelerated/README.md +134 -0
- package/skills/tutorial/tracks/accelerated/assessment/checkpoint-1.md +161 -0
- package/skills/tutorial/tracks/accelerated/assessment/checkpoint-2.md +175 -0
- package/skills/tutorial/tracks/accelerated/day-1-core-concepts.md +234 -0
- package/skills/tutorial/tracks/accelerated/day-2-essential-commands.md +270 -0
- package/skills/tutorial/tracks/accelerated/day-3-workflow-mastery.md +305 -0
- package/skills/tutorial/tracks/accelerated/day-4-compliance-zoho.md +304 -0
- package/skills/tutorial/tracks/accelerated/day-5-hooks-skills.md +344 -0
- package/skills/tutorial/tracks/accelerated/day-6-client-testing.md +386 -0
- package/skills/tutorial/tracks/accelerated/day-7-graduation.md +369 -0
- package/skills/zoho-patterns/CHANGELOG.md +108 -0
- package/skills/zoho-patterns/SKILL.md +446 -0
- package/skills/zoho-patterns/analytics/dashboard-patterns.md +352 -0
- package/skills/zoho-patterns/analytics/zoho-to-bigquery-pipeline.md +427 -0
- package/skills/zoho-patterns/catalyst/appsail-deployment.md +349 -0
- package/skills/zoho-patterns/catalyst/context-close-patterns.md +354 -0
- package/skills/zoho-patterns/catalyst/cron-batch-processing.md +374 -0
- package/skills/zoho-patterns/catalyst/function-patterns.md +439 -0
- package/skills/zoho-patterns/creator/form-design.md +304 -0
- package/skills/zoho-patterns/creator/publish-api-patterns.md +313 -0
- package/skills/zoho-patterns/creator/widget-integration.md +306 -0
- package/skills/zoho-patterns/creator/workflow-automation.md +253 -0
- package/skills/zoho-patterns/deluge/api-patterns.md +468 -0
- package/skills/zoho-patterns/deluge/batch-processing.md +403 -0
- package/skills/zoho-patterns/deluge/cross-app-integration.md +356 -0
- package/skills/zoho-patterns/deluge/error-handling.md +423 -0
- package/skills/zoho-patterns/deluge/syntax-reference.md +65 -0
- package/skills/zoho-patterns/integration/cors-proxy-architecture.md +426 -0
- package/skills/zoho-patterns/integration/crm-books-native-sync.md +277 -0
- package/skills/zoho-patterns/integration/oauth-token-management.md +461 -0
- package/skills/zoho-patterns/integration/zoho-flow-patterns.md +334 -0
|
@@ -0,0 +1,439 @@
|
|
|
1
|
+
# Catalyst Function Patterns
|
|
2
|
+
|
|
3
|
+
## Function Types and Timeouts
|
|
4
|
+
|
|
5
|
+
| Type | Timeout | Trigger | Use Case |
|
|
6
|
+
|------|---------|---------|----------|
|
|
7
|
+
| Basic I/O | 30 seconds | HTTP request (client SDK) | CRUD endpoints, widget backends |
|
|
8
|
+
| Cron | 15 minutes | Schedule (cron expression) | Batch syncs, archival, reports |
|
|
9
|
+
| Event | 15 minutes | Catalyst event (table ops, file ops) | Post-processing, notifications |
|
|
10
|
+
|
|
11
|
+
> **CRITICAL**: `context.close()` is MANDATORY in ALL function types. Without it, the function hangs until timeout. ALWAYS place it in a `finally` block to guarantee execution.
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## Basic I/O Function (30s timeout)
|
|
16
|
+
|
|
17
|
+
### Pattern: CRUD Endpoint for Creator Widget
|
|
18
|
+
|
|
19
|
+
```javascript
|
|
20
|
+
'use strict';
|
|
21
|
+
|
|
22
|
+
const catalyst = require('zcatalyst-sdk-node');
|
|
23
|
+
|
|
24
|
+
module.exports = async (context, basicIO) => {
|
|
25
|
+
const app = catalyst.initialize(context);
|
|
26
|
+
const response = { status: 'success', data: null };
|
|
27
|
+
|
|
28
|
+
try {
|
|
29
|
+
const method = context.getHTTPMethod ? context.getHTTPMethod() : 'GET';
|
|
30
|
+
const input = JSON.parse(basicIO.getArgument('data') || '{}');
|
|
31
|
+
|
|
32
|
+
switch (input.action) {
|
|
33
|
+
case 'getContacts': {
|
|
34
|
+
const zcql = app.zcql();
|
|
35
|
+
const result = await zcql.executeZCQLQuery(
|
|
36
|
+
`SELECT * FROM Contacts WHERE Status = 'Active' LIMIT 200`
|
|
37
|
+
);
|
|
38
|
+
response.data = result;
|
|
39
|
+
break;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
case 'createContact': {
|
|
43
|
+
const table = app.datastore().table('Contacts');
|
|
44
|
+
const row = await table.insertRow({
|
|
45
|
+
Name: input.name,
|
|
46
|
+
Email: input.email,
|
|
47
|
+
Phone: input.phone || '',
|
|
48
|
+
Status: 'Active',
|
|
49
|
+
Created_By: input.userId
|
|
50
|
+
});
|
|
51
|
+
response.data = { id: row.ROWID, message: 'Contact created' };
|
|
52
|
+
break;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
case 'updateContact': {
|
|
56
|
+
const table = app.datastore().table('Contacts');
|
|
57
|
+
const row = await table.updateRow({
|
|
58
|
+
ROWID: input.recordId,
|
|
59
|
+
Name: input.name,
|
|
60
|
+
Email: input.email,
|
|
61
|
+
Phone: input.phone
|
|
62
|
+
});
|
|
63
|
+
response.data = { id: row.ROWID, message: 'Contact updated' };
|
|
64
|
+
break;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
case 'deleteContact': {
|
|
68
|
+
const table = app.datastore().table('Contacts');
|
|
69
|
+
await table.deleteRow(input.recordId);
|
|
70
|
+
response.data = { message: 'Contact deleted' };
|
|
71
|
+
break;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
default:
|
|
75
|
+
response.status = 'error';
|
|
76
|
+
response.message = `Unknown action: ${input.action}`;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
basicIO.write(JSON.stringify(response));
|
|
80
|
+
} catch (error) {
|
|
81
|
+
console.error('Function error:', error.message);
|
|
82
|
+
response.status = 'error';
|
|
83
|
+
response.message = error.message;
|
|
84
|
+
basicIO.write(JSON.stringify(response));
|
|
85
|
+
} finally {
|
|
86
|
+
context.close(); // MANDATORY - always in finally block
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### Pattern: Webhook Handler (External API Callback)
|
|
92
|
+
|
|
93
|
+
```javascript
|
|
94
|
+
'use strict';
|
|
95
|
+
|
|
96
|
+
const crypto = require('crypto');
|
|
97
|
+
const catalyst = require('zcatalyst-sdk-node');
|
|
98
|
+
|
|
99
|
+
module.exports = async (context, basicIO) => {
|
|
100
|
+
const app = catalyst.initialize(context);
|
|
101
|
+
const response = { status: 'received' };
|
|
102
|
+
|
|
103
|
+
try {
|
|
104
|
+
const payload = JSON.parse(basicIO.getArgument('data') || '{}');
|
|
105
|
+
const signature = basicIO.getArgument('signature') || '';
|
|
106
|
+
|
|
107
|
+
// Verify webhook signature
|
|
108
|
+
const secret = await app.cache()
|
|
109
|
+
.segment('config')
|
|
110
|
+
.getItem('webhook_secret');
|
|
111
|
+
|
|
112
|
+
const expectedSignature = crypto
|
|
113
|
+
.createHmac('sha256', secret.cache_value)
|
|
114
|
+
.update(JSON.stringify(payload))
|
|
115
|
+
.digest('hex');
|
|
116
|
+
|
|
117
|
+
if (!crypto.timingSafeEqual(
|
|
118
|
+
Buffer.from(signature, 'hex'),
|
|
119
|
+
Buffer.from(expectedSignature, 'hex')
|
|
120
|
+
)) {
|
|
121
|
+
console.error('Invalid webhook signature');
|
|
122
|
+
response.status = 'unauthorized';
|
|
123
|
+
basicIO.write(JSON.stringify(response));
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Process webhook event
|
|
128
|
+
const eventType = payload.event_type;
|
|
129
|
+
console.log(`Processing webhook: ${eventType}`);
|
|
130
|
+
|
|
131
|
+
switch (eventType) {
|
|
132
|
+
case 'payment.succeeded':
|
|
133
|
+
await handlePaymentSuccess(app, payload.data);
|
|
134
|
+
break;
|
|
135
|
+
case 'payment.failed':
|
|
136
|
+
await handlePaymentFailure(app, payload.data);
|
|
137
|
+
break;
|
|
138
|
+
default:
|
|
139
|
+
console.log(`Unhandled event type: ${eventType}`);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
response.status = 'processed';
|
|
143
|
+
basicIO.write(JSON.stringify(response));
|
|
144
|
+
} catch (error) {
|
|
145
|
+
console.error('Webhook processing error:', error.message);
|
|
146
|
+
response.status = 'error';
|
|
147
|
+
response.message = error.message;
|
|
148
|
+
basicIO.write(JSON.stringify(response));
|
|
149
|
+
} finally {
|
|
150
|
+
context.close(); // MANDATORY
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
async function handlePaymentSuccess(app, data) {
|
|
155
|
+
const table = app.datastore().table('Payments');
|
|
156
|
+
await table.insertRow({
|
|
157
|
+
Transaction_ID: data.transaction_id,
|
|
158
|
+
Amount: data.amount,
|
|
159
|
+
Currency: data.currency,
|
|
160
|
+
Customer_Email: data.customer_email,
|
|
161
|
+
Status: 'Succeeded',
|
|
162
|
+
Processed_At: new Date().toISOString()
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
async function handlePaymentFailure(app, data) {
|
|
167
|
+
const table = app.datastore().table('Payments');
|
|
168
|
+
await table.insertRow({
|
|
169
|
+
Transaction_ID: data.transaction_id,
|
|
170
|
+
Amount: data.amount,
|
|
171
|
+
Status: 'Failed',
|
|
172
|
+
Failure_Reason: data.failure_reason,
|
|
173
|
+
Processed_At: new Date().toISOString()
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
// Send notification via Catalyst Mail
|
|
177
|
+
const mail = app.email();
|
|
178
|
+
await mail.sendMail({
|
|
179
|
+
from_email: 'payments@clientapp.com',
|
|
180
|
+
to_email: data.customer_email,
|
|
181
|
+
subject: 'Payment Failed',
|
|
182
|
+
content: `Your payment of ${data.currency} ${data.amount / 100} could not be processed.`
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
---
|
|
188
|
+
|
|
189
|
+
## Cron Function (15min timeout)
|
|
190
|
+
|
|
191
|
+
### Pattern: Scheduled Zoho-to-Datastore Sync
|
|
192
|
+
|
|
193
|
+
```javascript
|
|
194
|
+
'use strict';
|
|
195
|
+
|
|
196
|
+
const catalyst = require('zcatalyst-sdk-node');
|
|
197
|
+
const fetch = require('node-fetch');
|
|
198
|
+
|
|
199
|
+
module.exports = async (cronDetails, context) => {
|
|
200
|
+
const app = catalyst.initialize(context);
|
|
201
|
+
const startTime = Date.now();
|
|
202
|
+
let syncedCount = 0;
|
|
203
|
+
let errorCount = 0;
|
|
204
|
+
|
|
205
|
+
try {
|
|
206
|
+
console.log(`Cron started: ${cronDetails.cron_name}`);
|
|
207
|
+
|
|
208
|
+
// Get last sync timestamp from Catalyst Cache
|
|
209
|
+
const cache = app.cache().segment('sync_state');
|
|
210
|
+
const lastSyncItem = await cache.getItem('zoho_contacts_last_sync')
|
|
211
|
+
.catch(() => null);
|
|
212
|
+
const lastSync = lastSyncItem
|
|
213
|
+
? lastSyncItem.cache_value
|
|
214
|
+
: new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString();
|
|
215
|
+
|
|
216
|
+
// Get Zoho access token from cache (refreshed by separate function)
|
|
217
|
+
const tokenItem = await cache.getItem('zoho_access_token');
|
|
218
|
+
if (!tokenItem) {
|
|
219
|
+
throw new Error('No Zoho access token in cache - run token refresh first');
|
|
220
|
+
}
|
|
221
|
+
const accessToken = tokenItem.cache_value;
|
|
222
|
+
|
|
223
|
+
// Fetch modified records from Zoho CRM (paginated)
|
|
224
|
+
let page = 1;
|
|
225
|
+
let hasMore = true;
|
|
226
|
+
|
|
227
|
+
while (hasMore) {
|
|
228
|
+
// Check if we're approaching the 15-minute timeout
|
|
229
|
+
if (Date.now() - startTime > 13 * 60 * 1000) {
|
|
230
|
+
console.log('Approaching timeout limit - stopping pagination');
|
|
231
|
+
break;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const response = await fetch(
|
|
235
|
+
`https://www.zohoapis.com/crm/v5/Contacts?modified_since=${lastSync}&page=${page}&per_page=200`,
|
|
236
|
+
{ headers: { 'Authorization': `Zoho-oauthtoken ${accessToken}` } }
|
|
237
|
+
);
|
|
238
|
+
|
|
239
|
+
if (response.status === 204) {
|
|
240
|
+
hasMore = false;
|
|
241
|
+
continue;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if (!response.ok) {
|
|
245
|
+
throw new Error(`Zoho API error: ${response.status} ${await response.text()}`);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const data = await response.json();
|
|
249
|
+
const records = data.data || [];
|
|
250
|
+
|
|
251
|
+
// Batch insert into Catalyst Datastore
|
|
252
|
+
const table = app.datastore().table('CRM_Contacts');
|
|
253
|
+
for (const record of records) {
|
|
254
|
+
try {
|
|
255
|
+
await table.insertRow({
|
|
256
|
+
Zoho_ID: record.id,
|
|
257
|
+
Full_Name: `${record.First_Name || ''} ${record.Last_Name || ''}`.trim(),
|
|
258
|
+
Email: record.Email || '',
|
|
259
|
+
Phone: record.Phone || '',
|
|
260
|
+
Company: record.Account_Name?.name || '',
|
|
261
|
+
Modified_Time: record.Modified_Time,
|
|
262
|
+
Synced_At: new Date().toISOString()
|
|
263
|
+
});
|
|
264
|
+
syncedCount++;
|
|
265
|
+
} catch (insertError) {
|
|
266
|
+
// Record may already exist - try update
|
|
267
|
+
if (insertError.message.includes('already exists')) {
|
|
268
|
+
// Use ZCQL to find and update
|
|
269
|
+
const zcql = app.zcql();
|
|
270
|
+
const existing = await zcql.executeZCQLQuery(
|
|
271
|
+
`SELECT ROWID FROM CRM_Contacts WHERE Zoho_ID = '${record.id}'`
|
|
272
|
+
);
|
|
273
|
+
if (existing.length > 0) {
|
|
274
|
+
await table.updateRow({
|
|
275
|
+
ROWID: existing[0].CRM_Contacts.ROWID,
|
|
276
|
+
Full_Name: `${record.First_Name || ''} ${record.Last_Name || ''}`.trim(),
|
|
277
|
+
Email: record.Email || '',
|
|
278
|
+
Phone: record.Phone || '',
|
|
279
|
+
Modified_Time: record.Modified_Time,
|
|
280
|
+
Synced_At: new Date().toISOString()
|
|
281
|
+
});
|
|
282
|
+
syncedCount++;
|
|
283
|
+
}
|
|
284
|
+
} else {
|
|
285
|
+
errorCount++;
|
|
286
|
+
console.error(`Insert error for ${record.id}: ${insertError.message}`);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
hasMore = data.info?.more_records || false;
|
|
292
|
+
page++;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// Update last sync timestamp
|
|
296
|
+
await cache.putItem('zoho_contacts_last_sync', new Date().toISOString());
|
|
297
|
+
|
|
298
|
+
console.log(`Cron complete: ${syncedCount} synced, ${errorCount} errors, ${Date.now() - startTime}ms`);
|
|
299
|
+
} catch (error) {
|
|
300
|
+
console.error(`Cron fatal error: ${error.message}`);
|
|
301
|
+
// Optionally notify admin via Catalyst Mail
|
|
302
|
+
} finally {
|
|
303
|
+
context.close(); // MANDATORY - always in finally block
|
|
304
|
+
}
|
|
305
|
+
};
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
---
|
|
309
|
+
|
|
310
|
+
## Event Function (15min timeout)
|
|
311
|
+
|
|
312
|
+
### Pattern: Post-Insert Processing
|
|
313
|
+
|
|
314
|
+
```javascript
|
|
315
|
+
'use strict';
|
|
316
|
+
|
|
317
|
+
const catalyst = require('zcatalyst-sdk-node');
|
|
318
|
+
|
|
319
|
+
module.exports = async (eventData, context) => {
|
|
320
|
+
const app = catalyst.initialize(context);
|
|
321
|
+
|
|
322
|
+
try {
|
|
323
|
+
const { table_name, action, data } = eventData;
|
|
324
|
+
console.log(`Event: ${action} on ${table_name}`);
|
|
325
|
+
|
|
326
|
+
if (table_name === 'Orders' && action === 'INSERT') {
|
|
327
|
+
const order = data;
|
|
328
|
+
|
|
329
|
+
// Send order confirmation email
|
|
330
|
+
const mail = app.email();
|
|
331
|
+
await mail.sendMail({
|
|
332
|
+
from_email: 'orders@clientapp.com',
|
|
333
|
+
to_email: order.Customer_Email,
|
|
334
|
+
subject: `Order Confirmed #${order.ROWID}`,
|
|
335
|
+
content: `Your order of $${order.Amount} has been confirmed.`
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
// Update analytics counter in cache
|
|
339
|
+
const cache = app.cache().segment('analytics');
|
|
340
|
+
const countItem = await cache.getItem('daily_order_count').catch(() => null);
|
|
341
|
+
const currentCount = countItem ? parseInt(countItem.cache_value) : 0;
|
|
342
|
+
await cache.putItem('daily_order_count', String(currentCount + 1));
|
|
343
|
+
}
|
|
344
|
+
} catch (error) {
|
|
345
|
+
console.error(`Event processing error: ${error.message}`);
|
|
346
|
+
} finally {
|
|
347
|
+
context.close(); // MANDATORY - always in finally block
|
|
348
|
+
}
|
|
349
|
+
};
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
---
|
|
353
|
+
|
|
354
|
+
## AppSail Deployment (Long-Running)
|
|
355
|
+
|
|
356
|
+
### Pattern: Express.js API Server
|
|
357
|
+
|
|
358
|
+
```javascript
|
|
359
|
+
'use strict';
|
|
360
|
+
|
|
361
|
+
const express = require('express');
|
|
362
|
+
const catalyst = require('zcatalyst-sdk-node');
|
|
363
|
+
|
|
364
|
+
const app = express();
|
|
365
|
+
app.use(express.json());
|
|
366
|
+
|
|
367
|
+
// MUST bind to process.env.PORT (Catalyst requirement)
|
|
368
|
+
const PORT = process.env.PORT || 9000;
|
|
369
|
+
|
|
370
|
+
// Health check endpoint
|
|
371
|
+
app.get('/health', (req, res) => {
|
|
372
|
+
res.json({ status: 'healthy', timestamp: new Date().toISOString() });
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
// CORS proxy for Creator Widgets (widgets can't call external APIs directly)
|
|
376
|
+
app.post('/api/proxy', async (req, res) => {
|
|
377
|
+
const catalystApp = catalyst.initialize(req);
|
|
378
|
+
|
|
379
|
+
try {
|
|
380
|
+
const { url, method, headers, body } = req.body;
|
|
381
|
+
|
|
382
|
+
// Whitelist allowed domains
|
|
383
|
+
const allowedDomains = ['api.stripe.com', 'api.sendgrid.com'];
|
|
384
|
+
const requestUrl = new URL(url);
|
|
385
|
+
if (!allowedDomains.includes(requestUrl.hostname)) {
|
|
386
|
+
return res.status(403).json({ error: 'Domain not whitelisted' });
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
const response = await fetch(url, {
|
|
390
|
+
method: method || 'GET',
|
|
391
|
+
headers: headers || {},
|
|
392
|
+
body: body ? JSON.stringify(body) : undefined
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
const data = await response.json();
|
|
396
|
+
res.json({ status: response.status, data });
|
|
397
|
+
} catch (error) {
|
|
398
|
+
console.error('Proxy error:', error.message);
|
|
399
|
+
res.status(500).json({ error: error.message });
|
|
400
|
+
}
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
app.listen(PORT, () => {
|
|
404
|
+
console.log(`AppSail server running on port ${PORT}`);
|
|
405
|
+
});
|
|
406
|
+
```
|
|
407
|
+
|
|
408
|
+
---
|
|
409
|
+
|
|
410
|
+
## Catalyst Cache Usage
|
|
411
|
+
|
|
412
|
+
```javascript
|
|
413
|
+
// Cache has a 24-hour max TTL
|
|
414
|
+
const cache = app.cache();
|
|
415
|
+
const segment = cache.segment('my_segment');
|
|
416
|
+
|
|
417
|
+
// Store value (expires in 24h max)
|
|
418
|
+
await segment.putItem('key', 'value');
|
|
419
|
+
|
|
420
|
+
// Retrieve value
|
|
421
|
+
const item = await segment.getItem('key');
|
|
422
|
+
const value = item.cache_value;
|
|
423
|
+
|
|
424
|
+
// Delete value
|
|
425
|
+
await segment.deleteItem('key');
|
|
426
|
+
```
|
|
427
|
+
|
|
428
|
+
---
|
|
429
|
+
|
|
430
|
+
## Common Mistakes
|
|
431
|
+
|
|
432
|
+
| Mistake | Consequence | Fix |
|
|
433
|
+
|---------|-------------|-----|
|
|
434
|
+
| Missing `context.close()` | Function hangs until timeout, wasted compute | Always use `finally { context.close(); }` |
|
|
435
|
+
| Not binding to `process.env.PORT` | AppSail container fails to start | `app.listen(process.env.PORT)` |
|
|
436
|
+
| Exceeding 30s in I/O function | Request timeout, 504 error | Move to Cron or Event function |
|
|
437
|
+
| Synchronous file I/O | Blocks event loop, slow response | Use async `fs.promises` |
|
|
438
|
+
| Not handling Catalyst SDK errors | Unhandled rejections crash function | Wrap all SDK calls in try-catch |
|
|
439
|
+
| Storing secrets in code | Security vulnerability | Use Catalyst Cache or Vault |
|