ch
Feedback
ToCode

ToCode

前往频道在 Telegram

טיפים קצרים למתכנתים מאת ינון פרק

显示更多
1 419
订阅者
无数据24 小时
无数据7
-230
帖子存档
ToCode
1 419
# היום למדתי: שליפת מידע מ JSON באמצעות JSON-ptr את xpath אני מכיר הרבה זמן אבל רק היום גיליתי שיש לו מקבילה וותיקה שתעזור לנו למשוך מידע בקלות מאוביקטי JSON. זה נקרא jsonptr וכבר יש עבורו מסמך איפיון של IETF שנקרא rfc6901 ומספר מימושים ב JavaScript ובשפות אחרות. בואו נראה איך זה עובד. ## למה צריך למשוך מידע מ JSON נתון אוביקט ה JSON הבא:
const data = {
    items: [ 'one', 'two', 'three' ],
    colors: [
        { name: 'red', score: 7 },
        { name: 'blue', score: 9 },
        { name: 'green', score: 15 },
    ],
};
ואנחנו רוצים לגלות מה השם של הצבע הראשון. ביום רגיל היינו יכולים לכתוב את קוד ה JavaScript הבא:
data.colors[0].name;
אבל: 1. הקוד יישבר אם מבנה האוביקט שונה 2. נדרש שינוי קוד כל פעם שמשנים את מבנה האוביקט. ## איך JSON ptr עוזר לפתור את הבעיה האיפיון של JSON Ptr הוא מאוד פשוט: אנחנו מעבירים לו מחרוזת שמייצגת נתיב בתוך ה JSON והוא מחזיר לנו את הערך שנמצא שם. זה קצת כמו XPath רק בלי כל היכולות השוות. הקוד הבא משתמש בספריית json-ptr כדי למשוך את השם של הצבע הראשון מאוביקט המידע:
import { JsonPointer } from "json-ptr";
const ptr = JsonPointer.create("/colors/0/name");
const name = ptr.get(data);
אז נכון אפשר היה לשמור כל מחרוזת בתור "נתיב" ולכתוב בעצמנו את הקוד שמחפש באוביקט או להשתמש בספריה כמו object-dig כדי לקבל את אותה תוצאה, אבל בכל זאת זה נחמד שהמחרוזת שלנו כתובה לפי תקן ושיהיה אפשר בקלות להחליף מימוש. ## מה עושים עם זה בחיים האמיתיים מנגנון של גישה לערך ב JSON לפי ייצוג מחרוזתי של הנתיב אליו יכול לעזור לנו במקרים הבאים: 1. כשה API שלי צריך לתמוך במספר מבנים של אוביקטים (למשל אחרי שינוי גירסה), אני יכול לשמור את הנתיב המתאים לכל גירסה וכך פונקציית הטיפול בשרת יכולה להתמודד עם אוביקטים משני הסוגים בצורה גנרית. 2. כשאני רוצה לאפשר למשתמשים לבחור ערכים מסוימים ב JSON ואני רוצה לשמור (בשרת או ב local storage) את הבחירה שלהם, כך שגם אם ה JSON ישתנה הבחירה תתעדכן בהתאם.

ToCode
1 419
הדבר הנכון לעשות היה לאפשר למשתמשים להגדיר 2FA באמצעות אפליקציית Authenticator או באמצעות סמס או קוד קולי, כדי שמשתמשים שכן רוצים להגן על עצמם יוכלו לבחור באופציה המאובטחת. בנוסף יש להגדיר מדיניות סיסמאות שמחייבת משתמשים לבחור סיסמה ארוכה וייחודית ולהחליף סיסמה כל מספר חודשים, לזהות התנהגות חשודה של משתמשים או כניסה ממקומות לא שגרתיים או ממכשירים לא בנוסף יש להגדיר מדיניות סיסמאות שמחייבת משתמשים לבחור סיסמה ארוכה וייחודית ולהחליף סיסמה כל מספר חודשים. כדאי גם להוסיף מנגנונים המזהים התנהגות חשודה של משתמשים או כניסה ממקומות לא שגרתיים או ממכשירים לא שגרתיים.

ToCode
1 419
# מה יותר מאובטח: סמס או משתמש וסיסמה? במייל שקיבלתי מחברת החשמל לאחרונה הם סיפרו שמחליפים את מנגנון האבטחה ובמקום להמשיך להיכנס עם שם משתמש וסיסמה כמו שהיינו עושים הכניסה מעכשיו תהיה רק באמצעות סמס לטלפון הנייד, כי זו צורה יותר מאובטחת. בגלל שאני בטוח שההתלבטות לא ייחודית רק לחברת החשמל, בואו נפתח את זה כדי להיות בטוחים. ## כניסה עם שם משתמש וסיסמה המנגנון הרגיל של כניסה לאתרים אומר שכל משתמש בוחר לעצמו שם משתמש וסיסמה וכל מי שיודע את שני הפרטים האלה יכול להיכנס לאתר. מבחינת האתר מלבד הסיסמה ושם המשתמש אין עוד כלום שמזהה את הבן אדם, ולכן אם מישהו יגלה את הסיסמה שלכם הוא יוכל להזדהות בשמכם באותו אתר. אנחנו יודעים כבר הרבה זמן שמערכת כניסה המבוססת על שם משתמש וסיסמה לנתונים פרטיים אינה מספיק מאובטחת וזאת מהסיבות הבאות: 1. אנשים לא יודעים לשמור סיסמאות בסוד: רוב האנשים יתנו את הסיסמה שלהם בטלפון כשנציג שירות דמיוני מתקשר וחלק מהאנשים יתנו את הסיסמה בתמורה לחטיף. 2. גם אם היינו יודעים לשמור סיסמאות מחשבים הם מנוולים: קל מאוד להכניס וירוס שמאזין ללחיצות מקלדת ושולח סיסמאות למרכז שליטה ובקרה. 3. גם אם היינו יודעים להתמודד עם וירוסים אתרים הם מנוולים: אנחנו נוטים לבחור את אותה סיסמה לכל האתרים, למרות שחלק מהאתרים הם כנראה זדוניים או פשוט כתובים רע ולכן במוקדם או במאוחר הסיסמה שלנו תיחשף. כניסה עם שם וסיסמה יכולה לעבוד רק אם המשתמשים יודעים שעליהם לבחור סיסמאות ארוכות ומאובטחות וייחודיות לכל אתר. באתר https://haveibeenpwned.com/ תוכלו לגלות לאן התגלגלו השם והסיסמה שלכם למגוון אתרים אליהם נרשמתם. הבדיקה שם היא חוויה מטלטלת. לכן אתרים רבים ובמיוחד אתרים השומרים מידע פרטי עברו להשתמש בסכימה מתוחכמת יותר שנקראת 2FA או Two Factor Authentication. במצב עבודה כזה האתר יבקש מכם להזדהות עם שם משתמש וסיסמה, ולאחר מכן אם אתם מתחברים ממכשיר שאתם לא נוהגים להתחבר ממנו או ממקום גיאוגרפי שאתם לא נוהגים להיות בו, האתר גם יבקש מכם לאמת את זהותכם באמצעי נוסף. האמצעי הפופולרי ביותר למימוש 2FA הוא שליחת סמס לטלפון הנייד. זהו אמצעי נוח כי כולם יודעים לקבל סמס, אבל הוא גם לא מאוד מאובטח כי סמסים הם לא מאובטחים מטבעם: חברת הטלפון וגם אפליקציות שהתקנו על הטלפון יכולים בקלות לקרוא את ההודעות. לכן היום נהוג להמליץ לממש 2FA באמצעות אפליקציית Authenticator כמו Google Authenticator. המנגנון של ה Authenticator הוא שהאפליקציה מגרילה כל חצי דקה קוד פסאודו אקראי חדש אבל שנגזר מתוך איזשהו Seed ראשוני. בהנחה שה Seed אצלי על המכשיר ובאתר הוא זהה, אז הקודים שהאפליקציה תייצר תמיד יהיו זהים אצלי ובאתר וייחודיים. הכנסת הקוד מה Authenticator מבטיחה שאכן יש לי גישה לאותו מנגנון יצירת קודים איתו נרשמתי לאתר. ## כניסה עם סמס אחרי טירוף ה 2FA אתרים רבים (ובמיוחד אתרי גופים ציבוריים ואתרי ממשלה) שמו לב שאנשים שמחים להכניס לאתר קודים שהם מקבלים בהודעות סמס, ולכן החליטו לקחת את העסק הזה צעד קדימה ולוותר לגמרי על הסיסמה. מספיק שיש להם את מספר הטלפון שלך ואת מספר תעודת הזהות שלך, והם יכולים לייצר מנגנון הזדהות המבוסס על תעודת זהות וגישה למספר הטלפון. סמסים חשופים לבעיות האבטחה הבאות: 1. חברות הטלפון ממש גרועות בלשמור על סוד: באמצעות הנדסה חברתית האקרים מצליחים לחטוף מספרי טלפון (ניוד מספר) ועד שהקורבן מבין מה קרה כבר חטפו לו את קוד האימות. 2. אנחנו ממש גרועים בלזהות רמאויות, ואנשים רבים מוסרים את קוד האימות שהם קיבלו ל"נציג שירות" דמיוני שמתקשר אליהם 3. לא כולם מקבלים סמסים. במקרה שהסמס לא נשלח מנגנון הגיבוי הוא שליחת הקוד באמצעות שיחה קולית. אם אני נמצא בדיוק במקום בלי קליטה אותה שיחה קולית עלולה להגיע לתא הקולי שלי שכנראה לא ידעתי על קיומו ולא שיניתי לו מעולם את הסיסמה. 4. בגלל שאנחנו אוהבים לחבר את המכשירים אחד לשני, יש גישה מהמון מקומות להודעות הסמס שלנו. מספיק מכשיר אחד שנגנב או אבד כדי שהאקר יוכל להשתמש בו ולקרוא משם את ההודעות. 5. אפליקציות (זדוניות או שכתובות רע) על הטלפון יכולות לקרוא את ההודעות ולדווח למרכז שליטה ובקרה זדוני. ## אז מה עדיף? התשובה הכואבת היא שאם אבטחת החשבון האישי של המשתמשים עומדת בראש מעייניכם לעבור מהזדהות באמצעות שם וסיסמה להזדהות באמצעות סמס זה חסר משמעות. אף אחת משתי השיטות לבדה לא מספיק טובה כדי להגן על פרטים אישיים.

ToCode
1 419
# מתקפת הכלים לפעמים (לעתים קרובות מדי) אני מרגיש שהכלים שאנחנו בוחרים פועלים בכוונה נגדנו, או לפחות שהאנשים שכותבים את הכלים והתיעוד שלהם לא לוקחים בחשבון שיש מתכנתים שצריכים להשתמש ולתחזק את השטויות שהם כותבים. ניקח דוגמה קטנה מפרויקט next.js. ל next יש מנגנון של קונפיגורציית זמן ריצה שכבר בדף התיעוד שלהם הם ממליצים לא להשתמש בו. בכל זאת זה קיים וגם עם דוגמת הקוד הבאה:
import getConfig from 'next/config'
import Image from 'next/image'

// Only holds serverRuntimeConfig and publicRuntimeConfig
const { serverRuntimeConfig, publicRuntimeConfig } = getConfig()
// Will only be available on the server-side
console.log(serverRuntimeConfig.mySecret)
// Will be available on both server-side and client-side
console.log(publicRuntimeConfig.staticFolder)

function MyImage() {
  return (
    <div>
      <Image
        src={`${publicRuntimeConfig.staticFolder}/logo.png`}
        alt="logo"
        layout="fill"
      />
    </div>
  )
}

export default MyImage
נשים לב לקריאה ל getConfig שמופיעה בתחילת הקובץ ולעובדה שמדובר בקובץ קומפוננטה רגיל כך שהציפיה היא שכל קומפוננטה שצריכה את הקונפיגורציה תפעיל את getConfig בתוך הקוד שלה. עכשיו מה - כשאני רוצה לקחת פרויקט כזה ולהעביר אותו לסביבת בדיקות הקוד כמובן נכשל. בסביבת הבדיקות אין לי שרת next רץ כי אני רק רוצה לבדוק את קוד צד הלקוח. זה לא סוף העולם אני יכול לעדכן את הקוד ולהחליף את הפונקציה ב mock, ויש אפילו התיחסות לזה ב Issue שנסגר ב next עצמה, ממנו קיבלתי את הרעיון. אבל, וזו הנקודה החשובה להיום, עבודה על פרויקט ווב כבר מזמן לא "קשורה" רק לטכנולוגיה אחת. מצד אחד כל כלי חדש בפרויקט מקל לנו על החיים מכיוון מסוים, ומצד שני ככל שמוסיפים יותר כלים כך מקבלים יותר התנגשויות ביניהם, ופיתרון ההתנגשויות עלול לגזול אפילו יותר זמן ממה שהיה לוקח לפתור את הבעיה המקורית בלי הכלי המדליק.

ToCode
1 419
עדכנו את טבלת ההזמנות כך שעמודת category תכיל את הערך 1 להזמנות קטנות מ 200, הערך 2 להזמנות בין 200 ל 500, הערך 3 להזמנות בסכום בין 500 ל 1,000 והערך 4 להזמנות בסכום מעל 1,000. זוכרים את knex.raw? בשילוב עם סימן הגרש ההפוך של JavaScript התרגיל הזה נפתר כמעט כולו ב SQL:
  await knex('commande').update({
    'category': knex.raw(`
      CASE
        WHEN cache_prix_total BETWEEN 0 AND 200 THEN 1
        WHEN cache_prix_total BETWEEN 200 AND 500 THEN 2
        WHEN cache_prix_total BETWEEN 500 AND 1000 THEN 4
        WHEN cache_prix_total > 1000 THEN 4
      END
    `),
  });
## משימה 12: יצירת טבלה חדשה צרו טבלה בשם commande_category ובה שתי עמודות: עמודה מספרית עם הערך של הקטגוריה, ועמודת טקסט עם תיאור הקטגוריה. השם של הפונקציה ליצירת טבלה ב knex הוא כבר הרבה יותר הגיוני והקוד לפיתרון נראה כך:
  await knex.schema.createTable('commande_category', function(table) {
    table.integer('category');
    table.string('description');
  });
## משימה 13: הכנסת ערכים במכה מלאו את הטבלה החדשה בתיאורי 4 הקטגוריות שיצרנו. הכנסה של 4 ערכים היא הזדמנות מצוינת להכיר את batchInsert של knex:
  await knex.batchInsert('commande_category', [
    { category: 1, description: '< 200' },
    { category: 2, description: 'Between 200 and 500' },
    { category: 3, description: 'Between 500 and 1000' },
    { category: 4, description: '> 1000' },
  ]);
היא עושה בדיוק מה שאתם מדמיינים ואם אתם יודעים יותר טוב מקנקס מה טוב בשבילכם תוכלו לקבוע מה יהיה גודל הצ'אנק שיישלח לבסיס הנתונים (ברירת המחדל היא 1,000). ## משימה 14: מחיקה מחקו את כל ההזמנות הישנות יותר מ 1.2.2019, ועבור כל הזמנה מחקו גם את השורות המתאימות בטבלת commande_ligne. הפונקציה del היא הדרך למחוק רשומות. תחילה נשתמש ב Sub Query כדי למחוק את כל ההזמנות מ commande_ligne:
  await knex('commande_ligne')
    .whereIn('commande_id', function() {
      this.
      select('id').
      from('commande').
      where('date_achat', '<', knex.raw("DATE('2019-02-1')"));
    }).del();
חשוב לשים לב בבניית ה whereIn שהפונקציה לא צריכה להחזיר ערך אלא בונה את השאילתה באמצעות שימוש באוביקט this ש knex מעביר לה. לאחר מכן אפשר להמשיך ולמחוק את כל ההזמנות מטבלת commande:
  await knex('commande').
    where('date_achat', '<', knex.raw("DATE('2019-02-1')")).
    del();
## לסיכום העבודה עם knex תהיה מאוד נוחה למתכנתים שמכירים טוב SQL ורוצים דרך קלה לכתוב שאילתות בצורה שגם תשתלב יפה בתוך קוד התוכנית, גם תאפשר Code Reuse וגם תאפשר יחסית בקלות לעבור לבסיס נתונים אחר. קנקס לא יגיד לכם איך לבנות את הקוד ולכן נוח להשתמש בו כמעט בכל מערכת. בצד השלילי הפוקוד רק על ה SQL אומר שאין לנו פינוקים כמו וולידציות וטיפול אוטומטי ב Associations שאנחנו יכולים לקבל מ ORM-ים אמיתיים כמו לדוגמה Sequelize.

ToCode
1 419
  const sol5 = await knex.
    select('commande.id').
    sum('commande_ligne.prix_total as prix_total').
    select('commande.date_achat', 'client.prenom', 'client.nom').
    from('commande').
    join('commande_ligne', 'commande.id', '=', 'commande_ligne.commande_id').
    join('client', 'client.id', '=', 'commande.client_id').
    groupBy('commande.id');

  console.log(sol5);
הזמנות ריקות לא יופיעו בתוצאות בגלל הבחירה ב INNER JOIN (זו ברירת המחדל). קנקס תומך גם בג'וין משמאל ומימין עם הפונקציות leftJoin ו rightJoin. ## משימה 6: שמירת המחיר הכולל הוסיפו לטבלת ההזמנות את המחיר הכולל של כל הזמנה בתוך עמודת cache_prix_total. המשימה הזאת היא היחידה שנתקעתי עליה מתוך ה-14 פה בפוסט, בגלל שביום יום אני עובד על PostgreSQL. מסתבר שבפעולת עדכון של MySQL אי אפשר להשתמש בטבלה אותה מעדכנים בתוך Sub Query. אחרי הרבה משחקים הבנתי איך לארגן מחדש את השאילתות כדי שלא נצטרך והנה התוצאה:
  await knex('commande').
    join(
      knex('commande_ligne').
        sum('commande_ligne.prix_total as price').
        select('commande_id').
        groupBy('commande_id').
        as('commande_lignes')
    , 'commande_lignes.commande_id', 'commande.id').
    update({
      'cache_prix_total': knex.raw('CAST(commande_lignes.price AS FLOAT)'),
    });
השאילתה המתאימה ב SQL נראית כך:
UPDATE `commande` INNER join 
    (SELECT
        sum(`commande_ligne`.`prix_total`) as `price`,
        `commande_id`
     FROM `commande_ligne`
     GROUP BY `commande_id`)
     AS `commande_lignes`
     ON `commande_lignes`.`commande_id` = `commande`.`id`
     SET `cache_prix_total` = CAST(commande_lignes.price AS FLOAT)
מה שיוצר לנו את השאילתה הפנימית ב JOIN זה ההעברה של אוביקט knex בתור פרמטר ראשון לפונקציה join במקום שם של טבלה (כמו שראינו בדוגמאות קודמות). נקודה מבלבלת בתחביר היא שבגירסת ה knex אני מפעיל את as מתוך השאילתה הפנימית, אבל ב SQL הוא נמצא אחרי הסוגריים. וכמובן החבר שלנו knex.raw נמצא שם בשביל שהקריאה ל CAST תעבור טוב לבסיס הנתונים. ## משימה 7: רווח חודשי הציגו את הסכום הכולל של ההזמנות שנכנסו בכל חודש. הפונקציה שמיד קופצת לראש היא DATE_FORMAT והפיתרון נראה כך:
  const sol7 = await knex('commande').
    select(knex.raw("DATE_FORMAT(date_achat, '%Y_%m') as date")).
    sum('cache_prix_total as total_orders').
    groupBy('date');

  console.log(sol7);
## משימה 8: סדר לפי מצאו את 10 הלקוחות שיצרו את ההזמנות הכי גדולות במערכת והציגו את מזהי הלקוחות וסכום כלל ההזמנות מכל אחד מהם. מאחר ובטבלת ההזמנות כבר הוספתי את עמודת cache_prix_total נשאר רק לסכום את הערך הזה לכל ההזמנות של כל לקוח ולהציג את 10 הלקוחות עבורם התוצאה היא הגבוהה ביותר:
  const sol8 = await knex('commande').
    select('client_id').
    sum('cache_prix_total as client_sum').
    groupBy('client_id').
    orderBy('client_sum', 'desc').
    limit(10);

  console.log(sol8);
## משימה 9: סכום הזמנות בכל יום הציגו את סכום ההזמנות שהתקבל בכל יום. בהנחה שלא טעיתי בתרגום השאלה הזאת דומה בצורה מחשידה למשימה 7. בכל מקרה זה היה הפיתרון שלי:
  const sol9 = await knex('commande').
    select(knex.raw("DATE_FORMAT(date_achat, '%Y_%m_%d') as date")).
    sum('cache_prix_total as total_orders').
    groupBy('date_achat');

  console.log(sol9);
## משימה 10: יצירת עמודה חדשה הוסיפו עמודה בשם category מסוג מספר לטבלת ההזמנות. קנקס כולל לא רק בונה שאילתות אלא גם מנגנון לעדכון הסכימה, כלומר יצירה ועדכון של טבלאות. בשביל להוסיף עמודה אנחנו ניגשים לאוביקט schema של knex ומפעילים את הפונקציה עם השם המבלבל table:
  await knex.schema.table('commande', function(table) {
    table.integer('category');
  });
וזה קצת מבלבל כי מצד אחד אנחנו לא מתארים את כל מבנה הטבלה אלא רק את מה שצריך לשנות, אבל התחביר מספיק פשוט בשביל שלא נהיה קשים איתם. ## משימה 11: עדכון הקטגוריה בהזמנות

ToCode
1 419
const knex = require('knex')({
  client: 'mysql2',
  connection: {
    host : '127.0.0.1',
    user : 'root',
    password : 'my-secret-pw',
    database : 'myapp_test'
  }
});
בעולם האמיתי נרצה לשמור את הסיסמה מחוץ לקוד ולגשת אליה באמצעות משתנה סביבה. את פקודת החיבור מפעילים פעם אחת ובכל שאר התוכנית משתמשים באוביקט הגלובאלי knex כדי ליצור שאילתות ולהריץ אותן מול בסיס הנתונים שבחרתם. קנקס משתמש ב Promises ולכן נוח לעבוד איתו בתחביר של async/await. הקוד הבא הוא מבנה של תוכנית מלאה שמתחברת לבסיס הנתונים ומדפיסה את כל הנתונים מטבלת client:
const knex = require('knex')({
  client: 'mysql2',
  connection: {
    host : '127.0.0.1',
    user : 'root',
    password : 'my-secret-pw',
    database : 'myapp_test'
  }
});


async function main() {
  // prints all the users details
  const users = await knex('client').select('*');
  console.log(users);

  knex.destroy();
}

main();
הקריאה ל knex.destroy בסוף סוגרת את כל החיבורים לבסיס הנתונים ומאפשרת לתוכנית להסתיים. בסעיפים הבאים אציג רק את פקודות knex עצמן שאני כותב. בשביל להפעיל כל פקודה צריך לכתוב אותה בתוך פוקנציית ה main לפני ה destroy. ## משימה 1: מיוריאל מצאו בטבלת הלקוחות את Muriel וודאו שהסיסמה שלה היא test11. הסיסמאות נשמרות ב SHA1 בעמודת password בטבלת הלקוחות (וכבר התחלנו ברגל שמאל בכל מה שקשור לאבטחת מידע - נו tant pis). בחזרה לקנקס השאילתה הראשונה שלנו תיראה כך:
  const muriel = await knex('client').where({
    'prenom': 'Muriel',
    'password': knex.raw('sha1("test11")'),
  }).first();
  console.log(muriel);
אפשר ממש לראות בראש את התרגום ל SQL. שימו לב ל knex.raw שנועד כדי להגיד ל knex שהערך שאנחנו מעבירים צריך לעבור כמו שהוא לשאילתה בלי להוסיף Quoting. עוד נראה הרבה ממנו בהמשך הפוסט. הסיום ב first גורם לקנקס להחזיר אוביקט יחיד ולא רשימה וגם await מאוד חשוב שם כי knex מחזיר Promise. הדפסה של התוצאה נראית כך:
TextRow {
  id: 11,
  prenom: 'Muriel',
  nom: 'Dupuis',
  email: 'muriel@example.com',
  ville: 'Paris',
  password: '100c4e57374fc998e57164d4c0453bd3a4876a58'
}
## משימה 2: חיפוש מוצרים בהזמנות מצאו את כל המוצרים שמופיעים ביותר מהזמנה אחת. זאת השאילתה שהייתי כותב ב SQL:
select nom, count(commande_id) as commande_count from commande_ligne group by nom having commande_count > 1;
והתרגום לקנקס הוא כמעט אחד לאחד. הנה הקוד:
  const products = await knex('commande_ligne').
    select('nom').
    count('commande_id as commande_count').
    groupBy('nom').
    having('commande_count', '>', '1');

  console.log(products);
הפקודות select ו count מגדירות איזה שדות לבחור, groupBy זה group by ו having זה having. ## משימה 3: הוספת עמודה מצאו את כל המוצרים שמופיעים ביותר מהזמנה אחת, והפעם הוסיפו לתוצאה עמודה עם כל מזהי ההזמנות בהם המוצר מופיע. הפונקציה group_concat של MySQL מיד קופצת לראש וקנקס נותן לנו להשתמש בה בלי לעשות יותר מדי עניין בעזרת knex.raw:
  const products3 = await knex.
    select('nom').
    count('commande_id as commande_count').
    select(knex.raw('group_concat(commande_id) as commandes')).
    groupBy('nom').
    from('commande_ligne').
    having('commande_count', '>', '1');

  console.log(products3);
שימו לב שהפעם במקום להעביר את שם הטבלה כפרמטר ל knex בתחילת השורה בחרתי להעביר אותה כפרמטר לפונקציה from. זה רק בשביל שיהיה נוח לקרוא. ## משימה 4: עדכון עמודה שמרו את המחיר הכולל של כל שורת בהזמנה בעמודת prix_total בטבלת commande_ligne. המחיר הכולל הוא המכפלה של prix_unitaire ו quantite. אז לקנקס יש תחביר update מאוד פשוט שמקבל אוביקט ונראה כך:
  await knex('commande_ligne')
    .update({
      prix_total: knex.raw('prix_unitaire * quantite'),
    });
ושוב השימוש ב knex.raw כדי לתת לבסיס הנתונים לעשות את החישוב. ## משימה 5: הצגת מחיר לכל הזמנה עכשיו כשכתבנו את המחיר של כל שורה בהזמנה נוכל לחבר את כל השורות ולהציג את המחיר של כל הזמנה. על הדרך הוסיפו בבקשה גם את שם הלקוח ותאריך הקניה. ג'וין משלוש טבלאות? C'est du gâteau:

ToCode
1 419
# ארבע עשרה דוגמאות לשאילתות SQL ב knex.js לאחרונה התחלתי לעבוד יותר ב knex.js בסביבת Node ובינתיים מאוד נהנה מהספריה והיכולות שלה. קנקס היא בעצם בונה שאילתות, קצת דומה ל Sequelize, רק שבמקום מבנה של ORM קנקס אימצה גישה יותר פתוחה ויותר קרובה ל SQL. הם לא מנסים להסתיר מכם את העובדה שאתם עובדים ב SQL, אלא לתת תחביר JavaScript קצת יותר גמיש במקום התחביר הטקסטואלי של SQL. ההבדל המרכזי בין גישה זו ל ORM הוא שכאן אנחנו צריכים לדעת טוב SQL כדי להצליח לכתוב את השאילתות, ואין קשר הכרחי בין השאילתות למבנה המחלקות ביישום. מי שירצה יוכל לבנות עדיין את היישום בגישה מונחית עצמים אבל אתם תצטרכו לחבר לבד בין השאילתות לבין המחלקות בתוכנית. בשביל להבין את הגישה של קנקס טוב יותר ולהתרשם מהתחביר הנה 14 דוגמאות לשאילתות SQL לא טריוויאליות באמצעותו. ## מבנה בסיס הנתונים את בסיס הנתונים והשאלות לקחתי מקורס SQL חינמי שמצאתי ברשת בקישור הזה: https://sql.sh/. לא עברתי על חומרי הלימוד אבל התרגילים מצאו חן בעיניי ולכן בחרתי להשתמש בהם לדוגמאות כאן בפוסט. את קובץ התרגילים המקורי יחד עם נתונים לדוגמה להורדה אפשר למצוא כאן: https://sql.sh/exercices-sql. בסיס הנתונים מורכב מ-3 טבלאות לפי המבנה הבא:
mysql> show tables;
+----------------------+
| Tables_in_myapp_test |
+----------------------+
| client               |
| commande             |
| commande_ligne       |
+----------------------+
3 rows in set (0.01 sec)
mysql> describe client;
+----------+--------------+------+-----+---------+----------------+
| Field    | Type         | Null | Key | Default | Extra          |
+----------+--------------+------+-----+---------+----------------+
| id       | int unsigned | NO   | PRI | NULL    | auto_increment |
| prenom   | varchar(255) | NO   |     | NULL    |                |
| nom      | varchar(255) | NO   |     | NULL    |                |
| email    | varchar(255) | NO   |     | NULL    |                |
| ville    | varchar(255) | NO   |     | NULL    |                |
| password | varchar(255) | NO   |     | NULL    |                |
+----------+--------------+------+-----+---------+----------------+
6 rows in set (0.00 sec)
mysql> describe commande;
+------------------+--------------+------+-----+---------+----------------+
| Field            | Type         | Null | Key | Default | Extra          |
+------------------+--------------+------+-----+---------+----------------+
| id               | int unsigned | NO   | PRI | NULL    | auto_increment |
| client_id        | int unsigned | NO   |     | NULL    |                |
| date_achat       | date         | NO   |     | NULL    |                |
| reference        | varchar(255) | NO   |     | NULL    |                |
| cache_prix_total | float        | NO   |     | NULL    |                |
+------------------+--------------+------+-----+---------+----------------+
5 rows in set (0.00 sec)
mysql> describe commande_ligne;
+---------------+----------------+------+-----+---------+----------------+
| Field         | Type           | Null | Key | Default | Extra          |
+---------------+----------------+------+-----+---------+----------------+
| id            | int unsigned   | NO   | PRI | NULL    | auto_increment |
| commande_id   | int unsigned   | NO   |     | NULL    |                |
| nom           | varchar(255)   | NO   |     | NULL    |                |
| quantite      | int unsigned   | NO   |     | NULL    |                |
| prix_unitaire | float unsigned | NO   |     | NULL    |                |
| prix_total    | float unsigned | NO   |     | NULL    |                |
+---------------+----------------+------+-----+---------+----------------+
6 rows in set (0.01 sec)
אל תיבהלו מהצרפתית השמות ממילא דומים לאנגלית ואני אתרגם פה מה שצריך. מוכנים? נצא לדרך. ## חיבור לבסיס הנתונים ושאילתה ראשונה קבצי הנתונים הגיעו בפורמט של MySQL ולכן זה בסיס הנתונים בו השתמשתי לתרגיל. בשביל להתחבר לבסיס נתונים MySQL מתוכנית node.js באמצעות קנקס נפעיל את הפקודה:

ToCode
1 419
# פייל כשאני מסתכל אחורה על פרויקטים שלי ושל חברים שנכשלו אני יכול למפות 3 סיבות מרכזיות לכישלונות: 1. (הכי נפוץ) הפרויקט פשוט לא עלה לאוויר: היה רעיון טוב, מישהו התחיל לכתוב משהו ובסוף זה נשאר באיזו תיקיית טיוטות על המחשב או נסגר כי לא הצליחו לגייס משקיעים או כל סיפור בסיגנון. 2. לא היה קהל - הפרויקט עלה לאוויר אבל אף אחד לא רצה להשתמש בו. 3. חילוקי דעות אומנותיים - זה יכול להיות ריב בין מייסדים או אפילו מייסד בודד שמשנה את דעתו. בתחילת הדרך חשבתי שהצלחה נראית בצורה מסוימת, הגענו לשם ועכשיו זה כבר לא נראה כל כך אטרקטיבי. הטעות במקרה הראשון היא לא להבין נכון איזה משאבים יש לך כשאתה מתחיל וכמה משאבים צריך בשביל לסיים את הפרויקט. טיפ טוב כדי לצמצם כישלונות כאלה הוא לרשום לעצמכם בתחילת כל פרויקט כמה זמן וכסף אתם מקצים עבורו וכמה אתם חושבים שתצטרכו. לאט לאט ההערכות שלכם יתחילו להיות יותר קרובות למציאות. הטעות במקרה השני היא הנטיה שלנו לבנות את המוצר לפני שאנחנו עושים שיעורי בית ומבינים עבור מי ועבור מה זה מיועד. הטיפ כאן הוא להתחיל מה"מי" ולעשות עבודה רצינית כדי למצוא "מה" רלוונטי לאותם אנשים. והמקרה השלישי? אני לא בטוח בכלל שצריך להגדיר אותו בתור כישלון. החיים הם רצף של פרויקטים ואולי הכיוון החדש שמצאת ייגמר טוב יותר מהקודם.

ToCode
1 419
# יכול וצריך אני יכול להשתמש ב Web Sockets כדי לייצר חווית משתמש טובה יותר לגולשים; אבל קודם אני צריך לבנות אתר ריספונסיבי ונגיש כדי שאנשים יוכלו להשתמש בו מכל מכשיר. אני יכול להתקין מערכת איתור חדירות מתוחכמת שתשלח סמס על כל פעולה חשודה; אבל קודם אני צריך לוודא שהתקנתי פיירוול והגדרתי את החוקים כמו שצריך. אני יכול להשקיע חודשים בבניית POC ב-5 טכנולוגיות שונות כדי למצוא מי הטכנולוגיה המהירה והטובה ביותר למוצר שלי; אבל קודם אני צריך לבנות מוצר מתפקד כלשהו כדי לוודא שבכלל יש לזה שוק. הרבה פעמים רק בגלל שאנחנו יכולים לעשות משהו אנחנו בטוחים שזה הדבר שעכשיו צריך לעשות ושבלעדיו אי אפשר להתקדם (כי לכולם יש את זה), ובזמן שאנחנו עובדים על בניית הפיתרון המתוחכם אנחנו שוכחים שצריך קודם כל לסגור את ה Basics כמו שצריך. אני לא יודע איך נראית טבלת היכול-מול-צריך שלכם, היא כמובן שונה לכל פרויקט, אבל בכל מקרה אתם צריכים שתהיה לכם אחת.

ToCode
1 419
# תוכנית תשעת השלבים בכניסה לקוד קיים מזל טוב! התקבלתם לעבודה בחברת שקרכלשהו. עוד לפני שקיבלתם את המחשב כבר יצרו לכם משתמש בג'ירה והתחילו להפנות אינספור באגים לכיוונכם. לשאלת "איפה הקוד?" מישהו שולח לכם לינק לאיזה ריפו לא מתועד, ובהצלחה. זה הרגע לקחת אוויר ולהיכנס לפרויקט לאט. הנה רשימת אבני הדרך שלי בכניסה לפרויקט תוכנה גדול וחדש: 1. משוטט קצת בקבצים להבין מה ה Stack הטכנולוגי: באיזה בסיס נתונים משתמשים? מה הספריות המרכזיות? באיזה גירסאות? איך הקבצים מחולקים לתיקיות ומה בגדול נמצא בכל תיקיה? בקיצור כל מה שאפשר לגלות רק משיטוט וקריאת קבצי המקור. 2. מפעיל את הפרויקט אצלי על המכונה. זה שלב ארוך שברוב המערכות דורש התקנות והתאמות של גירסאות. דוקר עוזר מאוד כאן. 3. מריץ את הבדיקות (כן אני יודע אני אופטימי שיש בדיקות אוטומטיות). 4. משנה משהו קטן שמצליח לשבור בדיקה. ממשיך את המשחק הזה כמה שיש זמן כדי להבין איזה בדיקות קשורות לאיזה חלקים בקוד. 5. נכנס למערכת בתור משתמש רגיל ומשחק עם הכפתורים 6. כותב בדיקה חדשה ל Flow שגיליתי כששיחקתי עם המערכת (זה לא משנה אם כבר קיימת בדיקה ל Flow הזה. לא חייבים לעשות קומיט). 7. מוצא את הבאג שנראה הכי פשוט בג'ירה ומנסה לתקן אותו. תוך כדי התיקון גם מוסיף את הבדיקות המתאימות. 8. ממשיך במשחק של למצוא ולתקן באגים תוך כדי המשך כתיבת בדיקות עד שאני מרגיש שאני מבין למה קשור כל חלק בקוד. 9. מוציא מהג'ירה כרטיס עם פיצ'ר חדש ומוסיף אותו יחד עם הבדיקות. בסיום תשעת השלבים לא רק שתמצאו את עצמכם עם פיצ'ר ראשון שכתבתם, גם המבט שלכם על המערכת יהיה הרבה יותר מפוקס ותוכלו להמשיך למשימות הכבדות: Refactoring ושיפורי ביצועים.

ToCode
1 419
# קומפוננטה משולשת הנה באג מעניין ששלח לי תלמיד בריאקט הזכיר לי כמה דברים לגבי ארכיטקטורת תוכנה בתוך העולם המוזר הזה של פריימוורקים וקונפוננטות. אנחנו יודעים שאפשר לכתוב קוד כזה בריאקט:
function MyTripleComponent(props) {
  const { text } = props;

  if (text === 1) {
    return <p>Hello</p>;
  } else if (text === 2) {
    return <p>Bye bye</p>;
  } else {
    return <p>I don't understand</p>;
  }
}
הקומפוננטה מקבלת מאפיין מספרי ולפי הערך שלו מחליטה איזה טקסט להציג. זה עובד, קל להבנה ושגוי (או לפחות מוביל בסבירות גבוהה לבאגים מוזרים בהמשך). במשחק של ארכיטקטורה אתה קודם כל צריך לשחק לפי הכללים של הפריימוורק. לא רק הכללים הקשיחים אלא גם תפיסת העולם - וזה החלק הקשה. בריאקט קומפוננטה כמו שכתבנו היא למעשה שלוש קומפוננטות שמחוברות ביניהן בדבק לא יותר מדי חזק. שינוי קטן בדרישה ימיס את הדבק וישלח אותנו לשוטט במדבר הבאגים. הנה כבר קוד שמתחיל להיות מסוכן:
function MyTripleComponent(props) {
  const { type } = props;

  if (type === 1) {
    const [text, setText] = React.useState("");

    return (
      <label>
        <input
          type="text"
          value={text}
          onChange={(e) => setText(e.target.value)}
        />
        {text}
      </label>
    );
  } else if (type === 2) {
    return <p>Bye bye</p>;
  } else {
    return <p>I don't understand</p>;
  }
}
הפעם באחד הענפים אני צריך גם לשמור סטייט והסטייט הזה לא רלוונטי לענפים האחרים. אם תבעטו קצת בקומפוננטה הזו בסוף תגיעו להודעת השגיאה הבאה:
Rendered more hooks than during the previous render.
הפיתרון במצב כזה לא יהיה למשוך את הקריאה ל useState לראש הפונקציה, אלא להפריד את הפונקציה ל-3 קומפוננטות שונות. זה האופן בו היא היתה צריכה להיכתב מההתחלה.