Nodejs,AWS Dynamodb如何在创建新记录之前检查用户是不是已经存在

Posted

技术标签:

【中文标题】Nodejs,AWS Dynamodb如何在创建新记录之前检查用户是不是已经存在【英文标题】:Nodejs , AWS Dynamodb how to check if user already exists before creating new recordNodejs,AWS Dynamodb如何在创建新记录之前检查用户是否已经存在 【发布时间】:2022-01-20 04:58:03 【问题描述】:

我正在创建 AWS lamda 函数来创建客户(潜在客户),但在创建客户之前,我需要检查用户是否已经存在。客户/潜在客户可以通过电子邮件和电话号码的组合来识别。

这是我的表定义: dynamodb-tables.ts

export default 


LeadsTable: 
    Type: "AWS::DynamoDB::Table",
    DeletionPolicy: "Retain",
    Properties: 
      TableName: "$self:provider.environment.LEADS_TABLE",
      AttributeDefinitions: [ AttributeName: "id", AttributeType: "S" ],
      KeySchema: [ AttributeName: "id", KeyType: "HASH" ],
      ProvisionedThroughput: 
        ReadCapacityUnits: "$self:custom.table_throughput",
        WriteCapacityUnits: "$self:custom.table_throughput",
      ,
    ,
  ,
  InterestsTable: 
    Type: "AWS::DynamoDB::Table",
    DeletionPolicy: "Retain",
    Properties: 
      TableName: "$self:provider.environment.INTERESTS_TABLE",
      AttributeDefinitions: [
         AttributeName: "id", AttributeType: "S" ,
         AttributeName: "leadId", AttributeType: "S" ,
      ],
      KeySchema: [
         AttributeName: "id", KeyType: "HASH" ,
         AttributeName: "leadId", KeyType: "RANGE" ,
      ],
      ProvisionedThroughput: 
        ReadCapacityUnits: "$self:custom.table_throughput",
        WriteCapacityUnits: "$self:custom.table_throughput",
      ,
      GlobalSecondaryIndexes: [
        
          IndexName: "lead_index",
          KeySchema: [ AttributeName: "leadId", KeyType: "HASH" ],
          Projection: 
            // attributes to project into the index
            ProjectionType: "ALL", // (ALL | KEYS_ONLY | INCLUDE)
          ,
          ProvisionedThroughput: 
            ReadCapacityUnits: "$self:custom.table_throughput",
            WriteCapacityUnits: "$self:custom.table_throughput",
          ,
        ,
      ],
    ,
  ,
;

这是我的模型:lead.model.ts

import  v4 as UUID  from "uuid";

// Interfaces
interface IProps 
  id?: string;
  email: string;
  phone: string;
  firstName: string;
  lastName: string;

interface ILeadInterface extends IProps 
  createdAt: Date;
  updatedAt: Date;

export default class LeadModel 
  private _id: string;

  private _email: string;

  private _phone: string;

  private _firstName: string;

  private _lastName: string;

  constructor(
    id = UUID(),
    email = "",
    phone = "",
    firstName = "",
    lastName = "",
  : IProps) 
    this._id = id;
    this._email = email;
    this._phone = phone;
    this._firstName = firstName;
    this._lastName = lastName;
  

  /**
   * Set Id
   * @param value
   */
  setId(value: string) 
    this._id = value !== "" ? value : null;
  

  /**
   * Get Id
   * @return string|*
   */
  getId() 
    return this._id;
  

  /**
   * Set Email
   * @param value
   */
  setEmail(value: string) 
    this._email = value !== "" ? value : null;
  

  /**
   * Get Email
   * @return string|*
   */
  getEmail() 
    return this._email;
  

  /**
   * Set Phone
   * @param value
   */
  setPhone(value: string) 
    this._phone = value !== "" ? value : null;
  

  /**
   * Get Phone
   * @return string|*
   */
  getPhone() 
    return this._phone;
  

  /**
   * Set First Name
   * @param value
   */
  setFirstName(value: string) 
    this._firstName = value !== "" ? value : null;
  

  /**
   * Get First Name
   * @return string|*
   */
  getFirstName() 
    return this._firstName;
  

  /**
   * Set Last Name
   * @param value
   */
  setLastName(value: string) 
    this._lastName = value !== "" ? value : null;
  

  /**
   * Get Last Name
   * @return string|*
   */
  getLastName() 
    return this._lastName;
  

  /**
   * Get Base entity mappings
   * @return ILeadInterface
   */
  getEntityMappings(): ILeadInterface 
    return 
      id: this.getId(),
      email: this.getEmail(),
      phone: this.getPhone(),
      firstName: this.getFirstName(),
      lastName: this.getLastName(),
      createdAt: new Date(),
      updatedAt: new Date(),
    ;
  

我的操作/功能:create-lead.action.ts

import 
  APIGatewayProxyHandler,
  APIGatewayEvent,
  Context,
  APIGatewayProxyResult,
 from "aws-lambda";
import "source-map-support/register";

// Models
import LeadModel from "../../models/lead.model";
import ResponseModel from "../../models/response.model";

// Services
import DatabaseService from "../../services/database.service";

// utils
import  validateAgainstConstraints  from "../../utils/util";

// Define the request constraints
import requestConstraints from "../../constraints/lead/create.constraint.json";

// Enums
import  StatusCode  from "../../enums/status-code.enum";
import  ResponseMessage  from "../../enums/response-message.enum";

/***
 * Create lead and insert into database
 *
 * @api post /lead/create
 * @apiName Create lead
 * @apiGroup lead
 * @apiDescription Create lead
 *
 * @apiParam string           email          The email id of the lead
 * @apiParam string           phone          The phone number of the lead
 * @apiParam string           firstName     The first name of the lead
 * @apiParam string           lastName      The last name of the lead
 *
 * @apiSuccess object         data
 * @apiSuccess string         message       The response message
 * @apiSuccess string         status        The response status
 *
 * @apiParamExample json Request-Example:
 *     
 *      "email": "jj@mailinator.com",
 *      "phone": "+7352726252",
 *      "firstName":"jj",
 *      "lastName":"jo"
 *    
 *
 * @apiSuccessExample json Success-Response:
 *     HTTP/1.1 200 OK
 *     
 *       "data":  "leadId": "468c8094-a756-4000-a919-974a64b5be8e" ,
 *       "message": "Lead successfully created"
 *       "status": "success"
 *     
 *      *
 *  @apiErrorExample json Error-Response: Validation Errors
 *     HTTP/1.1 400 Bad Request
 *    
 *      "data": 
 *          "validation": 
                "email": [
                    "Email can't be blank"
                ]
            
 *      ,
 *      "message": "required fields are missing",
 *      "status": "bad request"
 *    
 *
 *  @apiErrorExample json Error-Response: Unknown Error
 *     HTTP/1.1 500 Internal Server Error
 *    
 *      "data": ,
 *      "message": "Unknown error",
 *      "status": "error"
 *    
 */
export const createLead: APIGatewayProxyHandler = async (
  event: APIGatewayEvent,
  _context: Context
): Promise<APIGatewayProxyResult> => 
  // Initialize response variable
  let response;

  // Parse request parameters
  const requestData = JSON.parse(event.body);

  // Validate against constraints
  return validateAgainstConstraints(requestData, requestConstraints)
    .then(async () => 
      // Initialise database service
      const databaseService = new DatabaseService();

      // Initialise and hydrate model
      const leadModel = new LeadModel(requestData);

      // Get model data
      const data = leadModel.getEntityMappings();

      // Initialise DynamoDB PUT parameters
      const params = 
        TableName: process.env.LEADS_TABLE,
        Item: 
          id: data.id,
          email: data.email,
          phone: data.phone,
          firstName: data.firstName,
          lastName: data.lastName,
          createdAt: data.createdAt,
          updatedAt: data.updatedAt,
        ,
      ;

      // check if lead is uneque
      const unequeCheckParams = 
        TableName: process.env.LEADS_TABLE,
        FilterExpression: "#email = :emailval OR #phone = :phoneval",
        ExpressionAttributeNames: 
          "#email": "email",
          "#phone": "phone",
        ,
        ExpressionAttributeValues: 
          ":emailval": data.email,
          ":phoneval": data.phone,
        ,
      ;

      const isLead = await databaseService.query(unequeCheckParams);
      if (isLead) 
        throw new ResponseModel(
          ,
          409,
          `create-error: $ResponseMessage.CREATE_LEAD_FAIL_DUPLICATE`
        );
      
      // Inserts item into DynamoDB table
      await databaseService.create(params);
      return data.id;
    )
    .then((leadId) => 
      // Set Success Response
      response = new ResponseModel(
         leadId ,
        StatusCode.OK,
        ResponseMessage.CREATE_LEAD_SUCCESS
      );
    )
    .catch((error) => 
      // Set Error Response
      response =
        error instanceof ResponseModel
          ? error
          : new ResponseModel(
              ,
              StatusCode.ERROR,
              ResponseMessage.CREATE_LEAD_FAIL
            );
    )
    .then(() => 
      // Return API Response
      return response.generate();
    );
;

我的数据库服务如下所示:database.service.ts

/* eslint-disable no-await-in-loop */

import * as AWS from "aws-sdk";

// Models
import ResponseModel from "../models/response.model";

// Interfaces
import IConfig from "../interfaces/config.interface";

// Enums
import  StatusCode  from "../enums/status-code.enum";
import  ResponseMessage  from "../enums/response-message.enum";

// Put
type PutItem = AWS.DynamoDB.DocumentClient.PutItemInput;
type PutItemOutput = AWS.DynamoDB.DocumentClient.PutItemOutput;

// Batch write
type BatchWrite = AWS.DynamoDB.DocumentClient.BatchWriteItemInput;
type BatchWriteOutPut = AWS.DynamoDB.DocumentClient.BatchWriteItemOutput;

// Update
type UpdateItem = AWS.DynamoDB.DocumentClient.UpdateItemInput;
type UpdateItemOutPut = AWS.DynamoDB.DocumentClient.UpdateItemOutput;

// Query
type QueryItem = AWS.DynamoDB.DocumentClient.QueryInput;
type QueryItemOutput = AWS.DynamoDB.DocumentClient.QueryOutput;

// Get
type GetItem = AWS.DynamoDB.DocumentClient.GetItemInput;
type GetItemOutput = AWS.DynamoDB.DocumentClient.GetItemOutput;

// Delete
type DeleteItem = AWS.DynamoDB.DocumentClient.DeleteItemInput;
type DeleteItemOutput = AWS.DynamoDB.DocumentClient.DeleteItemOutput;

type Item =  [index: string]: string ;

const 
  STAGE,
  DYNAMODB_LOCAL_STAGE,
  DYNAMODB_LOCAL_ACCESS_KEY_ID,
  DYNAMODB_LOCAL_SECRET_ACCESS_KEY,
  DYNAMODB_LOCAL_ENDPOINT,
 = process.env;

const config: IConfig =  region: "eu-west-1" ;
if (STAGE === DYNAMODB_LOCAL_STAGE) 
  config.accessKeyId = DYNAMODB_LOCAL_ACCESS_KEY_ID; // local dynamodb accessKeyId
  config.secretAccessKey = DYNAMODB_LOCAL_SECRET_ACCESS_KEY; // local dynamodb secretAccessKey
  config.endpoint = DYNAMODB_LOCAL_ENDPOINT; // local dynamodb endpoint

AWS.config.update(config);

const documentClient = new AWS.DynamoDB.DocumentClient();

export default class DatabaseService 
  getItem = async ( key, hash, hashValue, tableName : Item) => 
    const params = 
      TableName: tableName,
      Key: 
        id: key,
      ,
    ;
    if (hash) 
      params.Key[hash] = hashValue;
    
    const results = await this.get(params);
    if (Object.keys(results).length) 
      return results;
    
    console.error("Item does not exist");
    throw new ResponseModel(
       id: key ,
      StatusCode.BAD_REQUEST,
      ResponseMessage.INVALID_REQUEST
    );
  ;

  create = async (params: PutItem): Promise<PutItemOutput> => 
    try 
      return await documentClient.put(params).promise();
     catch (error) 
      console.error(`create-error: $error`);
      throw new ResponseModel(, 500, `create-error: $error`);
    
  ;

  batchCreate = async (params: BatchWrite): Promise<BatchWriteOutPut> => 
    try 
      return await documentClient.batchWrite(params).promise();
     catch (error) 
      console.error(`batch-write-error: $error`);
      throw new ResponseModel(, 500, `batch-write-error: $error`);
    
  ;

  update = async (params: UpdateItem): Promise<UpdateItemOutPut> => 
    try 
      // result.Attributes
      return await documentClient.update(params).promise();
     catch (error) 
      console.error(`update-error: $error`);
      throw new ResponseModel(, 500, `update-error: $error`);
    
  ;

  query = async (params: QueryItem): Promise<QueryItemOutput> => 
    try 
      return await documentClient.query(params).promise();
     catch (error) 
      console.error(`query-error: $error`);
      throw new ResponseModel(, 500, `query-error: $error`);
    
  ;

  get = async (params: GetItem): Promise<GetItemOutput> => 
    console.log("DB GET - STAGE: ", STAGE);
    console.log("DB GET - params.TableName: ", params.TableName);
    console.log("DB GET - params.Key: ", params.Key);

    try 
      return await documentClient.get(params).promise();
     catch (error) 
      console.error(`get-error - TableName: $params.TableName`);
      console.error(`get-error: $error`);
      throw new ResponseModel(, 500, `get-error: $error`);
    
  ;

  delete = async (params: DeleteItem): Promise<DeleteItemOutput> => 
    try 
      return await documentClient.delete(params).promise();
     catch (error) 
      console.error(`delete-error: $error`);
      throw new ResponseModel(, 500, `delete-error: $error`);
    
  ;

  getAllData = async (params: QueryItem) => 
    try 
      const _getAllData = async (params, startKey) => 
        if (startKey) 
          params.ExclusiveStartKey = startKey;
        
        return documentClient.query(params).promise();
      ;
      let lastEvaluatedKey = null;
      let rows = [];
      do 
        const result = await _getAllData(params, lastEvaluatedKey);
        rows = rows.concat(result.Items);
        lastEvaluatedKey = result.LastEvaluatedKey;
       while (lastEvaluatedKey);
      return rows;
     catch (error) 
      console.error(`get-error: $error`);
      throw new ResponseModel(, 500, `get-error: $error`);
    
  ;

我的创建操作遇到以下错误:

查询错误:ValidationException:必须在请求中指定 KeyConditions 或 KeyConditionExpression 参数

我该如何解决这个问题,或者有更好的方法

【问题讨论】:

【参考方案1】:

我建议使用Conditional Put

AWS 文档:https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Expressions.ConditionExpressions.html#Expressions.ConditionExpressions.PreventingOverwrites

PutItem 操作使用相同的键(如果存在)覆盖项目。如果您想避免这种情况,请使用条件表达式。这允许写入仅在有问题的项目还没有相同的密钥时继续进行。

aws dynamodb put-item \
    --table-name ProductCatalog \
    --item file://item.json \
    --condition-expression "attribute_not_exists(Id)"

如果条件表达式的计算结果为 false,DynamoDB 将返回以下错误消息:条件请求失败。

使用您的代码,它看起来像这样:

const params = 
  TableName: ...,
  Item: ...,
  ConditionExpression: 'attribute_not_exists(#a)',
  ExpressionAttributeNames:  '#a': '...' ,
;

create = async (params: PutItem): Promise<PutItemOutput> => 
    try 
      return await documentClient.put(params).promise();
     catch (error) 
      if (error is 'The conditional request failed') return; // pseudo code

      console.error(`create-error: $error`);
      throw new ResponseModel(, 500, `create-error: $error`);
    
  ;

【讨论】:

感谢您的回答,但如果我想同时检查电子邮件和电话的组合会发生什么ConditionExpression: "attribute_not_exists(email) OR attribute_not_exists(phone)", 没有帮助。它不适用于常规属性。

以上是关于Nodejs,AWS Dynamodb如何在创建新记录之前检查用户是不是已经存在的主要内容,如果未能解决你的问题,请参考以下文章

AWS DynamoDB - 分区密钥和密钥分片

亚马逊 dynamodb(通过 aws sdk)是不是有一个很好的对象映射器,可以在 nodejs 中使用?

在哪里可以找到 AWS-SDK for dynamodb 在 NodeJS 打字稿中的所有异常类?

Aws Appsync 解析器:如何创建解析器以更新列表映射 (DynaMoDB) 中的项目

如何在 DynamoDB 中添加列

用于管理状态数据的AWS服务-dynamodb / step函数/ sqs?