برنامه نویسی

ایجاد کاربر Cognito با شناسه افزایش خودکار

بنابراین چند موضوع جالب در اینجا وجود دارد.

  1. من با توابع AWS Step به گردش‌های کاری بدون کد متمایل شده‌ام و این State Machine چیزی جز ادغام‌های بومی SDK ندارد که شامل
    • DynamoDB (قرار دادن، حذف، دریافت)
    • مجموعه دانش/کاربر (AdminCreateUser)
  2. من با برخی از کدهای قدیمی مواجه شده ام که نیاز به یک نام کاربری دارد تا bigint باشد و نمی خواهم از RDBMS استفاده کنم، بنابراین از DynamoDB برای ایجاد یک کد برای خود استفاده می کنم در حالی که اثبات “شرایط مسابقه” است.

مثل همیشه، اگر می خواهید مستقیماً به کد بروید، اینجا مخزن Github است

خروجی (ماشین حالت نهایی)

پس از یادگیری، یک ماشین حالت کاربر ایجاد کنیدکاری که من می‌خواهم انجام دهم این است که از طریق Machine State قدم بزنم و بخش‌های هر مرحله را لمس کنم و آن را در نمودار بالا به هم بچسبانم.

آخرین شناسه را پیدا کنید

اول از همه، اساس عناصر DynamoDB از مقاله ای که در Bite-Sized Serverless خواندم به دست آمد.

اما در سناریوی من، من می‌خواستم یک جریان کامل ایجاد کاربر اضافه کنم، علاوه بر اینکه باید بتوانم یک کاربر با نام کاربری BigInt ایجاد کنم. عجیب به نظر می رسد و من دوست دارم بتوانم از یک UUID، KSUID یا ULID استفاده کنم، اما در سیستمی که من این را برای آن می سازم، قطعات قدیمی داریم که ارزش BigInt را تحمیل می کنند.

برای اینکه مجبور نباشید به RDBMS تکیه کنید و به جای آن از DynamoDB استفاده کنید، من از یک ردیف در جدول برای نگه داشتن “LastId” استفاده می کنم که نمی توان آن را به روز کرد و برای ساخت این کاربران استفاده کرد. ما ممکن است در شرایط مسابقه ای که در آن دو فرآیند همزمان تلاش می کنند رکورد را به روز کنند، شکست بخوریم، اما با استفاده از قفل خوش بینانه من از این مشکل جلوگیری می کنم و فقط فرآیند را دوباره امتحان می کنم. DynamoDB واقعاً این کار را به خوبی انجام می دهد و من از این الگو در بسیاری از مکان های دیگر در مقیاس با موفقیت بزرگ استفاده کرده ام.

خود جدول از الگوهایی استفاده می کند که من در مورد طراحی میز تکی از Alex DeBrie یاد گرفتم

با استفاده از یک ساده PK و SK ساختار من جدول را با قرار دادن چندین نهاد در آن بارگذاری می کنم. یکی از این نهادها است USERMETADATA موجودی که دارای LastId که در پروفایل کاربری استفاده شد

از آنجایی که من به Native Integrations پایبند هستم، از DynamoDB API برای اجرای یک getItem روی میز انتخابی من آن فراخوانی API شبیه این است

{
  "TableName": "Users",
  "ConsistentRead": true,
  "Key": {
    "PK": {
      "S": "USERMETADATA"
    },
    "SK": {
      "S": "USERMETADATA"
    }
  }
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

تنها هدف از این getItem واکشی است LastId از جدول تا بتوان از آن هنگام ساخت نام کاربری و نمایه استفاده کرد. کد زیر تابعی است که این انتقال را ایجاد می کند

buildFindLastId = (t: ITable): CallAwsService => {
    return new CallAwsService(this, 'FindLastId', {
        action: "getItem",
        iamResources: [t.tableArn],
        parameters: {
            TableName: t.tableName,
            ConsistentRead: true,
            Key: {
                PK: {
                    S: "USERMETADATA"
                },
                SK: {
                    S: "USERMETADATA"
                }
            }
        },
        service: "dynamodb",
        resultSelector: {
            "previousUserId.$": "$.Item.LastId.N",
            "userId.$": "States.Format('{}', States.MathAdd(States.StringToJson($.Item.LastId.N), 1))"
        },
        resultPath: "$.context"
    });
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

ایجاد کاربر DynamoDB

وقتی شناسه واکشی شد و 1 ((به استفاده از توابع ذاتی توجه کنید States.MathAdd، States.StringToJson و States.Format) می توانم شروع به جمع آوری Transaction کنم که رکورد را در DynamoDB می نویسد.

چند نکته قابل توجه

  1. attribute_not_exists در فیلد PK اگر آن مقدار مشخصه از قبل وجود داشته باشد، تراکنش با شکست مواجه خواهد شد
  2. به روز رسانی از USERMETADATA و ایجاد کاربر جدید در یک تراکنش اتفاق می افتد، بنابراین یک همه یا هیچ است. اگر چیزی برای هر یک از شرایطی که می‌گیرم شکست خورد، به مرحله LastId برمی‌گردد تا دوباره امتحان کنید.
buildCreateDynamoDBUser = (t: ITable): CallAwsService => {
    return new CallAwsService(this, 'CreateDynamoDBUser', {
        action: "transactWriteItems",
        iamResources: [t.tableArn],
        parameters: {
            "TransactItems": [
                {
                    "Put": {
                        "Item": {
                            PK: {
                                "S.$": "States.Format('USERPROFILE#{}', $.context.userId)"
                            },
                            SK: {
                                "S.$": "States.Format('USERPROFILE#{}', $.context.userId)"
                            },
                            FirstName: {
                                "S.$": "$.firstName"
                            },
                            LastName: {
                                "S.$": "$.lastName"
                            },
                            EmailAddress: {
                                "S.$": "$.emailAddress"
                            },
                            PhoneNumber: {
                                "S.$": "$.phoneNumber"
                            }
                        },
                        "ConditionExpression": "attribute_not_exists(PK)",
                        "TableName": t.tableName
                    }
                },
                {
                    "Update": {
                        "ConditionExpression": "LastId = :previousUserId",
                        "UpdateExpression": "SET LastId = :newUserId",
                        "ExpressionAttributeValues": {
                            ":previousUserId": {
                                "N.$": "$.context.previousUserId"
                            },
                            ":newUserId": {
                                "N.$": "$.context.userId"
                            }
                        },
                        "Key": {
                            "PK": {
                                "S": "USERMETADATA"
                            },
                            "SK": {
                                "S": "USERMETADATA"
                            }
                        },
                        "TableName": t.tableName
                    }
                }
            ]
        },
        service: "dynamodb",
        resultPath: JsonPath.DISCARD,

    });
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

بنابراین فکر می کنم می توانم حدس بزنم به چه فکر می کنید. این مقدار زیادی کد و جاوا اسکریپت/تایپ اسکریپت برای برقراری تماس API است. و من استدلال می کنم که این کد بسیار کمتر از تلاش برای انجام این کار با لامبدا است. و همچنین ارزان‌تر است، زیرا من مرحله راه‌اندازی لامبدا و متحمل شدن هزینه اجرا را صرف اجرای یک تماس API نمی‌کنم. ناگفته نماند، من نه هزینه ای را پرداخت می کنم و نه منتظر وقوع یک شروع سرد هستم. مطمئناً این روزها زیاد نیستند، اما چیزی هم نیستند.

همانطور که می بینید، من در حال به روز رسانی هستم USERMETADATA و همچنین ایجاد یک USERPROFILE برای نام کاربری جدیدی که ساخته و ارسال شده است

علاوه بر این، در صورت خرابی، مستقیماً به FindLastId برمی‌گردد تا گردش کار دوباره شروع شود. همانطور که در بالا گفتم، این الگو برای مقابله با قفل خوش بینانه عالی عمل می کند و هزینه های اضافی را که در سناریوهای دیگر اتفاق می افتد متحمل نمی شود. به‌علاوه، حجمی که این آزمایش مجدد را تجربه می‌کند، از نظر احتمال وقوع، علاوه بر تأخیر کمتر از 0.25 ثانیه، در صورتی که گردش کار مجبور باشد دوباره شروع شود، کاملاً خوب خواهد بود.

ایجاد کاربر Cognito

لحظه حقیقت فرا رسیده است. من آخرین شناسه را دریافت کردم، یک کاربر جدید در جدولی ایجاد کردم که برای پشتیبانی از نمایه کاربر علاوه بر ذخیره ادعاهای سفارشی‌سازی شده از User Pool (آن مقاله به زودی ارائه می‌شود) استفاده می‌شود و اکنون زمان ایجاد آن است. کاربر در Cognito

buildCreateCognitoUser = (u: IUserPool): CallAwsService => {
    return new CallAwsService(this, 'CreateCognitoUser', {
        action: "adminCreateUser",
        iamResources: [u.userPoolArn],
        parameters: {
            "UserPoolId": u.userPoolId,
            "Username.$": "$.context.userId",
            "UserAttributes": [
                {
                    "Name": "email",
                    "Value.$": "$.emailAddress"
                },
                {
                    "Name": "email_verified",
                    "Value": "true"
                }
            ]
        },
        service: "cognitoidentityprovider",
    });
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

این قسمت واقعا ساده است. ورودی را از بالا بگیرید و Cognito را صدا کنید adminCreateUser با API تماس بگیرید و به طور جادویی یک کاربر جدید دریافت خواهید کرد که ایمیل تأیید شده است و نیاز به تغییر اجباری رمز عبور دارد. علاوه بر این، همانطور که اشاره کردم، می‌توانید آن ادعاهای JWT را از داده‌های جدول سفارشی کنید.

چیزی که در این مورد نیز دوست دارم، این است که اگر کاربر قبلاً وجود داشته باشد، می‌خواهم ایجاد کاربر را به عقب برگردانم و طوری رفتار کنم که هرگز این اتفاق نیفتاده است.

buildStateMachine = (scope: Construct, t: ITable, u: IUserPool): stepfunctions.IChainable => {
    const pass = new stepfunctions.Pass(scope, 'Pass');
    const fail = new stepfunctions.Fail(scope, 'Fail');
    let rollbackUser = this.buildRollbackUser(t);
    let createCognitoUser = this.buildCreateCognitoUser(u)
    let createDbUser = this.buildCreateDynamoDBUser(t);
    let findLastId = this.buildFindLastId(t);

    createCognitoUser.addCatch(rollbackUser, {
        errors: [
            "CognitoIdentityProvider.UsernameExistsException"
        ],
        resultPath: "$.error"
    })

    createDbUser.addCatch(findLastId, {
        errors: [
            "DynamoDB.ConditionalCheckFailedException",
            "DynamoDb.TransactionCanceledException"
        ],
        resultPath: "$.error"
    })

    // correctLastId.next(findLastId);
    rollbackUser.next(fail);

    return findLastId
        .next(createDbUser)
        .next(createCognitoUser)
        .next(pass);
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

کد بالا کد گردش کار واقعی State Machine با استفاده از CDK API روان است. توجه کنید که در createCognitoUserIChainable من در حال رسیدگی به CognitoIdentityProvider.UsernameExistsException که سپس به “بازگشت” می رود. البته می توانید هر خطایی را که می خواهید در اینجا بررسی کنید.

و در بازگشت، من به سادگی در حال تمیز کردن هستم.

buildRollbackUser = (t: ITable): CallAwsService => {
    return new CallAwsService(this, 'RollbackUser', {
        action: "deleteItem",
        iamResources: [t.tableArn],
        parameters: {
            "TableName": t.tableName,
            "Key": {
                "PK": {
                    "S.$": "States.Format('USERPROFILE#{}', $.context.userId)"
                },
                "SK": {
                    "S.$": "States.Format('USERPROFILE#{}', $.context.userId)"
                }
            }
        },

        resultPath: "$.results",
        service: "dynamodb",
    });
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

بسته بندی

من عاشق این ماشین‌های حالتی هستم که خارج از ارکستراسیون کد صفر دارند. با توجه به مدت طولانی در فناوری، من این نوع چیزها را دیده ام که می آیند و می روند، اما چیزی که در مورد AWS Step Functions دوست دارم این است

  1. مقیاس می شود … به طور جدی این کار را انجام می دهد
  2. کد ساخت آن از طریق زبانی که من با آن راحت هستم انجام می شود. نه مقداری DSL
  3. من متوجه شدم که این نوع راه حل ها به راحتی قابل اشکال زدایی و استدلال هستند
  4. هرچه کد کمتری بنویسم، خطاهای کمتری ایجاد می کنم. به همین سادگی

بنابراین دفعه بعد که نیاز دارید برخی از چیزهای بدون سرور AWS را با هم ترکیب کنید، نگاهی به رویکرد #zecode بیندازید. من فکر می کنم شما ممکن است آن را دوست داشته باشید

نوشته های مشابه

دیدگاهتان را بنویسید

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *

دکمه بازگشت به بالا