דוגמה — מצא‑או‑צור ורשומה מקושרת
כמעט כל אינטגרציית שרת מיישמת את אותה זרימה: מגיע אירוע מבחוץ (הזמנה מחנות, פנייה ממערכת ישנה, שורה מקובץ), וצריך לוודא שיש איש קשר, ואז ליצור לו רשומה מקושרת.
הטאבים של Node.js ו‑Python בעמוד הזה הורצו כמו שהם מול טננט בדיקות (23.08.2026): הריצה הראשונה מצאה שלוש רשומות קיימות לאותו טלפון, לקחה את הראשונה ורשמה אזהרה; הריצה השנייה החזירה את אותו objectId; וגרסת ה‑batch יצרה 7 משימות בקבוצות. מה שנמצא שונה מהמקור תוקן בעמוד.
זה נשמע כמו שלושה שלבים, ובפועל יש שם ארבע החלטות שקובעות אם האינטגרציה תעבוד או תייצר כפילויות: איך מחפשים (הטלפון חי בכמה פורמטים), מה עושים כשיש יותר מהתאמה אחת, איך נראה Pointer בשכבה הזו, ומה קורה לטריגרים כשמריצים את זה על 5,000 שורות.
הזרימה
Section titled “הזרימה”1. חיפוש GET /parse/classes/Accounts?where={"$or":[…כל פורמטי הטלפון…, {"Email":…}]}&limit=5 ├─ 0 תוצאות → שלב 2 ├─ 1 תוצאה → יש objectId, לשלב 3 └─ 2+ תוצאות → כפילות קיימת במערכת: קחו את הראשונה ו**דווחו**2. יצירה POST /parse/classes/Accounts {"Name":…, "PhoneNumber":…, "IsAccount":false} → 201 {"objectId":"xK9mP2qRsT"}3. קישור POST /parse/classes/Tasks {"AccountId":{"__type":"Pointer",…}}למה חיפוש‑לפני‑יצירה הוא באחריותכם
Section titled “למה חיפוש‑לפני‑יצירה הוא באחריותכם”הבעיה שמפילה את רוב המימושים: פורמטי הטלפון
Section titled “הבעיה שמפילה את רוב המימושים: פורמטי הטלפון”באותה טבלה חיים במקביל 0501234567, 972501234567, +972-50-1234567 ו‑050-1234567. הפלטפורמה אינה מנרמלת אותם: שאילתה על פורמט אחד לא תמצא את השאר, תחזיר מערך ריק — ותיצור איש קשר כפול (אומת: רשומה שנשמרה כ‑05… לא נמצאה בשאילתה על 972… ולא על 05X-…).
מנוע זיהוי הכפילויות של שכבת הטפסים שואל את כל הווריאציות במפורש. אינטגרציה שעושה חיפוש בעצמה חייבת לעשות אותו הדבר:
{ "$or": [ { "PhoneNumber": "0501234567" }, { "PhoneNumber": "972501234567" }, { "PhoneNumber": "+972501234567" }, { "PhoneNumber": "050-1234567" }, { "PhoneNumber": "+972-50-1234567" }, { "Email": "israel@example.co.il" } ]}Pointer — שתי צורות, שתי שכבות
Section titled “Pointer — שתי צורות, שתי שכבות”זו הטעות מספר 1 של מי שעובר מטפסים ל‑REST, ולהפך:
| השכבה | הצורה של ערך Pointer |
|---|---|
REST (/parse/classes/…) | אובייקט עטוף: {"__type":"Pointer","className":"Accounts","objectId":"xK9mP2qRsT"} |
טפסים (getlead / web2table) | מחרוזת חשופה בת 10 תווים: "xK9mP2qRsT" |
בעמוד הזה אנחנו ב‑REST — כלומר אובייקט עטוף. מחרוזת חשופה ב‑REST לא תתאים לשום דבר בשאילתה (אומת: 200 עם מערך ריק, בלי שגיאה), ובכתיבה לשדה Pointer קיים היא נדחית: 400 {"code":111,"error":"schema mismatch for Tasks.AccountId; expected Pointer<Accounts> but got String"}. אותו דבר לתאריך: מחרוזת ISO שטוחה בשדה Date → code 111. רק בשם שדה חדש מחרוזת כזו תיצור שדה String בשקט. ראו סוגי נתונים.
הקוד — בחמש שפות
Section titled “הקוד — בחמש שפות”<?phpdeclare(strict_types=1);
const API = 'https://api.mbapps.co.il/parse';
function headers(): array{ return [ 'Content-Type: application/json', 'X-Parse-Application-Id: ' . getenv('MB_APP_ID'), 'X-Parse-API-Key: ' . getenv('MB_API_KEY'), ];}
function call(string $method, string $path, ?array $body = null, array $query = []): array{ $url = API . $path . ($query ? '?' . http_build_query($query) : ''); $ch = curl_init($url); curl_setopt_array($ch, [ CURLOPT_CUSTOMREQUEST => $method, CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 30, CURLOPT_HTTPHEADER => headers(), CURLOPT_POSTFIELDS => $body === null ? null : json_encode($body, JSON_UNESCAPED_UNICODE), ]); $raw = curl_exec($ch); $status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE); curl_close($ch);
if ($raw === false) throw new RuntimeException('שגיאת רשת מול ה-API'); if ($status >= 400) throw new RuntimeException("REST $status: $raw"); return json_decode($raw, true) ?: [];}
/** כל הפורמטים שבהם מספר ישראלי עשוי להיות שמור. */function phoneVariants(string $raw): array{ $digits = preg_replace('/\D/', '', $raw) ?? ''; if (str_starts_with($digits, '972')) $digits = '0' . substr($digits, 3); if ($digits === '') return [];
$intl = '972' . substr($digits, 1); return array_values(array_unique([ $digits, // 0501234567 $intl, // 972501234567 '+' . $intl, // +972501234567 substr($digits, 0, 3) . '-' . substr($digits, 3), // 050-1234567 '+972-' . substr($digits, 1, 2) . '-' . substr($digits, 3), // +972-50-1234567 ]));}
/** מחזירה objectId של איש קשר קיים, או null. */function findAccount(string $phone, ?string $email): ?string{ $or = array_map(static fn ($v) => ['PhoneNumber' => $v], phoneVariants($phone)); if ($email) $or[] = ['Email' => $email]; if (!$or) return null;
$res = call('GET', '/classes/Accounts', null, [ 'where' => json_encode(['$or' => $or], JSON_UNESCAPED_UNICODE), 'keys' => 'objectId,Name,PhoneNumber,Email', 'limit' => 5, // תמיד מפורש ]);
$hits = $res['results'] ?? []; if (count($hits) > 1) { error_log(sprintf('duplicate accounts for one contact: %d matches', count($hits))); } return $hits[0]['objectId'] ?? null;}
function createAccount(string $name, string $phone, ?string $email): string{ $body = array_filter([ 'Name' => $name, 'PhoneNumber' => $phone, 'Email' => $email, 'IsAccount' => false, // false = ליד · true = לקוח ], static fn ($v) => $v !== null && $v !== '');
return call('POST', '/classes/Accounts', $body)['objectId'];}
function createTask(string $accountId, string $title, string $dueIso): string{ $body = [ 'Name' => $title, 'Date' => ['__type' => 'Date', 'iso' => $dueIso], // אובייקט, לא מחרוזת 'AccountId' => ['__type' => 'Pointer', // אובייקט, לא מזהה חשוף 'className' => 'Accounts', 'objectId' => $accountId], ]; return call('POST', '/classes/Tasks', $body)['objectId'];}
// הזרימה המלאה$accountId = findAccount('0501234567', 'israel@example.co.il') ?? createAccount('ישראל ישראלי', '0501234567', 'israel@example.co.il');
createTask($accountId, 'התקשר ללקוח', '2026-09-15T10:30:00.000Z');"""מצא-או-צור איש קשר, ואז רשומה מקושרת."""import jsonimport loggingimport osimport re
import requests
API = "https://api.mbapps.co.il/parse"H = { "Content-Type": "application/json", "X-Parse-Application-Id": os.environ["MB_APP_ID"], "X-Parse-API-Key": os.environ["MB_API_KEY"],}log = logging.getLogger("crm-sync")
def phone_variants(raw: str) -> list[str]: """כל הפורמטים שבהם מספר ישראלי עשוי להיות שמור.""" digits = re.sub(r"\D", "", raw or "") if digits.startswith("972"): digits = "0" + digits[3:] if not digits: return []
intl = "972" + digits[1:] seen = [ digits, # 0501234567 intl, # 972501234567 f"+{intl}", # +972501234567 f"{digits[:3]}-{digits[3:]}", # 050-1234567 f"+972-{digits[1:3]}-{digits[3:]}", # +972-50-1234567 ] return list(dict.fromkeys(seen)) # ייחודי, בסדר יציב
def find_account(phone: str, email: str | None) -> str | None: """מחזירה objectId של איש קשר קיים, או None.""" or_clause = [{"PhoneNumber": v} for v in phone_variants(phone)] if email: or_clause.append({"Email": email}) if not or_clause: return None
r = requests.get( f"{API}/classes/Accounts", headers=H, params={ "where": json.dumps({"$or": or_clause}), "keys": "objectId,Name,PhoneNumber,Email", "limit": 5, # תמיד מפורש }, timeout=30, ) r.raise_for_status()
hits = r.json()["results"] if len(hits) > 1: log.warning("duplicate accounts for one contact: %d matches", len(hits)) return hits[0]["objectId"] if hits else None
def create_account(name: str, phone: str, email: str | None) -> str: body = {"Name": name, "PhoneNumber": phone, "Email": email, "IsAccount": False} body = {k: v for k, v in body.items() if v not in (None, "")}
r = requests.post(f"{API}/classes/Accounts", headers=H, json=body, timeout=30) r.raise_for_status() return r.json()["objectId"]
def create_task(account_id: str, title: str, due_iso: str) -> str: body = { "Name": title, "Date": {"__type": "Date", "iso": due_iso}, # אובייקט, לא מחרוזת "AccountId": { # אובייקט, לא מזהה חשוף "__type": "Pointer", "className": "Accounts", "objectId": account_id, }, } r = requests.post(f"{API}/classes/Tasks", headers=H, json=body, timeout=30) r.raise_for_status() return r.json()["objectId"]
account_id = find_account("0501234567", "israel@example.co.il") or create_account( "ישראל ישראלי", "0501234567", "israel@example.co.il")create_task(account_id, "התקשר ללקוח", "2026-09-15T10:30:00.000Z")// מצא-או-צור איש קשר, ואז רשומה מקושרתconst API = 'https://api.mbapps.co.il/parse';const H = { 'Content-Type': 'application/json', 'X-Parse-Application-Id': process.env.MB_APP_ID, 'X-Parse-API-Key': process.env.MB_API_KEY,};
async function call(method, path, { body, query } = {}) { const qs = query ? `?${new URLSearchParams(query)}` : ''; const res = await fetch(`${API}${path}${qs}`, { method, headers: H, body: body ? JSON.stringify(body) : undefined, signal: AbortSignal.timeout(30_000), }); if (!res.ok) throw new Error(`REST ${res.status}: ${await res.text()}`); return res.json();}
/** כל הפורמטים שבהם מספר ישראלי עשוי להיות שמור. */function phoneVariants(raw) { let digits = (raw ?? '').replace(/\D/g, ''); if (digits.startsWith('972')) digits = `0${digits.slice(3)}`; if (!digits) return [];
const intl = `972${digits.slice(1)}`; return [...new Set([ digits, // 0501234567 intl, // 972501234567 `+${intl}`, // +972501234567 `${digits.slice(0, 3)}-${digits.slice(3)}`, // 050-1234567 `+972-${digits.slice(1, 3)}-${digits.slice(3)}`, // +972-50-1234567 ])];}
/** מחזירה objectId של איש קשר קיים, או null. */async function findAccount(phone, email) { const or = phoneVariants(phone).map((v) => ({ PhoneNumber: v })); if (email) or.push({ Email: email }); if (!or.length) return null;
const { results } = await call('GET', '/classes/Accounts', { query: { where: JSON.stringify({ $or: or }), keys: 'objectId,Name,PhoneNumber,Email', limit: '5', // תמיד מפורש }, });
if (results.length > 1) { console.warn(`duplicate accounts for one contact: ${results.length} matches`); } return results[0]?.objectId ?? null;}
async function createAccount(name, phone, email) { const body = Object.fromEntries( Object.entries({ Name: name, PhoneNumber: phone, Email: email }).filter(([, v]) => v), ); body.IsAccount = false; // false = ליד · true = לקוח
const { objectId } = await call('POST', '/classes/Accounts', { body }); return objectId;}
async function createTask(accountId, title, dueIso) { const body = { Name: title, Date: { __type: 'Date', iso: dueIso }, // אובייקט, לא מחרוזת AccountId: { // אובייקט, לא מזהה חשוף __type: 'Pointer', className: 'Accounts', objectId: accountId, }, }; const { objectId } = await call('POST', '/classes/Tasks', { body }); return objectId;}
const accountId = (await findAccount('0501234567', 'israel@example.co.il')) ?? (await createAccount('ישראל ישראלי', '0501234567', 'israel@example.co.il'));
await createTask(accountId, 'התקשר ללקוח', '2026-09-15T10:30:00.000Z');using System.Net.Http.Json;using System.Text.Json;using System.Text.RegularExpressions;using System.Web;
public sealed class CrmClient(HttpClient http, ILogger log){ private const string Api = "https://api.mbapps.co.il/parse";
/// <summary>כל הפורמטים שבהם מספר ישראלי עשוי להיות שמור.</summary> public static IReadOnlyList<string> PhoneVariants(string raw) { var digits = Regex.Replace(raw ?? "", @"\D", ""); if (digits.StartsWith("972")) digits = "0" + digits[3..]; if (digits.Length == 0) return [];
var intl = "972" + digits[1..]; return new[] { digits, // 0501234567 intl, // 972501234567 "+" + intl, // +972501234567 $"{digits[..3]}-{digits[3..]}", // 050-1234567 $"+972-{digits[1..3]}-{digits[3..]}", // +972-50-1234567 }.Distinct().ToArray(); }
/// <summary>מחזירה objectId של איש קשר קיים, או null.</summary> public async Task<string?> FindAccountAsync(string phone, string? email, CancellationToken ct = default) { var or = PhoneVariants(phone) .Select(v => new Dictionary<string, string> { ["PhoneNumber"] = v }) .ToList(); if (!string.IsNullOrWhiteSpace(email)) or.Add(new Dictionary<string, string> { ["Email"] = email }); if (or.Count == 0) return null;
var qs = HttpUtility.ParseQueryString(string.Empty); qs["where"] = JsonSerializer.Serialize(new Dictionary<string, object> { ["$or"] = or }); qs["keys"] = "objectId,Name,PhoneNumber,Email"; qs["limit"] = "5"; // תמיד מפורש
var doc = await http.GetFromJsonAsync<JsonElement>($"{Api}/classes/Accounts?{qs}", ct); var results = doc.GetProperty("results");
if (results.GetArrayLength() > 1) log.LogWarning("duplicate accounts for one contact: {Count} matches", results.GetArrayLength());
return results.GetArrayLength() == 0 ? null : results[0].GetProperty("objectId").GetString(); }
public async Task<string> CreateAccountAsync(string name, string phone, string? email, CancellationToken ct = default) { var body = new Dictionary<string, object?> { ["Name"] = name, ["PhoneNumber"] = phone, ["Email"] = email, ["IsAccount"] = false, // false = ליד · true = לקוח }; foreach (var k in body.Where(kv => kv.Value is null or "").Select(kv => kv.Key).ToList()) body.Remove(k);
var res = await http.PostAsJsonAsync($"{Api}/classes/Accounts", body, ct); res.EnsureSuccessStatusCode(); var created = await res.Content.ReadFromJsonAsync<JsonElement>(cancellationToken: ct); return created.GetProperty("objectId").GetString()!; }
public async Task<string> CreateTaskAsync(string accountId, string title, string dueIso, CancellationToken ct = default) { var body = new Dictionary<string, object> { ["Name"] = title, ["Date"] = new { __type = "Date", iso = dueIso }, // אובייקט, לא מחרוזת ["AccountId"] = new { __type = "Pointer", // אובייקט, לא מזהה חשוף className = "Accounts", objectId = accountId }, };
var res = await http.PostAsJsonAsync($"{Api}/classes/Tasks", body, ct); res.EnsureSuccessStatusCode(); var created = await res.Content.ReadFromJsonAsync<JsonElement>(cancellationToken: ct); return created.GetProperty("objectId").GetString()!; }}
// הזרימה המלאהvar accountId = await crm.FindAccountAsync("0501234567", "israel@example.co.il") ?? await crm.CreateAccountAsync("ישראל ישראלי", "0501234567", "israel@example.co.il");
await crm.CreateTaskAsync(accountId, "התקשר ללקוח", "2026-09-15T10:30:00.000Z");הכותרות נקבעות פעם אחת על ה‑HttpClient:
http.DefaultRequestHeaders.Add("X-Parse-Application-Id", appId);http.DefaultRequestHeaders.Add("X-Parse-API-Key", apiKey); // ממנהל סודותhttp.Timeout = TimeSpan.FromSeconds(30);// בדפדפן — קוראים לשרת שלכם, והוא מריץ את מצא-או-צורconst res = await fetch('/api/enroll', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ phone: '0501234567', email: 'israel@example.co.il', name: 'ישראל ישראלי', task: 'התקשר ללקוח', }),});
if (!res.ok) throw new Error('שמירת הפנייה נכשלה');const { accountId, taskId } = await res.json();ואם מדובר בטופס ציבורי ולא בשרת שלכם — אל תממשו את הזרימה הזו בכלל: web2table עושה את מצא‑או‑צור בצד השרת, בלי מפתח, ועל כל פורמטי הטלפון. ראו הרשמה לקורס.
וריאנט batch — כמה רשומות בבת אחת
Section titled “וריאנט batch — כמה רשומות בבת אחת”כשצריך ליצור הרבה רשומות מקושרות (למשל: 300 משימות מעקב לאחר ייבוא), POST /parse/batch אורז כמה פעולות לבקשת HTTP אחת וחוסך סבבי רשת. ההמלצה היא עד 50 פעולות לבקשה.
<?php/** * יוצר עד 50 משימות בבקשה אחת. מחזיר [הצלחות, כישלונות]. * @param list<array{accountId:string,title:string,dueIso:string}> $items */function createTasksBatch(array $items): array{ if (count($items) > 50) { throw new InvalidArgumentException('batch: מומלץ עד 50 פעולות — חתכו בצד הלקוח (השרת אינו אוכף)'); }
$ops = array_map(static fn ($it) => [ 'method' => 'POST', 'path' => '/parse/classes/Tasks', // הנתיב כולל /parse — בלעדיו: 400 code 107 "cannot route batch path" 'body' => [ 'Name' => $it['title'], 'Date' => ['__type' => 'Date', 'iso' => $it['dueIso']], 'AccountId' => ['__type' => 'Pointer', 'className' => 'Accounts', 'objectId' => $it['accountId']], ], ], $items);
$res = call('POST', '/batch', ['requests' => $ops]);
$ok = $failed = []; foreach ($res as $i => $item) { if (isset($item['success'])) { $ok[] = $item['success']; } else { // שומרים את הבקשה המקורית לצד הכישלון — כדי שיהיה מה לנסות שוב $failed[] = ['request' => $ops[$i], 'error' => $item['error'] ?? null]; } } return [$ok, $failed];}
// חיתוך ל-50 והשהיה בין קבוצותfunction createAllTasks(array $items, float $pause = 0.3): array{ $created = $errors = []; foreach (array_chunk($items, 50) as $group) { [$ok, $bad] = createTasksBatch($group); $created = array_merge($created, $ok); $errors = array_merge($errors, $bad); usleep((int) ($pause * 1_000_000)); } return [$created, $errors];}import time
def create_tasks_batch(items: list[dict]) -> tuple[list, list]: """יוצר עד 50 משימות בבקשה אחת. מחזיר (הצלחות, כישלונות-עם-הבקשה-המקורית).""" assert len(items) <= 50, "batch: מומלץ עד 50 פעולות — חתכו בצד הלקוח (השרת אינו אוכף)"
ops = [ { "method": "POST", "path": "/parse/classes/Tasks", # הנתיב כולל /parse "body": { "Name": it["title"], "Date": {"__type": "Date", "iso": it["due_iso"]}, "AccountId": { "__type": "Pointer", "className": "Accounts", "objectId": it["account_id"], }, }, } for it in items ]
r = requests.post(f"{API}/batch", headers=H, json={"requests": ops}, timeout=60) r.raise_for_status() # תופס רק כשל של הבקשה כולה
ok, failed = [], [] for op, result in zip(ops, r.json()): if "success" in result: ok.append(result["success"]) else: # שומרים את הבקשה המקורית לצד הכישלון — כדי שיהיה מה לנסות שוב failed.append({"request": op, "error": result.get("error")}) return ok, failed
def create_all_tasks(items: list[dict], pause: float = 0.3) -> tuple[list, list]: created, errors = [], [] for i in range(0, len(items), 50): # חיתוך ל-50 ok, bad = create_tasks_batch(items[i:i + 50]) created += ok errors += bad time.sleep(pause) # האטה מרצון return created, errorsconst sleep = (ms) => new Promise((r) => setTimeout(r, ms));
/** יוצר עד 50 משימות בבקשה אחת. מחזיר { ok, failed }. */async function createTasksBatch(items) { if (items.length > 50) throw new Error('batch: מומלץ עד 50 פעולות — חתכו בצד הלקוח (השרת אינו אוכף)');
const ops = items.map((it) => ({ method: 'POST', path: '/parse/classes/Tasks', // הנתיב כולל /parse body: { Name: it.title, Date: { __type: 'Date', iso: it.dueIso }, AccountId: { __type: 'Pointer', className: 'Accounts', objectId: it.accountId }, }, }));
const results = await call('POST', '/batch', { body: { requests: ops } });
const ok = []; const failed = []; results.forEach((result, i) => { if (result.success) ok.push(result.success); // שומרים את הבקשה המקורית לצד הכישלון — כדי שיהיה מה לנסות שוב else failed.push({ request: ops[i], error: result.error }); }); return { ok, failed };}
async function createAllTasks(items, pause = 300) { const created = []; const errors = []; for (let i = 0; i < items.length; i += 50) { // חיתוך ל-50 const { ok, failed } = await createTasksBatch(items.slice(i, i + 50)); created.push(...ok); errors.push(...failed); await sleep(pause); // האטה מרצון } return { created, errors };}/// <summary>יוצר עד 50 משימות בבקשה אחת. מחזיר הצלחות וכישלונות בנפרד.</summary>public async Task<(List<JsonElement> Ok, List<(object Request, JsonElement Error)> Failed)> CreateTasksBatchAsync(IReadOnlyList<TaskItem> items, CancellationToken ct = default){ if (items.Count > 50) throw new ArgumentException("batch: מומלץ עד 50 פעולות — חתכו בצד הלקוח (השרת אינו אוכף)");
var ops = items.Select(it => (object)new { method = "POST", path = "/parse/classes/Tasks", // הנתיב כולל /parse body = new Dictionary<string, object> { ["Name"] = it.Title, ["Date"] = new { __type = "Date", iso = it.DueIso }, ["AccountId"] = new { __type = "Pointer", className = "Accounts", objectId = it.AccountId }, }, }).ToList();
var res = await http.PostAsJsonAsync($"{Api}/batch", new { requests = ops }, ct); res.EnsureSuccessStatusCode(); // תופס רק כשל של הבקשה כולה
var body = await res.Content.ReadFromJsonAsync<JsonElement>(cancellationToken: ct);
var ok = new List<JsonElement>(); var failed = new List<(object, JsonElement)>(); var i = 0; foreach (var item in body.EnumerateArray()) { if (item.TryGetProperty("success", out var success)) ok.Add(success); // שומרים את הבקשה המקורית לצד הכישלון — כדי שיהיה מה לנסות שוב else failed.Add((ops[i], item.GetProperty("error"))); i++; } return (ok, failed);}
public record TaskItem(string AccountId, string Title, string DueIso);לדף שצריך להפעיל ייבוא — הפעילו משימה בצד שרת והציגו את מצבה:
// בדפדפן — מפעילים ייבוא ומציגים התקדמותconst { jobId } = await fetch('/api/import', { method: 'POST' }).then((r) => r.json());
const timer = setInterval(async () => { const { done, created, failed } = await fetch(`/api/import/${jobId}`).then((r) => r.json()); document.getElementById('progress').textContent = `נוצרו ${created} · נכשלו ${failed}`; if (done) clearInterval(timer);}, 2000);:::
כל כתיבה מפעילה טריגרים
Section titled “כל כתיבה מפעילה טריגרים”יצירת רשומה מפעילה את אוטומציות ה‑data change של הטבלה: מיילים, הודעות WhatsApp, יצירת רשומות נוספות. Master Key עוקף הרשאות — לא אוטומציות (אומת: POST /parse/classes/Accounts עם Master Key הפעיל את טריגר ההמרה המובנה — LeadConversionDate ו‑updatedByTrigger נכתבו).
ביצירה בודדת זה בדרך כלל רצוי. בייבוא של 5,000 שורות זו סופה: 5,000 שרשראות עצמאיות, כל אחת עד 3 רמות עומק, ואם ברמה הראשונה יושבת פעולת “שלח WhatsApp” — יצאו 5,000 הודעות ללקוחות אמיתיים.
| הדרך | ההגבלה |
|---|---|
POST /parse/batch |
אין דגל השתקה כלל (אומת: skipTriggers ברמת הבקשה מתעלמים ממנו) |
הכלי Create-Many של MCP עם skipTriggers: true |
מכובד רק באימות Master Key (אומת: רשומת Accounts עם IsAccount: true נוצרה בלי שטריגר ההמרה כתב LeadConversionDate; בלי הדגל — הטריגר רץ). ראו כלי נתונים |
| כיבוי זמני של הטריגרים בממשק | דורש תיאום עם מי שמחזיק את האוטומציות — ואל תשכחו להדליק בחזרה |
מה לעשות לפני ייבוא גדול: לשאול מי מחזיק את הטריגרים של טבלת היעד, ולהריץ קודם קבוצה של חמש שורות ולראות מה יצא. מגבלות ומכסות.
מרוץ בין שתי בקשות במקביל
Section titled “מרוץ בין שתי בקשות במקביל”מצא‑או‑צור אינו אטומי: בין החיפוש ליצירה יש חלון. שני תהליכים שמעבדים את אותו איש קשר בו‑זמנית ימצאו שניהם “אין”, ושניהם ייצרו — ותקבלו כפילות שאף אחד לא ביקש.
| הפתרון | מתי מתאים |
|---|---|
| תור בעל עובד יחיד לכל אינטגרציה | הפתרון הפשוט והנכון ברוב המקרים |
| מפתח פירוק לפי איש קשר (מפצלים תורים לפי טלפון מנורמל) | כשצריך מקביליות אמיתית |
ExternalId + חיפוש חד‑משמעי |
מקטין את החלון, לא סוגר אותו |
| בדיקה חוזרת אחרי היצירה ומיזוג אם נוצרה כפילות | תיקון בדיעבד, לא מניעה |
ExternalId — לוותר על ניחוש טלפונים
Section titled “ExternalId — לוותר על ניחוש טלפונים”אם אתם מסנכרנים ממערכת חיצונית שיש בה מזהה משלה, כתבו אותו לשדה משלכם (למשל ExternalId) ביצירה. מאותו רגע הזיהוי הוא שאילתה אחת חד‑משמעית, בלי ווריאציות ובלי $or:
{ "ExternalId": "SHOP-84213" }זה גם מה שהופך ריצה חוזרת לאידמפוטנטית: מצאתם — עדכנו; לא מצאתם — צרו. ראו עבודה עם רשומות.
מקרי קצה
Section titled “מקרי קצה”| מה קורה | למה | מה עושים |
|---|---|---|
| השאילתה מחזירה מערך ריק למרות שהרשומה קיימת | סינון Pointer במחרוזת חשופה במקום באובייקט, או תאריך שנשלח שטוח ב‑where |
שאילתות |
404 code 101 Object not found |
הרשומה לא קיימת — או שההרשאות מסתירות אותה | עם API Key זה תמיד “לא קיימת” (אומת: GET על מזהה מומצא → 404 code 101) |
| נמצאו 3 התאמות לאותו טלפון | כפילות שכבר קיימת במערכת מלפני האינטגרציה | קחו את הראשונה, ודווחו. מיזוג הוא החלטה אנושית |
| הרשומה נוצרה, ה‑Pointer ריק | ערך Pointer שאינו אובייקט עטוף בשכבת ה‑REST | סוגי נתונים |
| מחיקת איש קשר שיש לו משימות נכשלת | בטננט חדש מופעל checkDependenciesBeforeDelete: 400 code 119 Pointer to Object … exists in table Tasks in key AccountId. Delete is not allowed. (אומת) |
זו הגנה רצויה. העדיפו ארכיון על מחיקה. כשההגדרה כבויה — המצביע נשאר תלוי באוויר. עבודה עם רשומות |
500 Internal server error על שאילתה |
where שאינו JSON תקין (תוצאה טיפוסית של שרשור ידני) — השרת מחזיר 500 במקום 400 (באג examples-02). רווח גולמי ב‑URL → 400 Bad Request בדף HTML |
תנו לספרייה לקודד, ואל תנסו שוב אוטומטית על 500 לפני שבדקתם את ה‑where |
| בדיוק 100 תוצאות, תמיד | לא צוין limit — זו ברירת המחדל של REST |
ציינו תמיד |
צ’קליסט
Section titled “צ’קליסט”- החיפוש מכסה את כל פורמטי הטלפון, ולא רק את הפורמט שהמערכת השולחת
- יותר מהתאמה אחת → נרשם ביומן, לא נבלע
- המפתח מגיע ממשתנה סביבה או ממנהל סודות
-
limitמפורש בכל שאילתה - ערך Pointer הוא אובייקט עטוף (שכבת REST), ותאריך הוא
{"__type":"Date","iso":…} -
batch— חיתוך ל‑50 בצד הלקוח (השרת אינו אוכף),pathמתחיל ב‑/parse, וכל איבר בתשובה נבדק בנפרד (וגם איבר שגיאה בליcode) - הכישלונות נשמרים לצד הבקשה המקורית, כדי שיהיה מה לנסות שוב
- נשאלה השאלה מי מחזיק את הטריגרים של טבלת היעד — לפני ההרצה, לא אחריה
- הריצה הראשונה על ייצור הייתה על חמש שורות, ונבדק מה יצא
- עבודה עם רשומות — CRUD, פעולות אטומיות, והזרימה הקנונית
- שאילתות —
$or, אופרטורים, ומלכודות הסינון - batch — 50 פעולות, והתשובה שחייבים לקרוא
- מגבלות ומכסות — טריגרים, מזהים, וקצב
- זיהוי כפילויות — מה שכבת הטפסים עושה בשבילכם, ואתם צריכים לחקות
- סנכרון יוצא עם עימוד — הצד הקורא של אותה אינטגרציה
- דוגמאות קוד — הקטלוג המלא