{"id":"l-aws-4-2","courseId":"aws-cloud-foundations","lang":"ar","kind":"lesson","summary":"ابنِ الـ API الخاص بتأكيد الحضور في Lantern من HTTP API في Amazon API Gateway، ودالة AWS Lambda بـ role يتبع أقل الصلاحيات، وجدول DynamoDB، مع حماية من التكرار وتكاليف تهبط إلى ما يقارب الصفر عند الخمول.","html":"<p>أصبحت الواجهة الأمامية تُحمَّل من مواقع الحافة خلال أجزاء من الألف من الثانية. لكن تأكيد الحضور ليس ملفًا ثابتًا: يجب التحقّق منه، وتخزينه، وعدّه. على الخادم القديم كان ذلك مسارًا (route) في تطبيق Node.js، يعمل طوال اليوم من أجل الدقائق القليلة في الأسبوع التي يؤكّد فيها أحدهم حضوره. أما الآن فيصبح ثلاث قطع مُدارة لا تكلّف شيئًا وهي خاملة، وتتوسّع حين تنتشر فعالية ما.</p>\n<h2 id=\"sec-1\" tabindex=\"-1\">الصورة العامة</h2>\n<figure class=\"diagram\"><svg class=\"diagram-svg\" focusable=\"false\" viewBox=\"0 0 680 220\" role=\"img\" aria-labelledby=\"l-aws-4-2-t1\"><defs><marker id=\"arrow-l-aws-4-2-1\" viewBox=\"0 0 10 10\" refX=\"9\" refY=\"5\" markerWidth=\"7\" markerHeight=\"7\" orient=\"auto-start-reverse\"><path d=\"M0 0 L10 5 L0 10 z\" class=\"d-arrowhead\"/></marker></defs>\n  <title id=\"l-aws-4-2-t1\">يرسل المتصفّح POST /events/{eventId}/rsvps إلى HTTP API في API Gateway. يستدعي API Gateway دالة Lambda المسمّاة lantern-rsvp، التي تعمل بـ execution role وتكتب في جدول DynamoDB المسمّى lantern-rsvps. وتذهب سجلات الدالة إلى CloudWatch Logs.</title>\n  <rect class=\"d-box\" x=\"10\" y=\"70\" width=\"110\" height=\"60\" rx=\"10\"/>\n  <text class=\"d-label\" x=\"65\" y=\"105\" text-anchor=\"middle\">المتصفّح</text>\n  <rect class=\"d-box-accent\" x=\"170\" y=\"70\" width=\"140\" height=\"60\" rx=\"10\"/>\n  <text class=\"d-label\" x=\"240\" y=\"97\" text-anchor=\"middle\">API Gateway</text>\n  <text class=\"d-label-muted\" x=\"240\" y=\"117\" text-anchor=\"middle\">HTTP API</text>\n  <rect class=\"d-box-primary\" x=\"360\" y=\"70\" width=\"140\" height=\"60\" rx=\"10\"/>\n  <text class=\"d-label\" x=\"430\" y=\"97\" text-anchor=\"middle\">Lambda</text>\n  <text class=\"d-label-muted\" x=\"430\" y=\"117\" text-anchor=\"middle\">lantern-rsvp</text>\n  <rect class=\"d-box-success\" x=\"550\" y=\"70\" width=\"120\" height=\"60\" rx=\"10\"/>\n  <text class=\"d-label\" x=\"610\" y=\"97\" text-anchor=\"middle\">DynamoDB</text>\n  <text class=\"d-label-muted\" x=\"610\" y=\"117\" text-anchor=\"middle\">lantern-rsvps</text>\n  <rect class=\"d-box-warn\" x=\"360\" y=\"170\" width=\"140\" height=\"40\" rx=\"8\"/>\n  <text class=\"d-label\" x=\"430\" y=\"195\" text-anchor=\"middle\">CloudWatch Logs</text>\n  <path class=\"d-arrow\" d=\"M120 100 L166 100\" marker-end=\"url(#arrow-l-aws-4-2-1)\"/>\n  <path class=\"d-arrow\" d=\"M310 100 L356 100\" marker-end=\"url(#arrow-l-aws-4-2-1)\"/>\n  <path class=\"d-arrow\" d=\"M500 100 L546 100\" marker-end=\"url(#arrow-l-aws-4-2-1)\"/>\n  <path class=\"d-arrow d-dashed\" d=\"M430 130 L430 166\" marker-end=\"url(#arrow-l-aws-4-2-1)\"/>\n  <text class=\"d-label-muted\" x=\"333\" y=\"60\" text-anchor=\"middle\">سياسة المورد</text>\n  <text class=\"d-label-muted\" x=\"523\" y=\"60\" text-anchor=\"middle\">execution role</text>\n</svg><figcaption>طلب واحد عبر الـ API الخاص بتأكيد الحضور</figcaption></figure>\n<p>مساران يكفيان للإطلاق:</p>\n<div class=\"table-wrap\" role=\"region\" aria-label=\"جدول قابل للتمرير\" tabindex=\"0\"><table><thead>\n<tr>\n<th>المسار</th>\n<th>ما يفعله</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><code dir=\"ltr\">POST /events/{eventId}/rsvps</code></td>\n<td>يخزّن تأكيد حضور واحدًا، ويرفض التكرار</td>\n</tr>\n<tr>\n<td><code dir=\"ltr\">GET /events/{eventId}/rsvps</code></td>\n<td>يعيد عدد تأكيدات الحضور للفعالية</td>\n</tr>\n</tbody>\n</table></div><h2 id=\"sec-2\" tabindex=\"-1\">الدالة</h2>\n<p>يمرّر الـ HTTP API في API Gateway إلى Lambda حدث JSON (بصيغة الحمولة 2.0) يحتوي على معاملات المسار (path parameters) والطريقة (method) والمحتوى كنص. وتعيد الدالة رمز حالة وترويسات ومحتوى نصيًا:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"js\" data-index=\"0\"><div class=\"code-head\"><span class=\"code-title\">rsvp/index.mjs</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"نسخ الكود\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>نسخ</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">import</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> { DynamoDBClient } </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">from</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> \"@aws-sdk/client-dynamodb\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">import</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> { DynamoDBDocumentClient, PutCommand, QueryCommand } </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">from</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> \"@aws-sdk/lib-dynamodb\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#6E7781;--shiki-dark:#8B949E\">// Created once per execution environment and reused across invocations.</span></span>\n<span class=\"line\"><span style=\"color:#6E7781;--shiki-dark:#8B949E\">// removeUndefinedValues: an RSVP without a \"name\" is stored without that attribute.</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">const</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> db</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> DynamoDBDocumentClient.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">from</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">new</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> DynamoDBClient</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">({}), {</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  marshallOptions: { removeUndefinedValues: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">true</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> },</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">});</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">const</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> TABLE</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> process.env.</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">TABLE_NAME</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">const</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> reply</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> (</span><span style=\"color:#953800;--shiki-dark:#FFA657\">statusCode</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#953800;--shiki-dark:#FFA657\">data</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=></span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> ({</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  statusCode,</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  headers: { </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"content-type\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"application/json\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> },</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  body: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">JSON</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">stringify</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(data),</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">});</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">export</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> const</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> handler</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> async</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> (</span><span style=\"color:#953800;--shiki-dark:#FFA657\">event</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=></span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> {</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  const</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> eventId</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> event.pathParameters?.eventId;</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  if</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> (event.requestContext.http.method </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">===</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> \"GET\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">) {</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">    const</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> res</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> await</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> db.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">send</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">new</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> QueryCommand</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">({</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">      TableName: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">TABLE</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">,</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">      KeyConditionExpression: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"eventId = :e\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">,</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">      ExpressionAttributeValues: { </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\":e\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: eventId },</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">      Select: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"COUNT\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">,</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    }));</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">    return</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> reply</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">200</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, { eventId, count: res.Count });</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  }</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  const</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> { </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">email</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">name</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> } </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> JSON</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">parse</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(event.body </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">??</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> \"{}\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">);</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  if</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> (</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">typeof</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> email </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">!==</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> \"string\"</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> ||</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> !</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">email.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">includes</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"@\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">)) {</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">    return</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> reply</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">400</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, { error: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"A valid email is required\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> });</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  }</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  try</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> {</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">    await</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> db.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">send</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">new</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> PutCommand</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">({</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">      TableName: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">TABLE</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">,</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">      Item: { eventId, email: email.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">toLowerCase</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(), name, createdAt: </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">new</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> Date</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">().</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">toISOString</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">() },</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">      ConditionExpression: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"attribute_not_exists(email)\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">,</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    }));</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  } </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">catch</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> (err) {</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">    if</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> (err.name </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">===</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> \"ConditionalCheckFailedException\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">) {</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">      return</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> reply</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">409</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, { error: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"You have already RSVPed to this event\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> });</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    }</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">    throw</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> err;</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  }</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  return</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> reply</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">201</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, { eventId, email });</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">};</span></span></code></pre></div>\n<p>لاحظ ما ليس موجودًا: لا بيانات اعتماد، ولا ARN للجدول، ولا مجمّع اتصالات (connection pool). تعثر الـ SDK على بيانات اعتماد الـ execution role، ويأتي اسم الجدول من متغيّر بيئة كي يعمل الكود نفسه على جداول التطوير والإنتاج. تتضمّن بيئة تشغيل Node.js الإصدار v3 من AWS SDK for JavaScript، لكن احزم مع الكود الخاص بك الإصدار الذي اختبرته، كي لا يغيّره تحديث لبيئة التشغيل من تحت قدميك.</p>\n<p>وهناك حدّ واحد يجب ذكره بصراحة: يقرأ Query الواحد 1 MB كحد أقصى، لذا فإن فعالية بعدد هائل من تأكيدات الحضور ستحتاج في العدّ إلى تقسيم إلى صفحات (pagination)، أو إلى عنصر عدّاد منفصل يُحدَّث مع كل كتابة. أما لفعاليات Lantern، فاستدعاء واحد أكثر من كافٍ.</p>\n<aside class=\"callout callout-mistake\" role=\"note\"><p class=\"callout-title\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><path d=\"m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3\"/><path d=\"M12 9v4\"/><path d=\"M12 17h.01\"/></svg><span><span class=\"callout-kind\">خطأ شائع</span><span class=\"callout-sep\" aria-hidden=\"true\"> · </span>ترك إعادة المحاولات تُنشئ تكرارات</span></p>\n<p>المتصفّحات ترسل النماذج مرتين، والمستخدمون ينقرون مرتين، والعملاء يعيدون المحاولة عند انتهاء المهلة. دون حارس، يصبح كل واحد منها عنصرًا جديدًا. تجعل <code dir=\"ltr\">ConditionExpression</code> الكتابة تنجح فقط إن لم يوجد بعدُ عنصر بذلك الـ <code dir=\"ltr\">eventId</code> والـ <code dir=\"ltr\">email</code>، ويقيّمها DynamoDB بشكل ذرّي، فلا يمكن أن ينجح الطلبان المتزامنان كلاهما.</p>\n</aside>\n<h2 id=\"sec-3\" tabindex=\"-1\">الصلاحيات</h2>\n<p>يحصل <strong>الـ execution role</strong> الخاص بالدالة على ما يستدعيه الكود بالضبط، على جدول واحد بالضبط، إضافة إلى السياسة المُدارة من AWS <code dir=\"ltr\">AWSLambdaBasicExecutionRole</code> كي يستطيع كتابة السجلات:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"json\" data-index=\"1\"><div class=\"code-head\"><span class=\"code-title\">rsvp-role-policy.json</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"نسخ الكود\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>نسخ</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">{</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">  \"Version\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"2012-10-17\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">,</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">  \"Statement\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: [</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    {</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">      \"Sid\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"RsvpTable\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">,</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">      \"Effect\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"Allow\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">,</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">      \"Action\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: [</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"dynamodb:PutItem\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"dynamodb:Query\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">],</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">      \"Resource\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"arn:aws:dynamodb:eu-west-1:111122223333:table/lantern-rsvps\"</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    }</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  ]</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">}</span></span></code></pre></div>\n<p>والاتجاه الآخر هو <strong>السياسة القائمة على المورد</strong> الخاصة بالدالة: يحق لـ API Gateway استدعاؤها. يضيف الـ console هذه السياسة حين تربط مسارًا؛ ومن الـ CLI تبدو هكذا، مقيّدة بـ API واحد عبر الـ ARN الخاص به:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"bash\" data-index=\"2\"><div class=\"code-head\"><span class=\"code-title\">Terminal</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"نسخ الكود\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>نسخ</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#953800;--shiki-dark:#FFA657\">aws</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> lambda</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> add-permission</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> --function-name</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> lantern-rsvp</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> \\</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  --statement-id</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> allow-lantern-http-api</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> \\</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  --action</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> lambda:InvokeFunction</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> \\</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  --principal</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> apigateway.amazonaws.com</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> \\</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  --source-arn</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> \"arn:aws:execute-api:eu-west-1:111122223333:a1b2c3d4e5/*\"</span></span></code></pre></div>\n<h2 id=\"sec-4\" tabindex=\"-1\">الـ API</h2>\n<p>في console الـ API Gateway، أنشئ <strong>HTTP API</strong>، وأضف المسارين مع تكامل Lambda (Lambda integration) إلى <code dir=\"ltr\">lantern-rsvp</code>، واستخدم المرحلة <code dir=\"ltr\">$default</code> التي تُنشر تلقائيًا. الـ HTTP APIs هي النوع الأرخص والأبسط. واختر <strong>REST API</strong> بدلًا منها حين تحتاج إلى API keys مع خطط الاستخدام، أو تحديد معدّل لكل عميل، أو التحقّق من الطلبات، أو التخزين المؤقت للاستجابات، أو AWS WAF.</p>\n<p>لأن الموقع يُقدَّم من <code dir=\"ltr\">https://lantern.example</code> والـ API يعيش على نطاق آخر، اضبط <strong>CORS</strong> على الـ HTTP API للسماح بذلك الـ origin، أو أضف الـ API كـ origin ثانٍ في الـ distribution الخاص بـ CloudFront تحت <code dir=\"ltr\">/api/*</code> كي يرى المتصفّح نطاقًا واحدًا. ثم اختبر:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"bash\" data-index=\"3\"><div class=\"code-head\"><span class=\"code-title\">Terminal</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"نسخ الكود\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>نسخ</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#953800;--shiki-dark:#FFA657\">curl</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> -i</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> -X</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> POST</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> \\</span></span>\n<span class=\"line\"><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">  \"https://a1b2c3d4e5.execute-api.eu-west-1.amazonaws.com/events/evt-2026-street-food/rsvps\"</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> \\</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  -H</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> \"content-type: application/json\"</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> \\</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  -d</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> '{\"email\":\"rita@example.com\",\"name\":\"Rita\"}'</span></span></code></pre></div>\n<p>يعيد الاستدعاء الأول <code dir=\"ltr\">201</code>؛ أرسله مرة أخرى فتحصل على <code dir=\"ltr\">409</code>.</p>\n<aside class=\"callout callout-tip\" role=\"note\"><p class=\"callout-title\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><path d=\"M15 14c.2-1 .7-1.7 1.5-2.5 1-.9 1.5-2.2 1.5-3.5A6 6 0 0 0 6 8c0 1 .2 2.2 1.5 3.5.7.7 1.3 1.5 1.5 2.5\"/><path d=\"M9 18h6\"/><path d=\"M10 22h4\"/></svg><span><span class=\"callout-kind\">نصيحة</span><span class=\"callout-sep\" aria-hidden=\"true\"> · </span>لماذا لا نستخدم function URL؟</span></p>\n<p>تستطيع Lambda أيضًا أن تمنح الدالة نقطة نهاية HTTPS خاصة بها، تُسمّى function URL، دون API Gateway أمامها. وهذا مناسب للـ webhooks والأدوات الداخلية. يستخدم Lantern الـ API Gateway لأنه يريد مسارات منفصلة بحدود معدّل خاصة بكلٍّ منها، ونطاقًا مخصّصًا بجوار الموقع، وإمكانية إضافة authorizer لاحقًا دون لمس كود الدالة.</p>\n</aside>\n<h2 id=\"sec-5\" tabindex=\"-1\">كم يكلّف، وكيف تضع له سقفًا</h2>\n<p>كل قطعة تُحتسب حسب الاستخدام: API Gateway لكل طلب، وLambda لكل طلب ولكل GB-second، وDynamoDB بنمط on-demand لكل قراءة وكتابة. الأسبوع الهادئ لا يكاد يكلّف شيئًا، وأمسية المهرجان تكلّف ما يعادل بضعة آلاف من الطلبات. وللتأكّد من أن بوتًا لا يستطيع تضخيم الفاتورة أو إغراق أي شيء في المراحل التالية، اضبط <strong>reserved concurrency</strong> على الدالة (سقف لعدد النسخ المتزامنة) وحدًّا للمعدّل (throttling) على مرحلة الـ API.</p>\n<p>في الدرس التالي ستراقب هذا الـ API في الإنتاج: السجلات والمقاييس والإنذارات، ومراجعة منظّمة للتصميم كله.</p>\n","toc":[{"id":"sec-1","text":"الصورة العامة","level":2},{"id":"sec-2","text":"الدالة","level":2},{"id":"sec-3","text":"الصلاحيات","level":2},{"id":"sec-4","text":"الـ API","level":2},{"id":"sec-5","text":"كم يكلّف، وكيف تضع له سقفًا","level":2}],"takeaways":["يستقبل الـ HTTP API في API Gateway الطلب، ويستدعي Lambda بحدث JSON، ويعيد أيّ رمز حالة ومحتوى تعيدهما الدالة.","تحتاج دالة Lambda إلى نوعين من الصلاحيات؛ الـ execution role يتيح لها استدعاء DynamoDB، وسياستها القائمة على المورد تتيح لـ API Gateway استدعاءها.","الكتابة المشروطة مثل attribute_not_exists تمنع النقرات المزدوجة وإعادة المحاولات من إنشاء عناصر مكرّرة.","اختر HTTP APIs للـ APIs البسيطة منخفضة التكلفة، وREST APIs حين تحتاج إلى API keys، أو تحديد معدّل لكل عميل، أو التحقّق من الطلبات، أو التخزين المؤقت، أو AWS WAF.","كل قطعة تُحتسب لكل طلب، فالـ API الخامل لا يكاد يكلّف شيئًا، والـ reserved concurrency يضع سقفًا لمدى توسّع الذروة."],"further":[{"title":"Choose between REST APIs and HTTP APIs","url":"https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-vs-rest.html"},{"title":"Create AWS Lambda proxy integrations for HTTP APIs","url":"https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-develop-integrations-lambda.html"},{"title":"Condition expressions in DynamoDB","url":"https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Expressions.ConditionExpressions.html"},{"title":"Tutorial: Create a CRUD HTTP API with Lambda and DynamoDB","url":"https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-dynamo-db.html"}],"quiz":[{"q":"<p>تعمل الدالة عند اختبارها في console الـ Lambda، لكن الاستدعاءات عبر API Gateway تفشل لأن API Gateway غير مسموح له باستدعائها. ما الناقص؟</p>","options":[{"text":"عبارة في الـ execution role الخاص بالدالة تسمح بـ <code dir=\"ltr\">apigateway:*</code>.","why":"يتحكّم الـ execution role فيما تستطيع الدالة استدعاءه، لا فيمن يستطيع استدعاء الدالة."},{"text":"سياسة لجدول DynamoDB تسمّي API Gateway.","why":"لا يتحدّث API Gateway إلى DynamoDB أبدًا في هذا التصميم؛ إنه يستدعي الدالة فقط."},{"text":"مستخدم IAM لـ API Gateway مع مفتاح وصول.","why":"تعمل خدمات AWS عبر service principals وroles، ولا تعمل أبدًا عبر مستخدمي IAM بمفاتيح."},{"text":"عبارة في السياسة القائمة على المورد الخاصة بالدالة تسمح لـ <code dir=\"ltr\">apigateway.amazonaws.com</code> باستدعاء <code dir=\"ltr\">lambda:InvokeFunction</code> لهذا الـ API.","why":"صحيح. من يحق له استدعاء الدالة يُحدَّد في سياستها القائمة على المورد، مقيّدًا بـ source ARN لهذا الـ API."}],"answer":3},{"q":"<p>نقر أحد المنظّمين على زر الإرسال مرتين، فوصل طلبا تأكيد حضور متطابقان خلال أجزاء من الألف من الثانية. كيف تتجنّب دالة الدرس تخزين تأكيدَي حضور؟</p>","options":[{"text":"يستخدم <code dir=\"ltr\">PutItem</code> العبارة <code dir=\"ltr\">ConditionExpression attribute_not_exists(email)</code>، فتفشل الكتابة الثانية بـ <code dir=\"ltr\">ConditionalCheckFailedException</code> وتعيد 409.","why":"صحيح. يتحقّق DynamoDB من الشرط بشكل ذرّي (atomic) على العنصر، فتنجح كتابة واحدة بالضبط من الاثنتين."},{"text":"يزيل API Gateway الطلبات المكرّرة تلقائيًا.","why":"يمرّر API Gateway كل طلب يستقبله. إزالة التكرار مهمة التطبيق."},{"text":"تشغّل Lambda نسخة واحدة فقط من الدالة في كل مرة.","why":"تشغّل Lambda نسخًا بعدد الطلبات المتزامنة، فيمكن أن يعمل الطلبان في الوقت نفسه."}],"answer":0},{"q":"<p>يحتاج Lantern إلى API keys كي يستطيع موقعان شريكان إرسال تأكيدات الحضور، لكلٍّ منهما حد استخدام خاص به. أيّ نوع من API Gateway يناسب ذلك؟</p>","options":[{"text":"HTTP API، لأنه أرخص.","why":"الـ HTTP APIs أرخص لأنها تستغني عن بعض الميزات، والـ API keys مع خطط الاستخدام (usage plans) من بينها."},{"text":"WebSocket API، لأن الشريكين يحتاجان إلى اتصال حي.","why":"الـ WebSocket APIs للاتصالات ثنائية الاتجاه طويلة الأمد، والشريكان اللذان يرسلان نماذج لا يحتاجان إلى ذلك."},{"text":"REST API، الذي يدعم API keys مع خطط الاستخدام وتحديد المعدّل لكل عميل.","why":"صحيح. حين تحتاج إلى ميزات إدارة الـ API هذه، فنوع REST API هو الذي يوفّرها."}],"answer":2}],"exercise":{"kind":"spot-bug","language":"js","code":"import { DynamoDBClient } from \"@aws-sdk/client-dynamodb\";\nimport { DynamoDBDocumentClient, PutCommand, ScanCommand } from \"@aws-sdk/lib-dynamodb\";\n\nconst db = DynamoDBDocumentClient.from(new DynamoDBClient({\n  credentials: { accessKeyId: \"AKIAIOSFODNN7EXAMPLE\", secretAccessKey: process.env.SECRET },\n}));\nconst TABLE = process.env.TABLE_NAME;\n\nconst reply = (statusCode, data) => ({\n  statusCode,\n  headers: { \"content-type\": \"application/json\" },\n  body: JSON.stringify(data),\n});\n\nexport const handler = async (event) => {\n  const eventId = event.pathParameters?.eventId;\n\n  if (event.requestContext.http.method === \"GET\") {\n    const res = await db.send(new ScanCommand({\n      TableName: TABLE,\n      FilterExpression: \"eventId = :e\",\n      ExpressionAttributeValues: { \":e\": eventId },\n    }));\n    return reply(200, { eventId, count: res.Items.length });\n  }\n\n  const { email, name } = JSON.parse(event.body ?? \"{}\");\n  if (typeof email !== \"string\" || !email.includes(\"@\")) {\n    return reply(400, { error: \"A valid email is required\" });\n  }\n\n  try {\n    await db.send(new PutCommand({\n      TableName: TABLE,\n      Item: { eventId, email: email.toLowerCase(), name },\n      ConditionExpression: \"attribute_exists(email)\",\n    }));\n  } catch (err) {\n    if (err.name === \"ConditionalCheckFailedException\") {\n      return reply(409, { error: \"You have already RSVPed to this event\" });\n    }\n    throw err;\n  }\n  return reply(201, { eventId, email });\n};","bugLines":[5,19,36],"title":"راجع دالة تأكيد الحضور","prompt":"<p>فُتح pull request بهذه النسخة من <code dir=\"ltr\">lantern-rsvp</code>. تبدو مطابقة لدالة الدرس إلى حد كبير، لكن ثلاثة أسطر فيها ستتسبّب في حادثة أمنية، أو فاتورة متضخّمة، أو تعطّل تأكيدات الحضور في الإنتاج. اخترها.</p>\n","hints":["كيف يجب أن تحصل دالة Lambda على بيانات اعتمادها؟","أيّ عملية في DynamoDB تقرأ الجدول كله، وأيّها تقرأ partition واحدًا؟","اقرأ تعبير الشرط بصوت عالٍ: تنجح الكتابة فقط إذا كان العنصر … ؟"],"explanation":"<p>السطر 5 يثبّت مفتاح وصول داخل الـ client. للدالة execution role بالفعل، وتستخدمه الـ SDK تلقائيًا حين لا تمرّر أي بيانات اعتماد، لذا احذف الخيار <code dir=\"ltr\">credentials</code> كليًا؛ ولا تضع المفاتيح في الكود أو متغيّرات البيئة أبدًا. السطر 19 يعدّ تأكيدات الحضور باستخدام <code dir=\"ltr\">Scan</code> مع ترشيح، فيقرأ كل عنصر في الجدول ويحتسبه مع كل مشاهدة للصفحة؛ استخدم <code dir=\"ltr\">QueryCommand</code> مع <code dir=\"ltr\">KeyConditionExpression: &quot;eventId = :e&quot;</code> و<code dir=\"ltr\">Select: &quot;COUNT&quot;</code>. (كما أن Scan يتوقّف بعد 1 MB، فيكون هذا العدّ في جدول كبير خاطئًا فضلًا عن كونه مكلفًا.) السطر 36 يعكس حارس التكرار: <code dir=\"ltr\">attribute_exists(email)</code> يجعل الكتابة تنجح فقط حين يكون تأكيد الحضور موجودًا من قبل، فيفشل كل تأكيد حضور جديد بالرمز 409. يجب أن يكون <code dir=\"ltr\">attribute_not_exists(email)</code>.</p>\n","checkLabels":{}},"words":623,"readMinutes":4}