דוגמה — סנכרון יוצא עם עימוד
“תמשכו לנו את כל הלקוחות למחסן הנתונים” נשמעת כמו משימה של חצי שעה, והיא באמת כזו — עד שהטבלה גדולה מ‑1,000 שורות, וכל עוד אף אחד לא עובד במערכת בזמן שאתם קוראים. שני התנאים האלה כמעט אף פעם לא מתקיימים.
העמוד הזה הוא המימוש שעובד גם כשהם לא: לולאת עימוד עם סמן המשך, בחירת שדות מצומצמת, נסיגה על שגיאות, ונקודת המשך שנשמרת — כך שהריצה הבאה היא סנכרון דלתא ולא משיכה מלאה נוספת.
לפני שמושכים — לספור
Section titled “לפני שמושכים — לספור”ספירה היא הקריאה הזולה ביותר ב‑API, והיא ההבדל בין “שתי קריאות” ל‑“180 קריאות ותכנון”.
curl -sS -G "https://api.mbapps.co.il/parse/classes/Accounts" \ -H "X-Parse-Application-Id: $MB_APP_ID" \ -H "X-Parse-API-Key: $MB_API_KEY" \ --data-urlencode 'where={"IsAccount":true}' \ --data-urlencode 'limit=0' \ --data-urlencode 'count=1'const params = new URLSearchParams({ where: '{"IsAccount":true}', limit: '0', count: '1',});
const res = await fetch(`https://api.mbapps.co.il/parse/classes/Accounts?${params}`, { headers: { 'X-Parse-Application-Id': process.env.MB_APP_ID, 'X-Parse-API-Key': process.env.MB_API_KEY, },});const data = await res.json(); // { results: [], count: 1842 }import os
import requests
res = requests.get( "https://api.mbapps.co.il/parse/classes/Accounts", headers={ "X-Parse-Application-Id": os.environ["MB_APP_ID"], "X-Parse-API-Key": os.environ["MB_API_KEY"], }, params={ "where": '{"IsAccount":true}', "limit": 0, "count": 1, },)data = res.json() # { "results": [], "count": 1842 }<?php$query = http_build_query([ 'where' => '{"IsAccount":true}', 'limit' => 0, 'count' => 1,]);
$ch = curl_init("https://api.mbapps.co.il/parse/classes/Accounts?$query");curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ 'X-Parse-Application-Id: ' . getenv('MB_APP_ID'), 'X-Parse-API-Key: ' . getenv('MB_API_KEY'), ],]);$response = curl_exec($ch);curl_close($ch);$data = json_decode($response, true); // { "results": [], "count": 1842 }{ "results": [], "count": 1842 }limit=0&count=1 מחזיר את הספירה בלי לשלוף שורה אחת. 1,842 רשומות = שתי קריאות של 1,000. 184,000 = 184 קריאות, וכדאי לפרוס אותן על זמן.
הפרמטרים שהדוגמה משתמשת בהם
Section titled “הפרמטרים שהדוגמה משתמשת בהם”| פרמטר | הערך בדוגמה | למה |
|---|---|---|
where |
סמן ההמשך: createdAt > cursor |
מה שהופך את הריצה השנייה לדלתא ולא למשיכה חוזרת |
keys |
רשימה מפורשת של שדות | בטבלה עם 130 שדות זה ההבדל בין מגה‑בייטים לקילו‑בייטים |
order |
createdAt (עולה) |
מיון יציב — השדה הזה לא משתנה אחרי היצירה |
limit |
1000 |
גודל העמוד המומלץ. השרת אינו אוכף תקרה (limit=2000 מחזיר את כל השורות, ללא חיתוך), אבל עמודים גדולים יותר רק מאריכים תשובה וזיכרון. תמיד מפורש |
include |
— | לא בשימוש כאן. ראו מתי כן |
skip |
— | לא בשימוש בכוונה. ראו למה לא skip |
הפירוט המלא של כל פרמטר ואופרטור: שאילתות.
למה לא skip עמוק
Section titled “למה לא skip עמוק”הדפוס הנאיבי הוא limit=1000&skip=0, ואז skip=1000, וכן הלאה. הוא עובד מצוין על טבלה שאף אחד לא נוגע בה, ונשבר בשקט על טבלה חיה:
| מה קורה בזמן המשיכה | מה זה עושה ל‑skip |
|---|---|
| איש מכירות יוצר רשומה חדשה | כל הרשומות אחרי נקודת ההכנסה מוסטות באחת — רשומה אחת תיקרא פעמיים או תדולג |
מישהו מעדכן רשומה, והמיון הוא לפי -updatedAt |
הרשומה קופצת לתחילת התוצאות — ואתם מדלגים על אחרת |
| הטבלה גדולה | skip=150000 יקר גם בביצועים: השרת עדיין עובר על מה שדילג עליו |
הכי גרוע: אין שגיאה. הייצוא מסתיים בהצלחה עם פחות רשומות ממה שיש, ומישהו יגלה את זה חודשיים אחר כך.
הדפוס העמיד — סמן לפי createdAt
Section titled “הדפוס העמיד — סמן לפי createdAt”במקום לספור שורות, זוכרים איפה עצרנו. createdAt נקבע ביצירה ולא משתנה, ולכן הוא סמן יציב:
עמוד 1: where {} order createdAt limit 1000 → 1000 שורות, האחרונה נוצרה ב-2026-03-04T09:12:44.081Zעמוד 2: where { createdAt: { $gt: 2026-03-04T09:12:44.081Z } } ← אותו מיון, אותו limit → 842 שורות ⇒ פחות מ-limit ⇒ זה היה העמוד האחרוןשתי הנקודות שהופכות את זה לעמיד: הכנסה חדשה תמיד נוחתת אחרי הסמן (כי createdAt שלה גדול יותר), ורשומה שהתעדכנה לא זזה במיון. לדוגמה, בטבלה של 1,100 שורות אותה לולאה עם limit=500 מושכת 500 + 500 + 100, וריצה שנייה מנקודת ההמשך מחזירה 0 שורות. עם PAGE_SIZE = 1000 שבקוד למטה אותה טבלה תתחלק ל‑1000 + 100.
הקוד — בחמש שפות
Section titled “הקוד — בחמש שפות”הדוגמה מושכת את כל הלידים שנוצרו מאז נקודת ההמשך השמורה, שומרת את הסמן אחרי כל עמוד, ומטפלת בשגיאות עם נסיגה אקספוננציאלית.
<?phpdeclare(strict_types=1);
const API = 'https://api.mbapps.co.il/parse';const PAGE_SIZE = 1000; // המקסימום של שכבת ה-RESTconst CHECKPOINT = __DIR__ . '/.sync-cursor'; // נקודת ההמשך בין ריצות
function headers(): array{ return [ 'X-Parse-Application-Id: ' . getenv('MB_APP_ID'), 'X-Parse-API-Key: ' . getenv('MB_API_KEY'), ];}
/** קריאת GET עם נסיגה אקספוננציאלית על 429 ו-5xx. */function getWithBackoff(string $url, int $attempts = 5): array{ $delay = 0.5; for ($i = 0; $i < $attempts; $i++) { $ch = curl_init($url); curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 30, CURLOPT_HTTPHEADER => headers(), ]); $body = curl_exec($ch); $status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE); curl_close($ch);
if ($body !== false && $status === 200) { return json_decode($body, true) ?: []; } if ($body !== false && $status < 500 && $status !== 429) { throw new RuntimeException("REST $status: $body"); // 400/401 — לא מנסים שוב } usleep((int) (($delay + mt_rand(0, 300) / 1000) * 1_000_000)); $delay *= 2; } throw new RuntimeException('REST failed after retries');}
/** משיכת כל הלידים שנוצרו אחרי הסמן. מחזירה את הסמן החדש. */function syncLeads(?string $cursor): ?string{ while (true) { $where = $cursor ? ['createdAt' => ['$gt' => ['__type' => 'Date', 'iso' => $cursor]]] : new stdClass();
$qs = http_build_query([ 'where' => json_encode($where, JSON_UNESCAPED_UNICODE), 'keys' => 'Name,PhoneNumber,Email,City,LeadStatusId,createdAt', 'order' => 'createdAt', // מיון יציב — לא -createdAt 'limit' => PAGE_SIZE, // תמיד מפורש ]);
$rows = getWithBackoff(API . "/classes/Accounts?$qs")['results'] ?? []; if (!$rows) return $cursor;
foreach ($rows as $row) { upsertIntoWarehouse($row); // הכתיבה שלכם, לפי objectId }
$cursor = $rows[count($rows) - 1]['createdAt']; // מחרוזת ISO שטוחה file_put_contents(CHECKPOINT, $cursor); // שמירה אחרי כל עמוד
if (count($rows) < PAGE_SIZE) return $cursor; // העמוד האחרון }}
$cursor = is_file(CHECKPOINT) ? trim((string) file_get_contents(CHECKPOINT)) : null;syncLeads($cursor !== '' ? $cursor : null);"""סנכרון יוצא — משיכת כל הלידים שנוצרו מאז הריצה הקודמת."""import jsonimport osimport pathlibimport randomimport timefrom collections.abc import Iterator
import requests
API = "https://api.mbapps.co.il/parse"PAGE_SIZE = 1000 # המקסימום של שכבת ה-RESTCHECKPOINT = pathlib.Path(".sync-cursor") # נקודת ההמשך בין ריצותH = { "X-Parse-Application-Id": os.environ["MB_APP_ID"], "X-Parse-API-Key": os.environ["MB_API_KEY"],}
def get_with_backoff(url: str, params: dict, attempts: int = 5) -> dict: """GET עם נסיגה אקספוננציאלית על 429 ו-5xx בלבד.""" delay = 0.5 for attempt in range(attempts): try: r = requests.get(url, headers=H, params=params, timeout=30) except requests.RequestException: if attempt == attempts - 1: raise else: if r.status_code == 200: return r.json() if r.status_code < 500 and r.status_code != 429: r.raise_for_status() # 400/401 — אין טעם לנסות שוב time.sleep(delay + random.random() * 0.3) # jitter delay *= 2 raise RuntimeError("REST failed after retries")
def iter_since(table: str, cursor: str | None) -> Iterator[tuple[dict, str]]: """מייצר (רשומה, הסמן אחרי העמוד שלה) בסדר createdAt עולה.""" while True: where = ( {"createdAt": {"$gt": {"__type": "Date", "iso": cursor}}} if cursor else {} ) params = { "where": json.dumps(where), # requests מקודד ל-URL "keys": "Name,PhoneNumber,Email,City,LeadStatusId,createdAt", "order": "createdAt", # מיון יציב "limit": PAGE_SIZE, # תמיד מפורש } rows = get_with_backoff(f"{API}/classes/{table}", params)["results"] if not rows: return
cursor = rows[-1]["createdAt"] # מחרוזת ISO שטוחה for row in rows: yield row, cursor
if len(rows) < PAGE_SIZE: # העמוד האחרון return
def main() -> None: cursor = CHECKPOINT.read_text().strip() if CHECKPOINT.exists() else None last = cursor for row, page_cursor in iter_since("Accounts", cursor): upsert_into_warehouse(row) # הכתיבה שלכם, לפי objectId last = page_cursor if last and last != cursor: CHECKPOINT.write_text(last) # שמירה בסוף הריצה
if __name__ == "__main__": main()// סנכרון יוצא — משיכת כל הלידים שנוצרו מאז הריצה הקודמתimport { readFile, writeFile } from 'node:fs/promises';
const API = 'https://api.mbapps.co.il/parse';const PAGE_SIZE = 1000; // המקסימום של שכבת ה-RESTconst CHECKPOINT = '.sync-cursor'; // נקודת ההמשך בין ריצותconst H = { 'X-Parse-Application-Id': process.env.MB_APP_ID, 'X-Parse-API-Key': process.env.MB_API_KEY,};
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
async function getWithBackoff(url, attempts = 5) { let delay = 500; for (let i = 0; i < attempts; i++) { try { const res = await fetch(url, { headers: H, signal: AbortSignal.timeout(30_000) }); if (res.ok) return res.json(); if (res.status < 500 && res.status !== 429) { throw new Error(`REST ${res.status}: ${await res.text()}`); // לא מנסים שוב } } catch (err) { if (i === attempts - 1) throw err; } await sleep(delay + Math.random() * 300); // jitter delay *= 2; } throw new Error('REST failed after retries');}
async function* iterSince(table, cursor) { for (;;) { const where = cursor ? { createdAt: { $gt: { __type: 'Date', iso: cursor } } } : {};
const qs = new URLSearchParams({ where: JSON.stringify(where), // URLSearchParams מקודד עבורכם keys: 'Name,PhoneNumber,Email,City,LeadStatusId,createdAt', order: 'createdAt', // מיון יציב limit: String(PAGE_SIZE), // תמיד מפורש });
const { results } = await getWithBackoff(`${API}/classes/${table}?${qs}`); if (!results.length) return;
cursor = results[results.length - 1].createdAt; // מחרוזת ISO שטוחה for (const row of results) yield { row, cursor };
if (results.length < PAGE_SIZE) return; // העמוד האחרון }}
const start = await readFile(CHECKPOINT, 'utf8').catch(() => null);let last = start;
for await (const { row, cursor } of iterSince('Accounts', start?.trim() || null)) { await upsertIntoWarehouse(row); // הכתיבה שלכם, לפי objectId last = cursor;}
if (last && last !== start) await writeFile(CHECKPOINT, last, 'utf8');// סנכרון יוצא — משיכת כל הלידים שנוצרו מאז הריצה הקודמתusing System.Net;using System.Net.Http.Json;using System.Runtime.CompilerServices;using System.Text.Json;using System.Web;
public sealed class OutboundSync(HttpClient http){ private const string Api = "https://api.mbapps.co.il/parse"; private const int PageSize = 1000; // המקסימום של שכבת ה-REST private const string Checkpoint = ".sync-cursor"; // נקודת ההמשך בין ריצות
private async Task<JsonElement> GetWithBackoffAsync(string url, CancellationToken ct) { var delay = TimeSpan.FromMilliseconds(500); for (var attempt = 0; attempt < 5; attempt++) { using var res = await http.GetAsync(url, ct); if (res.IsSuccessStatusCode) return await res.Content.ReadFromJsonAsync<JsonElement>(cancellationToken: ct);
var status = (int)res.StatusCode; if (status < 500 && res.StatusCode != HttpStatusCode.TooManyRequests) throw new HttpRequestException($"REST {status}: {await res.Content.ReadAsStringAsync(ct)}");
await Task.Delay(delay + TimeSpan.FromMilliseconds(Random.Shared.Next(300)), ct); delay *= 2; } throw new HttpRequestException("REST failed after retries"); }
/// <summary>מייצר את כל הרשומות שנוצרו אחרי הסמן, בסדר createdAt עולה.</summary> public async IAsyncEnumerable<(JsonElement Row, string Cursor)> IterSinceAsync( string table, string? cursor, [EnumeratorCancellation] CancellationToken ct = default) { while (true) { var where = cursor is null ? "{}" : $$"""{"createdAt":{"$gt":{"__type":"Date","iso":"{{cursor}}"}}}""";
var qs = HttpUtility.ParseQueryString(string.Empty); qs["where"] = where; // מקודד אוטומטית qs["keys"] = "Name,PhoneNumber,Email,City,LeadStatusId,createdAt"; qs["order"] = "createdAt"; // מיון יציב qs["limit"] = PageSize.ToString(); // תמיד מפורש
var doc = await GetWithBackoffAsync($"{Api}/classes/{table}?{qs}", ct); var results = doc.GetProperty("results"); var count = results.GetArrayLength(); if (count == 0) yield break;
cursor = results[count - 1].GetProperty("createdAt").GetString()!; foreach (var row in results.EnumerateArray()) yield return (row, cursor);
if (count < PageSize) yield break; // העמוד האחרון } }
public async Task RunAsync(CancellationToken ct = default) { var start = File.Exists(Checkpoint) ? (await File.ReadAllTextAsync(Checkpoint, ct)).Trim() : null; var last = start;
await foreach (var (row, cursor) in IterSinceAsync("Accounts", start, ct)) { await UpsertIntoWarehouseAsync(row, ct); // הכתיבה שלכם, לפי objectId last = cursor; }
if (last is not null && last != start) await File.WriteAllTextAsync(Checkpoint, last, ct); }}הכותרות נקבעות פעם אחת על ה‑HttpClient שמוזרק פנימה:
http.DefaultRequestHeaders.Add("X-Parse-Application-Id", appId);http.DefaultRequestHeaders.Add("X-Parse-API-Key", apiKey); // ממנהל סודותhttp.Timeout = TimeSpan.FromSeconds(30);// מסך מצב — קורא מהשרת שלכם, לא מ-api.mbapps.co.ilasync function renderSyncStatus() { const res = await fetch('/api/sync/status'); if (!res.ok) throw new Error('שגיאה בטעינת מצב הסנכרון');
const { lastRunAt, cursor, rowsPulled, lastError } = await res.json();
document.getElementById('sync-status').textContent = lastError ? `הריצה האחרונה נכשלה: ${lastError}` : `סונכרנו ${rowsPulled} רשומות · נקודת המשך: ${cursor} · ${lastRunAt}`;}
setInterval(renderSyncStatus, 30_000);renderSyncStatus();ואם באמת צריך להציג נתונים מהמערכת בדף — נקודת קצה בשרת שמחזירה בדיוק את מה שמותר לפרסם, ומגבילה את הכמות בעצמה:
// בדפדפןconst leads = await fetch('/api/leads?limit=20').then((r) => r.json());מה קורה כשהטבלה משתנה תוך כדי המשיכה
Section titled “מה קורה כשהטבלה משתנה תוך כדי המשיכה”| מה קורה במערכת | מה קורה למשיכה שלכם | האם צריך לעשות משהו |
|---|---|---|
| נוצרות רשומות חדשות | הן נוחתות אחרי הסמן ונקראות בעמוד הבא, או בריצה הבאה | לא — זו התנהגות רצויה |
| רשומה קיימת מתעדכנת | היא לא תיקרא שוב — הסמן הוא createdAt |
כן, אם אתם צריכים עדכונים. ראו סנכרון עדכונים |
| רשומה נמחקת | המשיכה לעולם לא תדע על כך | כן. ראו מה משיכה לא תראה |
| שתי רשומות נוצרות באותה מילישנייה | $gt על הסמן ידלג על השנייה |
כן, בטבלאות עמוסות. ראו למטה |
| ההרשאות של הטבלה משתנות באמצע | חלק מהשורות פשוט לא יחזרו — בלי שגיאה | כן — השוו את הספירה בסוף |
# וריאנט עמיד: $gte + סינון כפילויות לפי objectIdwhere = {"createdAt": {"$gte": {"__type": "Date", "iso": cursor}}} if cursor else {}rows = get_with_backoff(f"{API}/classes/{table}", params)["results"]fresh = [r for r in rows if r["objectId"] not in seen_ids_of_previous_page]seen_ids_of_previous_page = {r["objectId"] for r in rows if r["createdAt"] == rows[-1]["createdAt"]}סנכרון עדכונים — updatedAt במקום createdAt
Section titled “סנכרון עדכונים — updatedAt במקום createdAt”אם המערכת היעד צריכה גם שינויים ולא רק רשומות חדשות, החליפו את שדה הסמן:
| הסמן | תופס | לא תופס | המחיר |
|---|---|---|---|
createdAt |
רשומות חדשות | עדכונים, מחיקות | סמן יציב לחלוטין |
updatedAt |
חדשות וגם מעודכנות | מחיקות | אותה רשומה תחזור בכל ריצה שבה עודכנה — חתכו כפילויות לפי objectId בצד שלכם, ובצעו upsert ולא insert |
עם updatedAt המיון כבר אינו יציב: רשומה שמתעדכנת בזמן המשיכה זזה קדימה. הסמן מגן עליכם מ“דילוג”, אבל לא מ“קריאה כפולה” — ולכן הכתיבה בצד שלכם חייבת להיות אידמפוטנטית, לפי objectId.
מה משיכה לעולם לא תראה
Section titled “מה משיכה לעולם לא תראה”בנוסף, שדות שההרשאות מסתירות פשוט לא יחזרו, בלי שגיאה, ורשומה שאינה נראית להרשאה שלכם תיראה כמו רשומה שלא קיימת (code 101). עם API Key זה לא רלוונטי (הוא עוקף הכול — קריאה ל‑_User עם API Key מחזירה 200), אבל עם Session Token — כן. ראו אימות.
ומה שכן מגן עליכם ממחיקות חלקית: במערכת חדשה מופעל checkDependenciesBeforeDelete, ולכן מחיקה של רשומה שיש אליה Pointer נחסמת (400 code 119 Pointer to Object … exists in table Tasks in key AccountId. Delete is not allowed.). רשומות שאין אליהן הפניות עדיין נעלמות בשקט.
include — מתי שווה לשלם עליו
Section titled “include — מתי שווה לשלם עליו”הדוגמה לא משתמשת ב‑include. הסיבה: היא שולפת את שדה ה‑Pointer (LeadStatusId) כמצביע, ומתרגמת אותו במחסן מול טבלת הסטטוסים שנמשכה פעם אחת.
| המצב | מה עדיף |
|---|---|
| טבלת lookup קטנה וקבועה (סטטוסים, מקורות, ערים) | משכו אותה פעם אחת לזיכרון ותרגמו אצלכם. אל תשלמו include על כל שורה |
| Pointer לטבלה גדולה שאתם צריכים ממנה 2–3 שדות | include — הוא חוסך קריאת N+1 לכל שורה |
| Pointer שאתם רק מעבירים הלאה כמזהה | בלי include — המזהה כבר בתשובה |
המגבלות שהדוגמה מכבדת
Section titled “המגבלות שהדוגמה מכבדת”| המגבלה | הערך | איך הדוגמה מתמודדת |
|---|---|---|
limit ברירת מחדל ב‑REST |
100 | מציינת limit במפורש, תמיד |
limit מרבי ב‑REST |
אין תקרה נאכפת — limit=2000 מחזיר את כל השורות שנמצאו; 1000 הוא המלצה |
PAGE_SIZE = 1000 — גודל עמוד, לא מגבלת שרת |
where הוא JSON שעובר URL‑encoding |
— | קידוד דרך הספרייה, לא שרשור מחרוזות |
$or תקף רק ברמה העליונה (או בתוך $and) |
— | $or בתוך שדה → 400 code 107 bad constraint: $or; בתוך $and עובד. לא בשימוש כאן; ראו שאילתות |
| אגרגציה | — | /parse/aggregate/… (כולל distinct) אינו קיים בפריסה — 404 גם עם Master Key. הנתיב הפנימי /parse/classes-aggregate/… עונה גם ל‑API Key. הדוגמה מושכת שורות; לסכומים — אגרגציות |
| מגבלות קצב | אין מגבלה מתועדת, ולא נצפתה — 150 קריאות במקביל (1.2 שניות) ועוד 100 עוקבות (2.2 שניות) חזרו כולן 200, בלי 429 |
נסיגה אקספוננציאלית + jitter, והשהיה מרצון בטעינות גדולות |
צ’קליסט לפני שמריצים על ייצור
Section titled “צ’קליסט לפני שמריצים על ייצור”- ספרתם קודם (
limit=0&count=1) ויודעים כמה קריאות זה - ה‑
whereמקודד על ידי הספרייה — JSON שבור ב‑whereמחזיר 500 ונראה כמו תקלת שרת שראוי לנסות שוב -
limitמפורש בכל קריאה - המיון הוא
createdAtעולה — לא-createdAtולא שדה שמשתנה -
keysמכיל רק את השדות שאתם באמת צריכים - הסמן נשמר במקום קבוע ששורד הפעלה מחדש, ונכתב אחרי כל עמוד
- הכתיבה אצלכם היא upsert לפי
objectId, לא insert - יש נסיגה אקספוננציאלית על 429 ו‑5xx — ואין ניסיון חוזר על 4xx אחרות
- יש timeout על כל קריאה
- הוחלט מה עושים עם מחיקות (ארכיון בצד המערכת, או השוואת ספירות)
- המפתח מגיע ממשתנה סביבה או ממנהל סודות — לא מהקוד
- שאילתות — כל פרמטר, כל אופרטור, וסעיף העימוד המלא
- עבודה עם רשומות — הצד הכותב
- אגרגציות — כשצריך סכום ולא רשימה
- מגבלות ומכסות — כל המספרים במקום אחד
- יומנים וראיות — מה מתועד ומה לא, כולל מחיקות
- מצא‑או‑צור ורשומה מקושרת — הצד הכותב של אותה אינטגרציה
- דוגמאות קוד — הקטלוג המלא