Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
.idea/
.venv/
*.pyc
.terraform*
build/
terraform.tfstate*
14 changes: 13 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,23 @@ $(VENV_ACTIVATE):
test -d .venv || $(VENV_BIN) .venv

clean:
rm -rf .venv/
rm -rf .venv/ build/

install: venv ## Install dependencies
$(VENV_RUN); pip install --upgrade localstack pytest requests ruff typedb-driver

tf-deploy: ## Deploy the app locally via Terraform
mkdir -p build/
cp -r app/lambda build/
docker run -it --rm --entrypoint= -v ./build/lambda:/tmp/lambda public.ecr.aws/lambda/python:3.11 pip install --target /tmp/lambda -r /tmp/lambda/requirements.txt
$(VENV_RUN); tflocal init; tflocal apply -auto-approve

requests: ## Send a couple of test requests to create entries in the database
endpoint=http://users-api.execute-api.localhost.localstack.cloud:4566/test/users; \
curl -H 'content-type: application/json' -d '{"name":"Alice","age":42}' $$endpoint; \
curl -H 'content-type: application/json' -d '{"name":"Bob","age":31}' $$endpoint; \
curl $$endpoint

format: ## Run ruff to format the whole codebase
$(VENV_RUN); python -m ruff format .; python -m ruff check --output-format=full --fix .

Expand Down
20 changes: 20 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,26 @@ Sample app that demonstrates how to use TypeDB + LocalStack, to develop and test
* Docker
* LocalStack Pro (free trial available [here](https://app.localstack.cloud))
* `localstack` CLI
* `terraform` CLI

## Enable the TypeDB Extension

To enable the TypeDB extension in LocalStack, use this command:
```
$ localstack extensions install "git+https://github.com/whummer/localstack-utils.git#egg=localstack-typedb&subdirectory=localstack-typedb"
```

## Deploy and Run the App

To deploy the sample app to LocalStack, run the following `make` target:
```
$ make tf-deploy
```

Once the app is deployed, we can run some HTTP requests against the local API Gateway, which spawns a local Lambda function, and interacts with local TypeDB:
```
$ make requests
```

## License

Expand Down
57 changes: 57 additions & 0 deletions app/lambda/handler.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import json

from typedb.driver import TypeDB, Credentials, DriverOptions, TransactionType

db_name = "test-db"
server_host = "typedb.localhost.localstack.cloud:4566"


def handler(event, context):
_create_database_and_schema()
method = event["httpMethod"]
result = {}
if method == "GET":
result = list_users()
elif method == "POST":
payload = json.loads(event["body"])
result = create_user(payload)
return {"statusCode": 200, "body": json.dumps(result)}


def create_user(payload: dict):
user_name = payload["name"]
user_age = payload["age"]
with _driver() as driver:
with driver.transaction(db_name, TransactionType.WRITE) as tx:
query = f"insert $p isa person, has name '{user_name}', has age {user_age};"
tx.query(query).resolve()
tx.commit()


def list_users():
with _driver() as driver:
with driver.transaction(db_name, TransactionType.READ) as tx:
result = tx.query(
'match $p isa person; fetch {"name": $p.name, "age": $p.age};'
).resolve()
result = list(result)
return result


def _create_database_and_schema():
with _driver() as driver:
driver.databases.create(db_name)
with driver.transaction(db_name, TransactionType.SCHEMA) as tx:
tx.query("define entity person;").resolve()
tx.query("define attribute name, value string; person owns name;").resolve()
tx.query("define attribute age, value integer; person owns age;").resolve()
tx.commit()


def _driver():
return TypeDB.driver(
server_host,
# TODO: make configurable
Credentials("admin", "password"),
DriverOptions(is_tls_enabled=False),
)
1 change: 1 addition & 0 deletions app/lambda/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
typedb-driver
125 changes: 125 additions & 0 deletions main.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@

# Create deployment package for Lambda
data "archive_file" "lambda_zip" {
type = "zip"
source_dir = "build/lambda"
output_path = "build/lambda.zip"
}

# IAM role for Lambda function
resource "aws_iam_role" "lambda_role" {
name = "snowflake-counter-lambda-role"

assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Action = "sts:AssumeRole"
Effect = "Allow"
Principal = {
Service = "lambda.amazonaws.com"
}
}
]
})
}

# Lambda function
resource "aws_lambda_function" "user_lambda" {
filename = data.archive_file.lambda_zip.output_path
function_name = "user-service"
role = aws_iam_role.lambda_role.arn
handler = "handler.handler"
source_code_hash = data.archive_file.lambda_zip.output_base64sha256
runtime = "python3.11"
timeout = 30
}

# API Gateway
resource "aws_api_gateway_rest_api" "user_api" {
name = "user-service-api"
description = "User Service API"
tags = {
_custom_id_ = "users-api"
}
}

# API Gateway resource 'create'
resource "aws_api_gateway_resource" "user_resource" {
rest_api_id = aws_api_gateway_rest_api.user_api.id
parent_id = aws_api_gateway_rest_api.user_api.root_resource_id
path_part = "users"
}

# API Gateway method 'create'
resource "aws_api_gateway_method" "create_user_method" {
rest_api_id = aws_api_gateway_rest_api.user_api.id
resource_id = aws_api_gateway_resource.user_resource.id
http_method = "POST"
authorization = "NONE"
}

# API Gateway integration 'create'
resource "aws_api_gateway_integration" "lambda_integration" {
rest_api_id = aws_api_gateway_rest_api.user_api.id
resource_id = aws_api_gateway_resource.user_resource.id
http_method = aws_api_gateway_method.create_user_method.http_method

integration_http_method = "POST"
type = "AWS_PROXY"
uri = aws_lambda_function.user_lambda.invoke_arn
}

# API Gateway method 'list'
resource "aws_api_gateway_method" "list_users_method" {
rest_api_id = aws_api_gateway_rest_api.user_api.id
resource_id = aws_api_gateway_resource.user_resource.id
http_method = "GET"
authorization = "NONE"
}

# API Gateway integration 'list'
resource "aws_api_gateway_integration" "lambda_list_integration" {
rest_api_id = aws_api_gateway_rest_api.user_api.id
resource_id = aws_api_gateway_resource.user_resource.id
http_method = aws_api_gateway_method.list_users_method.http_method

integration_http_method = "POST"
type = "AWS_PROXY"
uri = aws_lambda_function.user_lambda.invoke_arn
}

# Lambda permission for API Gateway
resource "aws_lambda_permission" "api_gateway_lambda" {
statement_id = "AllowExecutionFromAPIGateway"
action = "lambda:InvokeFunction"
function_name = aws_lambda_function.user_lambda.function_name
principal = "apigateway.amazonaws.com"
source_arn = "${aws_api_gateway_rest_api.user_api.execution_arn}/*/*"
}

# API Gateway deployment
resource "aws_api_gateway_deployment" "user_api_deployment" {
depends_on = [
aws_api_gateway_integration.lambda_integration,
]

rest_api_id = aws_api_gateway_rest_api.user_api.id

lifecycle {
create_before_destroy = true
}
}

# Stage
resource "aws_api_gateway_stage" "stage" {
deployment_id = aws_api_gateway_deployment.user_api_deployment.id
rest_api_id = aws_api_gateway_rest_api.user_api.id
stage_name = "test"
}

# Output the API Gateway ID
output "api_endpoint" {
value = "http://${aws_api_gateway_rest_api.user_api.id}.execute-api.localhost.localstack.cloud:4566/test/users"
description = "API Gateway ID"
}