Skip to content

Latest commit

 

History

History
608 lines (501 loc) · 15.7 KB

File metadata and controls

608 lines (501 loc) · 15.7 KB

Usage

This guide shows end-to-end ExtendDO usage with the AWS CLI and AWS-published SDKs for JavaScript, Go, and Python. The examples use DynamoDB endpoint overrides so requests go to ExtendDO instead of AWS service endpoints.

ExtendDO targets ExtendDB parity, not full AWS parity. Use the Worker-native management API to create accounts, users, policies, and access keys. Use the AWS CLI or SDKs only as clients for DynamoDB-compatible data-plane requests against an ExtendDO endpoint.

Start ExtendDO

For local development, configure local Wrangler variables:

cat > .dev.vars <<'EOF'
EXTENDDO_ADMIN_USER=admin
EXTENDDO_ADMIN_PASSWORD=secret
EOF

Start the Worker:

cp wrangler.example.json wrangler.jsonc
pnpm run build:wasm
pnpm run generate:types
pnpm exec wrangler dev --config wrangler.jsonc

Use the local endpoint in another terminal:

export EXTENDDO_ENDPOINT=http://localhost:8787
export EXTENDDO_ADMIN_USER=admin
export EXTENDDO_ADMIN_PASSWORD=secret
export EXTENDDO_ACCOUNT_ID=123456789012
export EXTENDDO_USER=quickstart
export EXTENDDO_USER_PASSWORD=secret
export AWS_REGION=us-east-1
export AWS_DEFAULT_REGION="$AWS_REGION"
export AWS_EC2_METADATA_DISABLED=true
export AWS_PAGER=""

For a deployed Worker, keep the same variables and change only the endpoint:

export EXTENDDO_ENDPOINT=https://<worker>

The endpoint should be the Worker origin. Do not append a DynamoDB path. ExtendDO does not proxy these requests to AWS.

Create Credentials

Create an ExtendDO account and user:

curl -u "${EXTENDDO_ADMIN_USER}:${EXTENDDO_ADMIN_PASSWORD}" \
  -X POST "$EXTENDDO_ENDPOINT/management/accounts" \
  -H 'content-type: application/json' \
  -d "{\"account_id\":\"$EXTENDDO_ACCOUNT_ID\",\"account_name\":\"dev\"}"

curl -u "${EXTENDDO_ADMIN_USER}:${EXTENDDO_ADMIN_PASSWORD}" \
  -X POST "$EXTENDDO_ENDPOINT/management/accounts/$EXTENDDO_ACCOUNT_ID/users" \
  -H 'content-type: application/json' \
  -d "{\"user_name\":\"$EXTENDDO_USER\",\"password\":\"$EXTENDDO_USER_PASSWORD\"}"

Attach a broad tutorial policy:

curl -u "${EXTENDDO_ADMIN_USER}:${EXTENDDO_ADMIN_PASSWORD}" \
  -X PUT "$EXTENDDO_ENDPOINT/management/accounts/$EXTENDDO_ACCOUNT_ID/users/$EXTENDDO_USER/policies/FullAccess" \
  -H 'content-type: application/json' \
  -d '{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":"dynamodb:*","Resource":"*"}]}'

Create an access key:

ACCESS_KEY_RESPONSE=$(curl -s \
  -u "$EXTENDDO_ACCOUNT_ID/$EXTENDDO_USER:$EXTENDDO_USER_PASSWORD" \
  -X POST "$EXTENDDO_ENDPOINT/management/accounts/$EXTENDDO_ACCOUNT_ID/users/$EXTENDDO_USER/access-keys")

printf '%s\n' "$ACCESS_KEY_RESPONSE"

Export the returned credentials:

export AWS_ACCESS_KEY_ID=$(printf '%s' "$ACCESS_KEY_RESPONSE" | jq -r '.access_key_id')
export AWS_SECRET_ACCESS_KEY=$(printf '%s' "$ACCESS_KEY_RESPONSE" | jq -r '.secret_access_key')

If jq is not available, copy access_key_id and secret_access_key from the JSON response and export them manually. The secret is returned only when the access key is created or imported.

AWS CLI

This section uses the AWS CLI as a DynamoDB-compatible HTTP client. All commands must include the ExtendDO endpoint override:

alias extenddo-dynamodb='aws --endpoint-url "$EXTENDDO_ENDPOINT" --region "$AWS_REGION" dynamodb --no-cli-pager'

Create a table:

extenddo-dynamodb create-table \
  --table-name Music \
  --attribute-definitions \
    AttributeName=Artist,AttributeType=S \
    AttributeName=SongTitle,AttributeType=S \
  --key-schema \
    AttributeName=Artist,KeyType=HASH \
    AttributeName=SongTitle,KeyType=RANGE \
  --billing-mode PAY_PER_REQUEST

Describe the table:

extenddo-dynamodb describe-table --table-name Music

Add items:

extenddo-dynamodb put-item \
  --table-name Music \
  --item '{"Artist":{"S":"No One You Know"},"SongTitle":{"S":"Call Me Today"},"AlbumTitle":{"S":"Somewhat Famous"},"Year":{"N":"2015"}}'

extenddo-dynamodb put-item \
  --table-name Music \
  --item '{"Artist":{"S":"No One You Know"},"SongTitle":{"S":"My Dog Spot"},"AlbumTitle":{"S":"Hey Now"},"Year":{"N":"2016"}}'

Get one item:

extenddo-dynamodb get-item \
  --table-name Music \
  --key '{"Artist":{"S":"No One You Know"},"SongTitle":{"S":"Call Me Today"}}'

Query by partition key:

extenddo-dynamodb query \
  --table-name Music \
  --key-condition-expression 'Artist = :artist' \
  --expression-attribute-values '{":artist":{"S":"No One You Know"}}'

Update an item:

extenddo-dynamodb update-item \
  --table-name Music \
  --key '{"Artist":{"S":"No One You Know"},"SongTitle":{"S":"Call Me Today"}}' \
  --update-expression 'SET Rating = :rating' \
  --expression-attribute-values '{":rating":{"N":"5"}}' \
  --return-values ALL_NEW

Delete an item:

extenddo-dynamodb delete-item \
  --table-name Music \
  --key '{"Artist":{"S":"No One You Know"},"SongTitle":{"S":"My Dog Spot"}}'

Delete the table:

extenddo-dynamodb delete-table --table-name Music

JavaScript SDK

Install the AWS-published SDK for JavaScript v3 DynamoDB client:

npm install @aws-sdk/client-dynamodb

Create usage.mjs:

import {
  CreateTableCommand,
  DeleteItemCommand,
  DeleteTableCommand,
  DynamoDBClient,
  GetItemCommand,
  PutItemCommand,
  QueryCommand,
  UpdateItemCommand
} from "@aws-sdk/client-dynamodb";

const endpoint = process.env.EXTENDDO_ENDPOINT ?? "http://localhost:8787";
const region = process.env.AWS_REGION ?? "us-east-1";
const accessKeyId = process.env.AWS_ACCESS_KEY_ID;
const secretAccessKey = process.env.AWS_SECRET_ACCESS_KEY;

if (!accessKeyId || !secretAccessKey) {
  throw new Error("Set AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY first");
}

const client = new DynamoDBClient({
  endpoint,
  region,
  credentials: { accessKeyId, secretAccessKey }
});

const TableName = "Music";

async function main() {
  await client.send(new CreateTableCommand({
    TableName,
    AttributeDefinitions: [
      { AttributeName: "Artist", AttributeType: "S" },
      { AttributeName: "SongTitle", AttributeType: "S" }
    ],
    KeySchema: [
      { AttributeName: "Artist", KeyType: "HASH" },
      { AttributeName: "SongTitle", KeyType: "RANGE" }
    ],
    BillingMode: "PAY_PER_REQUEST"
  }));

  await client.send(new PutItemCommand({
    TableName,
    Item: {
      Artist: { S: "No One You Know" },
      SongTitle: { S: "Call Me Today" },
      AlbumTitle: { S: "Somewhat Famous" },
      Year: { N: "2015" }
    }
  }));

  await client.send(new PutItemCommand({
    TableName,
    Item: {
      Artist: { S: "No One You Know" },
      SongTitle: { S: "My Dog Spot" },
      AlbumTitle: { S: "Hey Now" },
      Year: { N: "2016" }
    }
  }));

  const got = await client.send(new GetItemCommand({
    TableName,
    Key: {
      Artist: { S: "No One You Know" },
      SongTitle: { S: "Call Me Today" }
    }
  }));
  console.log("get-item", got.Item);

  const queried = await client.send(new QueryCommand({
    TableName,
    KeyConditionExpression: "Artist = :artist",
    ExpressionAttributeValues: {
      ":artist": { S: "No One You Know" }
    }
  }));
  console.log("query", queried.Items);

  const updated = await client.send(new UpdateItemCommand({
    TableName,
    Key: {
      Artist: { S: "No One You Know" },
      SongTitle: { S: "Call Me Today" }
    },
    UpdateExpression: "SET Rating = :rating",
    ExpressionAttributeValues: {
      ":rating": { N: "5" }
    },
    ReturnValues: "ALL_NEW"
  }));
  console.log("update-item", updated.Attributes);

  await client.send(new DeleteItemCommand({
    TableName,
    Key: {
      Artist: { S: "No One You Know" },
      SongTitle: { S: "My Dog Spot" }
    }
  }));

  await client.send(new DeleteTableCommand({ TableName }));
}

main().catch((error) => {
  console.error(error);
  process.exit(1);
});

Run it:

node usage.mjs

Go SDK

Create a small Go module using the AWS-published SDK for Go v2:

mkdir extenddo-go-usage
cd extenddo-go-usage
go mod init extenddo-go-usage
go get github.com/aws/aws-sdk-go-v2/config \
  github.com/aws/aws-sdk-go-v2/credentials \
  github.com/aws/aws-sdk-go-v2/service/dynamodb

Create main.go:

package main

import (
	"context"
	"fmt"
	"log"
	"os"

	"github.com/aws/aws-sdk-go-v2/aws"
	"github.com/aws/aws-sdk-go-v2/config"
	"github.com/aws/aws-sdk-go-v2/credentials"
	"github.com/aws/aws-sdk-go-v2/service/dynamodb"
	"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
)

func main() {
	ctx := context.Background()
	endpoint := getenv("EXTENDDO_ENDPOINT", "http://localhost:8787")
	region := getenv("AWS_REGION", "us-east-1")
	accessKeyID := os.Getenv("AWS_ACCESS_KEY_ID")
	secretAccessKey := os.Getenv("AWS_SECRET_ACCESS_KEY")
	if accessKeyID == "" || secretAccessKey == "" {
		log.Fatal("set AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY first")
	}

	cfg, err := config.LoadDefaultConfig(ctx,
		config.WithRegion(region),
		config.WithCredentialsProvider(
			credentials.NewStaticCredentialsProvider(accessKeyID, secretAccessKey, ""),
		),
	)
	if err != nil {
		log.Fatal(err)
	}

	client := dynamodb.NewFromConfig(cfg, func(options *dynamodb.Options) {
		options.BaseEndpoint = aws.String(endpoint)
	})

	tableName := "Music"
	_, err = client.CreateTable(ctx, &dynamodb.CreateTableInput{
		TableName: aws.String(tableName),
		AttributeDefinitions: []types.AttributeDefinition{
			{AttributeName: aws.String("Artist"), AttributeType: types.ScalarAttributeTypeS},
			{AttributeName: aws.String("SongTitle"), AttributeType: types.ScalarAttributeTypeS},
		},
		KeySchema: []types.KeySchemaElement{
			{AttributeName: aws.String("Artist"), KeyType: types.KeyTypeHash},
			{AttributeName: aws.String("SongTitle"), KeyType: types.KeyTypeRange},
		},
		BillingMode: types.BillingModePayPerRequest,
	})
	if err != nil {
		log.Fatal(err)
	}

	_, err = client.PutItem(ctx, &dynamodb.PutItemInput{
		TableName: aws.String(tableName),
		Item: map[string]types.AttributeValue{
			"Artist":     &types.AttributeValueMemberS{Value: "No One You Know"},
			"SongTitle":  &types.AttributeValueMemberS{Value: "Call Me Today"},
			"AlbumTitle": &types.AttributeValueMemberS{Value: "Somewhat Famous"},
			"Year":       &types.AttributeValueMemberN{Value: "2015"},
		},
	})
	if err != nil {
		log.Fatal(err)
	}

	_, err = client.PutItem(ctx, &dynamodb.PutItemInput{
		TableName: aws.String(tableName),
		Item: map[string]types.AttributeValue{
			"Artist":     &types.AttributeValueMemberS{Value: "No One You Know"},
			"SongTitle":  &types.AttributeValueMemberS{Value: "My Dog Spot"},
			"AlbumTitle": &types.AttributeValueMemberS{Value: "Hey Now"},
			"Year":       &types.AttributeValueMemberN{Value: "2016"},
		},
	})
	if err != nil {
		log.Fatal(err)
	}

	got, err := client.GetItem(ctx, &dynamodb.GetItemInput{
		TableName: aws.String(tableName),
		Key: map[string]types.AttributeValue{
			"Artist":    &types.AttributeValueMemberS{Value: "No One You Know"},
			"SongTitle": &types.AttributeValueMemberS{Value: "Call Me Today"},
		},
	})
	if err != nil {
		log.Fatal(err)
	}
	fmt.Printf("get-item: %#v\n", got.Item)

	queried, err := client.Query(ctx, &dynamodb.QueryInput{
		TableName:              aws.String(tableName),
		KeyConditionExpression: aws.String("Artist = :artist"),
		ExpressionAttributeValues: map[string]types.AttributeValue{
			":artist": &types.AttributeValueMemberS{Value: "No One You Know"},
		},
	})
	if err != nil {
		log.Fatal(err)
	}
	fmt.Printf("query count: %d\n", len(queried.Items))

	updated, err := client.UpdateItem(ctx, &dynamodb.UpdateItemInput{
		TableName: aws.String(tableName),
		Key: map[string]types.AttributeValue{
			"Artist":    &types.AttributeValueMemberS{Value: "No One You Know"},
			"SongTitle": &types.AttributeValueMemberS{Value: "Call Me Today"},
		},
		UpdateExpression: aws.String("SET Rating = :rating"),
		ExpressionAttributeValues: map[string]types.AttributeValue{
			":rating": &types.AttributeValueMemberN{Value: "5"},
		},
		ReturnValues: types.ReturnValueAllNew,
	})
	if err != nil {
		log.Fatal(err)
	}
	fmt.Printf("update-item: %#v\n", updated.Attributes)

	_, err = client.DeleteItem(ctx, &dynamodb.DeleteItemInput{
		TableName: aws.String(tableName),
		Key: map[string]types.AttributeValue{
			"Artist":    &types.AttributeValueMemberS{Value: "No One You Know"},
			"SongTitle": &types.AttributeValueMemberS{Value: "My Dog Spot"},
		},
	})
	if err != nil {
		log.Fatal(err)
	}

	_, err = client.DeleteTable(ctx, &dynamodb.DeleteTableInput{
		TableName: aws.String(tableName),
	})
	if err != nil {
		log.Fatal(err)
	}
}

func getenv(key string, fallback string) string {
	value := os.Getenv(key)
	if value == "" {
		return fallback
	}
	return value
}

Run it:

go run .

Python SDK

Install Boto3, the AWS-published SDK for Python:

python -m pip install boto3

Create usage.py:

import os

import boto3

endpoint = os.environ.get("EXTENDDO_ENDPOINT", "http://localhost:8787")
region = os.environ.get("AWS_REGION", "us-east-1")
access_key_id = os.environ.get("AWS_ACCESS_KEY_ID")
secret_access_key = os.environ.get("AWS_SECRET_ACCESS_KEY")

if not access_key_id or not secret_access_key:
    raise RuntimeError("Set AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY first")

client = boto3.client(
    "dynamodb",
    endpoint_url=endpoint,
    region_name=region,
    aws_access_key_id=access_key_id,
    aws_secret_access_key=secret_access_key,
)

table_name = "Music"

client.create_table(
    TableName=table_name,
    AttributeDefinitions=[
        {"AttributeName": "Artist", "AttributeType": "S"},
        {"AttributeName": "SongTitle", "AttributeType": "S"},
    ],
    KeySchema=[
        {"AttributeName": "Artist", "KeyType": "HASH"},
        {"AttributeName": "SongTitle", "KeyType": "RANGE"},
    ],
    BillingMode="PAY_PER_REQUEST",
)

client.put_item(
    TableName=table_name,
    Item={
        "Artist": {"S": "No One You Know"},
        "SongTitle": {"S": "Call Me Today"},
        "AlbumTitle": {"S": "Somewhat Famous"},
        "Year": {"N": "2015"},
    },
)

client.put_item(
    TableName=table_name,
    Item={
        "Artist": {"S": "No One You Know"},
        "SongTitle": {"S": "My Dog Spot"},
        "AlbumTitle": {"S": "Hey Now"},
        "Year": {"N": "2016"},
    },
)

got = client.get_item(
    TableName=table_name,
    Key={
        "Artist": {"S": "No One You Know"},
        "SongTitle": {"S": "Call Me Today"},
    },
)
print("get-item:", got.get("Item"))

queried = client.query(
    TableName=table_name,
    KeyConditionExpression="Artist = :artist",
    ExpressionAttributeValues={":artist": {"S": "No One You Know"}},
)
print("query:", queried.get("Items", []))

updated = client.update_item(
    TableName=table_name,
    Key={
        "Artist": {"S": "No One You Know"},
        "SongTitle": {"S": "Call Me Today"},
    },
    UpdateExpression="SET Rating = :rating",
    ExpressionAttributeValues={":rating": {"N": "5"}},
    ReturnValues="ALL_NEW",
)
print("update-item:", updated.get("Attributes"))

client.delete_item(
    TableName=table_name,
    Key={
        "Artist": {"S": "No One You Know"},
        "SongTitle": {"S": "My Dog Spot"},
    },
)

client.delete_table(TableName=table_name)

Run it:

python usage.py

Notes

  • Each example creates and deletes the Music table. Run one example at a time, or change TableName before running examples concurrently.
  • The examples use a broad tutorial policy. Production users should attach narrower policies that match the required DynamoDB actions and resource ARNs.
  • If you use temporary role-session credentials from the management assume-role endpoints, also export AWS_SESSION_TOKEN or pass the session token to the SDK client.
  • AWS IAM, AWS STS, and AWS account-management service APIs are not ExtendDO management APIs. Continue to use /management/... for ExtendDO accounts, users, roles, policies, and access keys.