@wanasapps/deluge-core 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.
@@ -0,0 +1,312 @@
1
+ # Zoho Deluge — how to write it correctly
2
+
3
+ Deluge is Zoho's scripting language. It runs inside Zoho products (CRM, Creator, Books, Desk, Recruit, People, Projects, Bigin, Inventory, Analytics, Cliq, Mail, Sheet, Writer, WorkDrive, Bookings, Subscriptions/Billing, SalesIQ, Calendar) and nowhere else. It looks like JavaScript or Java and **is not**: no classes, no closures, no `while`, no `switch`, a different standard library, and a runtime that validates far less than you expect. Most mistakes come from writing JavaScript with Deluge punctuation.
4
+
5
+ This document has two parts. **Part 1** is guidance: how to think, the contracts, and the traps — every trap marked **(verified)** was confirmed against a live Zoho CRM org, not taken from documentation. **Part 2** is the complete reference — every statement, data type, built-in function and integration task — generated from Zoho's own Deluge documentation (source and date at the top of Part 2). When the two disagree on a detail, Part 2 is what Zoho documents; Part 1 is what actually happened.
6
+
7
+ ## 1. Shape of a script
8
+
9
+ ```deluge
10
+ // Standalone function in Zoho CRM. The FIRST meaningful line is the signature:
11
+ // <returnType> standalone.<api_name>(<Type> <param>, ...)
12
+ string standalone.NormalizeLead(String leadId, Int retries)
13
+ {
14
+ lead = zoho.crm.getRecordById("Leads", leadId.toLong());
15
+ if(lead == null || lead.isEmpty())
16
+ {
17
+ return "not found";
18
+ }
19
+ info "normalizing " + leadId;
20
+ return "ok";
21
+ }
22
+ ```
23
+
24
+ - Every statement ends with `;`. Blocks use `{ }`. Indentation is tabs by convention (Zoho's editor writes tabs).
25
+ - No `var`/`let`/`const` and no type declarations: `x = 5;` creates `x`. Variables are function-scoped, dynamically typed, and can change type.
26
+ - Comments: `//` and `/* ... */`.
27
+ - Signature types: `String`, `Int`, `Long`, `Decimal`, `Boolean`, `Date`, `Map`, `List`. Return types: `string`, `int`, `decimal`, `boolean`, `map`, `list`, `void`.
28
+ - Reserved words you cannot use as variable names: `bool collection date false for each else else if float from if ifnull in int is list map null permissions portal reload return string thisapp true void zoho`.
29
+ - **Do not put a `//` comment block above the signature when uploading raw code (verified).** Zoho's raw code endpoint reads everything before the first `(` as the package declaration and rejects a leading comment with a misleading `COMPILATION_ERROR`. Comments inside the body are fine. Tooling built on `@wanasapps/deluge-core` (the `zone` CLI, the VS Code extension) strips a leading header for you.
30
+ - **Function names are case-insensitive (verified).** `standalone.SyncRecentCommissions(...)` and `standalone.syncrecentcommissions(...)` are the same function. A case-sensitive search for "is this function used anywhere" will report live code as dead.
31
+
32
+ ## 2. Control flow — what exists and what does not
33
+
34
+ ```deluge
35
+ if(a > 1 && b != null)
36
+ {
37
+ // ...
38
+ }
39
+ else if(c == "x")
40
+ {
41
+ // ...
42
+ }
43
+ else
44
+ {
45
+ // ...
46
+ }
47
+
48
+ label = if(score > 80, "high", "low"); // conditional-if EXPRESSION
49
+ name = ifNull(input.Name, "unknown"); // first value when not null, else the second
50
+
51
+ for each item in someList
52
+ {
53
+ if(item == null) { continue; }
54
+ if(item == "stop") { break; }
55
+ }
56
+
57
+ for each index i in someList // i is the position, 0-based
58
+ {
59
+ value = someList.get(i);
60
+ }
61
+
62
+ for each key in someMap.keys()
63
+ {
64
+ value = someMap.get(key);
65
+ }
66
+ ```
67
+
68
+ - Operators: `==`, `!=`, `<`, `<=`, `>`, `>=`, `&&`, `||`, `!`. String equality with `==` works and is case-sensitive; use `.equalsIgnoreCase()` otherwise.
69
+ - **There is no `while` loop.** Iterate a list, or a literal range such as `{1,2,3,4,5}`; if you need "until", loop a generous fixed range and `break`.
70
+ - **There is no `switch`.** Chain `else if`, or look the value up in a Map.
71
+ - **`throw` exists.** `throw "message";` or `throw {"message": "...", "data": {...}};` raises an exception that the nearest `catch` receives; inside a `catch(e)`, `throw e;` re-throws. There is no `finally`.
72
+ - `return;` is valid in a `void` function; every other function must return a value on every path.
73
+ - Criteria are written as `(<condition>) and/or (<condition>)` — parentheses around each condition, `and`/`or`/`&&`/`||` between them.
74
+
75
+ ## 3. Types, literals and the brace trap
76
+
77
+ | Type | Create | Notes |
78
+ |---|---|---|
79
+ | Map (key-value) | `m = Map();` or `m = {"a": 1, "b": "x"};` | keys unique; a repeated key overwrites |
80
+ | List | `l = List();` or `l = {1, 2, 3};` | 0-based; `List:String()` / `List:Int()` / `List:Float()` / `List:date()` / `List:Bool()` restrict the element type |
81
+ | Collection | `c = Collection();` | index-value or key-value, chosen by how you insert |
82
+ | Text | `"double"` (backslash escapes a quote inside) | concatenate with `+`; numbers auto-convert on `+` with text |
83
+ | Number | `5`, `1234567890123` | integer; -9,223,372,036,854,775,808 to 9,223,372,036,854,775,808 |
84
+ | Decimal | `5.25` | a Number in an operation with a Decimal yields a Decimal |
85
+ | Boolean | `true` / `false` | no quotes; case-insensitive |
86
+ | Date-time | `'15-Aug-1947'`, `'2026-09-01 14:30:00'` | **single quotes**; no time → 00:00:00; many formats accepted (see Part 2) |
87
+ | Time | `'19:00:00'`, `'06:00:00 PM'` | Creator only; all three components required |
88
+ | null | `null` | |
89
+
90
+ **Curly braces mean both Map and List.** `{"a": 1}` is a Map; `{1, 2}` is a List; `{}` is ambiguous. Use `Map()` and `List()` for empties.
91
+
92
+ **Record ids are Longs, not Strings.** They arrive as strings from JSON and as `Long` from `zoho.crm.*`. Compare and pass them carefully: `id.toLong()` when a Zoho call expects an id, `id.toString()` when building a string.
93
+
94
+ ### Map
95
+ ```deluge
96
+ m = Map();
97
+ m.put("k", "v"); // put replaces
98
+ m.get("k"); // null when missing — no exception
99
+ m.containKey("k"); // note the spelling: containKey, not containsKey
100
+ m.containValue("v");
101
+ m.keys(); // List of keys
102
+ m.remove("k");
103
+ m.putAll(otherMap); // merge
104
+ m.size(); m.isEmpty();
105
+ ```
106
+
107
+ ### List
108
+ ```deluge
109
+ l = List();
110
+ l.add("x"); l.addAll(otherList); // addAll: at most 25,000 elements per call
111
+ l.get(0); // out-of-range throws
112
+ l.size(); l.isEmpty(); l.contains("x"); l.indexOf("x"); l.lastIndexOf("x");
113
+ l.removeElement("x"); // by value
114
+ l.remove(0); // by index
115
+ l.sort(); l.distinct(); l.intersect(other);
116
+ l.toString(); // "x,y" — comma-joined
117
+ ```
118
+
119
+ ### Detecting Map vs List in a value you did not build
120
+ **`getJSONType()` does not exist in this runtime (verified — COMPILATION_ERROR).** Look at the text:
121
+ ```deluge
122
+ text = value.toString();
123
+ if(text.startsWith("[")) { list = value.toJSONList(); }
124
+ else if(text.startsWith("{")) { map = value.toMap(); }
125
+ ```
126
+
127
+ ## 4. Strings — the methods you will actually use
128
+
129
+ ```deluge
130
+ s.length(); s.trim(); s.toUpperCase(); s.toLowerCase(); s.isEmpty();
131
+ s.contains("x"); s.startsWith("x"); s.endsWith("x"); s.indexOf("x");
132
+ s.subString(0, 3); // start inclusive, end exclusive (also subText, mid, left, right)
133
+ s.equalsIgnoreCase("X");
134
+ s.matches("^[0-9]+$"); // regex
135
+ s.toList(","); // split → List
136
+ s.getPrefix("@"); s.getSuffix("@"); s.getSuffixIgnoreCase("<body>");
137
+ s.removeFirstOccurence("00"); // Zoho's spelling — one 'r'
138
+ s.leftPad(10, "0"); s.rightPad(10, " ");
139
+ s.toNumber(); s.toLong(); s.toDecimal(); s.toDate(); s.toDateTime(); s.toTime();
140
+ s.toMap(); s.toJSONList(); // parse JSON text
141
+ ```
142
+
143
+ **`replaceAll` — the third argument changes the meaning (verified).**
144
+ ```deluge
145
+ s.replaceAll("[^0-9]", ""); // 2 args: pattern is a REGEX
146
+ s.replaceAll("+", "", true); // 3rd arg true: pattern is LITERAL text
147
+ ```
148
+ Passing `true` "to be safe" silently turns a character class into a literal search for the characters `[^0-9]`, and nothing is replaced. If the pattern is a regex, use the 2-argument form. Zoho documents the third parameter as "escape the regular expression".
149
+
150
+ Regex flavour is Java-like: `\d`, `\s`, `[^...]`, `(?i)`. Escape backslashes in the Deluge string literal as normal.
151
+
152
+ ## 5. Calling Zoho — `zoho.crm.*` and friends
153
+
154
+ ```deluge
155
+ rec = zoho.crm.getRecordById("Deals", dealId);
156
+ recs = zoho.crm.getRecords("Leads", 1, 200); // page 1, up to 200 per call
157
+ found = zoho.crm.searchRecords("Contacts", "(Email:equals:" + email + ")");
158
+ // criteria: (Field:operator:value), joined with and/or in outer parens
159
+ kids = zoho.crm.getRelatedRecords("Notes", "Leads", leadId);
160
+ resp = zoho.crm.createRecord("Leads", leadMap);
161
+ resp = zoho.crm.updateRecord("Leads", leadId, changesMap);
162
+ resp = zoho.crm.upsert("License_Transactions", txnMap); // matches on the module's unique field(s)
163
+ value = zoho.crm.getOrgVariable("StoreBaseURL"); // org variables = config, not hard-coded secrets
164
+ ```
165
+
166
+ Zoho now documents a versioned form for CRM — `zoho.crm.v8.getRecordById(...)`, `zoho.crm.v8.upsert(...)`, `zoho.crm.v8.bulkCreate(...)` etc. (Part 2 lists them with full signatures). The unversioned names above still work on existing orgs; prefer the `v8` form in new code.
167
+
168
+ **Write calls do not throw when Zoho rejects the record (verified).** They return a Map and carry the failure inside it. Always check:
169
+ ```deluge
170
+ resp = zoho.crm.upsert("Leads", data);
171
+ if(resp == null || resp.get("status") == "error")
172
+ {
173
+ info "upsert rejected: " + resp;
174
+ result.put("error", "upsert rejected: " + resp);
175
+ return result;
176
+ }
177
+ newId = resp.get("id");
178
+ ```
179
+ Treating a non-null response as success is the single most common way a Deluge job reports "0 failures" while writing nothing.
180
+
181
+ Search results are a List of Maps; an empty search returns an empty List (check `.size() > 0`, then `.get(0)`). Lookup fields inside a record are Maps: `rec.get("Account_Name").get("id")`.
182
+
183
+ **Field API names, not labels.** Use the exact `api_name` (`Last_Name`, `Deal_Name`, `Closing_Date`), and picklist values exactly as configured. A wrong field name is not an error on write — it is silently dropped. Get the real names from the org, never from memory: `zone crm pull` writes `metadata/.zcrm/.store/field_map.json` (`{ Module: { Field: type } }`).
184
+
185
+ **Every integration task and every `invokeurl` counts against a daily external-call limit** (2,000 per user per day for each), and each execution inside a loop counts once per iteration.
186
+
187
+ ## 6. HTTP — `invokeurl`
188
+
189
+ ```deluge
190
+ payload = Map();
191
+ payload.put("query", "acme");
192
+
193
+ response = invokeurl
194
+ [
195
+ url : "https://api.example.com/search"
196
+ type : POST
197
+ parameters : payload.toString() // JSON body: send the Map as text
198
+ headers : {"Content-Type": "application/json", "Authorization": "Bearer " + token}
199
+ connection : "my_connection" // OR a saved OAuth connection instead of headers
200
+ detailed : true
201
+ ];
202
+ ```
203
+
204
+ - `type` is `GET`, `POST`, `PUT`, `DELETE` or `PATCH` — bare, no quotes.
205
+ - `parameters` as a **Map** sends a form body; as a **String** sends the text as-is (use `map.toString()` for JSON). `body` sends a raw body; `files` uploads a file object.
206
+ - Without `detailed`, the result is the parsed body: a Map or List when the response is JSON, otherwise a String. With `detailed:true` you get `responseCode`, `responseText`, `responseHeaders`.
207
+ - `response-format` and `response-decoding` control how the body is parsed; the response is capped at **5 MB**.
208
+ - **Check the result before merging it into anything.** An error page or a null merged with `putAll` corrupts the record and surfaces later as a confusing write failure:
209
+ ```deluge
210
+ if(response == null || !response.toString().startsWith("{")) { return errorMap("detail fetch failed"); }
211
+ ```
212
+ - `connection:` (a connection created under Setup → Connections) is the right way to authenticate to Zoho's own APIs and to third parties. **Never paste an API key or secret into a script** — it ends up in every revision and every export. Use a connection or `zoho.crm.getOrgVariable("...")`.
213
+
214
+ ## 7. Dates
215
+
216
+ ```deluge
217
+ d = zoho.currentdate; // today
218
+ t = zoho.currenttime; // now
219
+ s = t.toString("yyyy-MM-dd'T'HH:mm:ss"); // Java-style patterns; quote literals with ' '
220
+ d2 = '2026-09-01'.toDate();
221
+ dt = '2026-09-01 14:30:00'.toDateTime();
222
+ d.addDay(7); d.subDay(30); d.addMonth(1); d.addYear(1); d.addBusinessDay(3);
223
+ d.getDay(); d.getMonth(); d.getYear(); d.getDayOfWeek();
224
+ d.daysBetween(d2); d.monthsBetween(d2);
225
+ d.toString("dd-MMM-yyyy", "Asia/Dubai"); // time zone as the second argument
226
+ ```
227
+ Parsing user-supplied dates fails often; wrap `toDate()`/`toDateTime()` in `try`/`catch` and skip the field rather than abort the run.
228
+
229
+ ## 8. Errors, logging, and what "success" means
230
+
231
+ ```deluge
232
+ try
233
+ {
234
+ risky = something.toMap();
235
+ }
236
+ catch (e)
237
+ {
238
+ info "parse failed: " + e; // e carries the message (and data, for a thrown Map)
239
+ }
240
+ ```
241
+
242
+ - `info <value>;` is the only logging. It shows in the function's execution log. Log outcomes and identifiers; **do not log phone numbers, emails or tokens** — logs are retained and readable by every admin.
243
+ - A `try`/`catch` catches runtime exceptions (bad `.get(9)`, failed parse) and anything you `throw`. It does **not** catch a rejected write — see §5.
244
+ - **Calling another standalone function** returns whatever that function returned, but if its declared return type is `string`, a Map comes back as a **string** and must be `.toMap()`'d; declare `map` as the return type when you mean a Map, or parse defensively:
245
+ ```deluge
246
+ raw = standalone.TransactionUpsert(txnJson);
247
+ resultMap = Map();
248
+ try { resultMap = raw.toMap(); } catch (e) { }
249
+ if(resultMap.get("error") != null) { failed = failed + 1; }
250
+ ```
251
+ - Design a return contract and keep it. The convention that works: success → Map with the useful ids; failure → Map with `"error"`; something-worth-knowing → `"warning"`. Callers check `error` explicitly.
252
+ - **Execution limits (documented):** 5,000 statements executed per function (a loop body counts once per iteration), 75 function calls per function, 500 `sendmail` per user per day (15 MB attachments), 2,000 webhook and 2,000 integration tasks per user per day.
253
+
254
+ ## 9. Functions exposed over REST
255
+
256
+ A standalone function can be invoked over HTTP once REST access is enabled in the console (Setup → Functions → REST API — it cannot be enabled through the API). Such a function takes one parameter, `crmAPIRequest`, a Map of the whole request:
257
+
258
+ ```deluge
259
+ string standalone.Lookup(String crmAPIRequest)
260
+ {
261
+ req = crmAPIRequest.toMap();
262
+ body = req.get("body"); // POST body (Map when JSON)
263
+ params = req.get("params"); // query string
264
+ q = body.get("query");
265
+
266
+ resp = Map();
267
+ resp.put("status_code", 200);
268
+ resp.put("body", {"ok": true, "query": q});
269
+ return {"crmAPIResponse": resp};
270
+ }
271
+ ```
272
+ Return `{"crmAPIResponse": {...}}` with `status_code` and `body`. Anything else is wrapped as text.
273
+
274
+ ## 10. Known runtime traps (each cost real time)
275
+
276
+ | You wrote | What happens | Do instead |
277
+ |---|---|---|
278
+ | `getJSONType(x)` | COMPILATION_ERROR — function does not exist **(verified)** | `x.toString().startsWith("[")` / `"{"` |
279
+ | `s.replaceAll("[^0-9]", "", true)` | pattern treated as literal; nothing replaced **(verified)** | 2-argument form for regex |
280
+ | `if(resp != null) { ...success... }` after `upsert` | rejected writes counted as success **(verified)** | check `resp.get("status") == "error"` |
281
+ | `// header` above the signature, pushed via API | rejected as a bad package declaration **(verified)** | comments inside the body only |
282
+ | grep -i missing for function usage | live callers reported dead **(verified)** | names are case-insensitive |
283
+ | `zoho.ai.parsePhoneNumber(n, country)` result used as-is | returns a populated object flagged `"invalid"` for a bad number **(verified)** | reject when `international_format` is `"invalid"` |
284
+ | `m.containsKey("k")` | no such method | `m.containKey("k")` |
285
+ | `l.get(l.size())` | index out of range (0-based) | `l.get(l.size() - 1)` |
286
+ | `{}` for an empty collection | ambiguous | `Map()` or `List()` |
287
+ | `id == "123"` with a Long id | false | `id.toString() == "123"` or `id == 123.toLong()` |
288
+ | `"2026-09-01".toDate()` | date literals want single quotes | `'2026-09-01'.toDate()` |
289
+ | Secrets in string literals | leaked in every revision/export | `zoho.crm.getOrgVariable`, or a Connection |
290
+ | `while(...)` / `switch` | do not exist | list loops, `else if` |
291
+
292
+ ## 11. Workflow with the `zone` CLI
293
+
294
+ ```bash
295
+ zone crm pull -o ./metadata # real field API names, picklists, layouts, every function
296
+ zone crm fn pull my_function -o ./functions # fetch a .ds
297
+ zone crm fmt ./functions/my_function.ds -w # canonical formatting
298
+ zone crm fn push ./functions/my_function.ds # save (strips a leading // header, surfaces COMPILATION_ERROR line numbers that match your file)
299
+ zone crm fn test my_function --args '{"leadId":"123"}' # run a script on the live org
300
+ zone crm fn rest-api my_function # is it REST-exposed? invoke URL
301
+ zone crm fn invoke my_function --args '{"id":"1"}' # call the SAVED function over REST
302
+ ```
303
+
304
+ **Before pushing:**
305
+ 1. Every code path returns a value (non-`void`) and every statement ends with `;`.
306
+ 2. No `while`, `switch`, `getJSONType`, `containsKey`.
307
+ 3. Every `zoho.crm.*` write checks `status == "error"`.
308
+ 4. Every `invokeurl` result is checked before use.
309
+ 5. No secrets or PII in literals or `info` lines.
310
+ 6. Field names came from the org's metadata, not from memory.
311
+ 7. Stays inside the limits: 5,000 executed statements, 75 calls, 2,000 external calls/day.
312
+ 8. Test with `zone crm fn test` on one record before wiring it to a workflow or schedule.