mirror of
https://github.com/VibedByKaKi/t3-code-android-nightly.git
synced 2026-10-10 04:11:15 +02:00
559 lines
20 KiB
TypeScript
559 lines
20 KiB
TypeScript
import * as DynamoDB from "@/AWS/DynamoDB";
|
|
import * as Lambda from "@/AWS/Lambda";
|
|
import * as S3 from "@/AWS/S3";
|
|
import * as Effect from "effect/Effect";
|
|
import * as Layer from "effect/Layer";
|
|
import { HttpServerRequest } from "effect/http/HttpServerRequest";
|
|
import * as HttpServerResponse from "effect/http/HttpServerResponse";
|
|
import path from "pathe";
|
|
|
|
const main = path.resolve(import.meta.dirname, "handler.ts");
|
|
|
|
export class DynamoDBTestFunction extends Lambda.Function<Lambda.Function>()(
|
|
"DynamoDBTestFunction",
|
|
) {}
|
|
|
|
export default DynamoDBTestFunction.make(
|
|
{
|
|
main,
|
|
functionUrl: true,
|
|
},
|
|
Effect.gen(function* () {
|
|
const sourceTable = yield* DynamoDB.Table("TestTable", {
|
|
partitionKey: "pk",
|
|
sortKey: "sk",
|
|
attributes: {
|
|
pk: "S",
|
|
sk: "S",
|
|
category: "S",
|
|
subcategory: "S",
|
|
rank: "S",
|
|
},
|
|
globalSecondaryIndexes: [
|
|
{
|
|
// Multi-attribute keys: composite partition key (category +
|
|
// subcategory) and composite sort key (rank + sk). Sparse — only
|
|
// items written with all four attributes appear in the index.
|
|
indexName: "MultiAttrIndex",
|
|
partitionKey: ["category", "subcategory"],
|
|
sortKey: ["rank", "sk"],
|
|
projection: { ProjectionType: "ALL" },
|
|
},
|
|
],
|
|
});
|
|
const restoreTargetTable = yield* DynamoDB.Table("RestoreTargetTable", {
|
|
partitionKey: "pk",
|
|
sortKey: "sk",
|
|
attributes: { pk: "S", sk: "S" },
|
|
});
|
|
const exportBucket = yield* S3.Bucket("ExportBucket", {
|
|
forceDestroy: true,
|
|
});
|
|
|
|
const getItem = yield* DynamoDB.GetItem(sourceTable);
|
|
const batchGetItem = yield* DynamoDB.BatchGetItem(sourceTable);
|
|
const batchWriteItem = yield* DynamoDB.BatchWriteItem(sourceTable);
|
|
const batchExecuteStatement =
|
|
yield* DynamoDB.BatchExecuteStatement(sourceTable);
|
|
const describeTable = yield* DynamoDB.DescribeTable(sourceTable);
|
|
const describeTimeToLive = yield* DynamoDB.DescribeTimeToLive(sourceTable);
|
|
const executeStatement = yield* DynamoDB.ExecuteStatement(sourceTable);
|
|
const executeTransaction = yield* DynamoDB.ExecuteTransaction(sourceTable);
|
|
const putItem = yield* DynamoDB.PutItem(sourceTable);
|
|
const deleteItem = yield* DynamoDB.DeleteItem(sourceTable);
|
|
const listTables = yield* DynamoDB.ListTables();
|
|
const listTagsOfResource = yield* DynamoDB.ListTagsOfResource(sourceTable);
|
|
const updateItem = yield* DynamoDB.UpdateItem(sourceTable);
|
|
const updateTimeToLive = yield* DynamoDB.UpdateTimeToLive(sourceTable);
|
|
const query = yield* DynamoDB.Query(sourceTable);
|
|
const scan = yield* DynamoDB.Scan(sourceTable);
|
|
const TableName = yield* sourceTable.tableName;
|
|
const restoreTableToPointInTime = yield* DynamoDB.RestoreTableToPointInTime(
|
|
sourceTable,
|
|
restoreTargetTable,
|
|
);
|
|
const transactGetItems = yield* DynamoDB.TransactGetItems(sourceTable);
|
|
const transactWriteItems = yield* DynamoDB.TransactWriteItems(sourceTable);
|
|
const createBackup = yield* DynamoDB.CreateBackup(sourceTable);
|
|
const describeBackup = yield* DynamoDB.DescribeBackup(sourceTable);
|
|
const listBackups = yield* DynamoDB.ListBackups(sourceTable);
|
|
const deleteBackup = yield* DynamoDB.DeleteBackup(sourceTable);
|
|
const describeContinuousBackups =
|
|
yield* DynamoDB.DescribeContinuousBackups(sourceTable);
|
|
const restoreTableFromBackup = yield* DynamoDB.RestoreTableFromBackup(
|
|
sourceTable,
|
|
restoreTargetTable,
|
|
);
|
|
const exportTableToPointInTime = yield* DynamoDB.ExportTableToPointInTime(
|
|
sourceTable,
|
|
exportBucket,
|
|
);
|
|
const describeExport = yield* DynamoDB.DescribeExport(sourceTable);
|
|
const listExports = yield* DynamoDB.ListExports(sourceTable);
|
|
|
|
return {
|
|
fetch: Effect.gen(function* () {
|
|
const request = yield* HttpServerRequest;
|
|
const url = new URL(request.originalUrl);
|
|
const pathname = url.pathname;
|
|
|
|
yield* Effect.logInfo(
|
|
`Request: ${request.method} ${pathname} (originalUrl: ${request.originalUrl}, url: ${request.url})`,
|
|
);
|
|
|
|
if (request.method === "POST" && pathname === "/put") {
|
|
const body = (yield* request.json) as unknown as {
|
|
pk: string;
|
|
sk: string;
|
|
data?: string;
|
|
category?: string;
|
|
subcategory?: string;
|
|
rank?: string;
|
|
};
|
|
const result = yield* putItem({
|
|
Item: {
|
|
pk: { S: body.pk },
|
|
sk: { S: body.sk },
|
|
...(body.data ? { data: { S: body.data } } : {}),
|
|
// Natural attributes indexed by the multi-attribute-key GSI —
|
|
// no synthetic concatenated keys.
|
|
...(body.category ? { category: { S: body.category } } : {}),
|
|
...(body.subcategory
|
|
? { subcategory: { S: body.subcategory } }
|
|
: {}),
|
|
...(body.rank ? { rank: { S: body.rank } } : {}),
|
|
},
|
|
});
|
|
return yield* HttpServerResponse.json({ success: true, result });
|
|
}
|
|
|
|
if (request.method === "GET" && pathname === "/get") {
|
|
const pk = url.searchParams.get("pk");
|
|
const sk = url.searchParams.get("sk");
|
|
if (!pk || !sk) {
|
|
return HttpServerResponse.text("Missing pk or sk", { status: 400 });
|
|
}
|
|
const result = yield* getItem({
|
|
Key: {
|
|
pk: { S: pk },
|
|
sk: { S: sk },
|
|
},
|
|
});
|
|
return yield* HttpServerResponse.json({ item: result.Item });
|
|
}
|
|
|
|
if (request.method === "GET" && pathname === "/describe-table") {
|
|
const result = yield* describeTable();
|
|
return yield* HttpServerResponse.json({
|
|
table: result.Table,
|
|
});
|
|
}
|
|
|
|
if (request.method === "GET" && pathname === "/describe-ttl") {
|
|
const result = yield* describeTimeToLive();
|
|
return yield* HttpServerResponse.json({
|
|
timeToLiveDescription: result.TimeToLiveDescription,
|
|
});
|
|
}
|
|
|
|
if (request.method === "DELETE" && pathname === "/delete") {
|
|
const body = (yield* request.json) as unknown as {
|
|
pk: string;
|
|
sk: string;
|
|
};
|
|
const result = yield* deleteItem({
|
|
Key: {
|
|
pk: { S: body.pk },
|
|
sk: { S: body.sk },
|
|
},
|
|
});
|
|
return yield* HttpServerResponse.json({ success: true, result });
|
|
}
|
|
|
|
if (request.method === "POST" && pathname === "/update") {
|
|
const body = (yield* request.json) as unknown as {
|
|
pk: string;
|
|
sk: string;
|
|
data: string;
|
|
};
|
|
const result = yield* updateItem({
|
|
Key: {
|
|
pk: { S: body.pk },
|
|
sk: { S: body.sk },
|
|
},
|
|
UpdateExpression: "SET #data = :data",
|
|
ExpressionAttributeNames: { "#data": "data" },
|
|
ExpressionAttributeValues: { ":data": { S: body.data } },
|
|
ReturnValues: "ALL_NEW",
|
|
});
|
|
return yield* HttpServerResponse.json({
|
|
success: true,
|
|
attributes: result.Attributes,
|
|
});
|
|
}
|
|
|
|
if (request.method === "POST" && pathname === "/update-ttl") {
|
|
const body = (yield* request.json) as unknown as {
|
|
attributeName: string;
|
|
enabled: boolean;
|
|
};
|
|
const result = yield* updateTimeToLive({
|
|
TimeToLiveSpecification: {
|
|
AttributeName: body.attributeName,
|
|
Enabled: body.enabled,
|
|
},
|
|
});
|
|
return yield* HttpServerResponse.json({
|
|
timeToLiveSpecification: result.TimeToLiveSpecification,
|
|
});
|
|
}
|
|
|
|
if (request.method === "POST" && pathname === "/batch-write") {
|
|
const body =
|
|
(yield* request.json) as unknown as DynamoDB.BatchWriteItemRequest;
|
|
const result = yield* batchWriteItem(body);
|
|
return yield* HttpServerResponse.json({
|
|
unprocessedItems: result.UnprocessedItems ?? {},
|
|
});
|
|
}
|
|
|
|
if (request.method === "POST" && pathname === "/batch-get") {
|
|
const body =
|
|
(yield* request.json) as unknown as DynamoDB.BatchGetItemRequest;
|
|
const result = yield* batchGetItem(body);
|
|
return yield* HttpServerResponse.json({
|
|
responses: result.Responses ?? {},
|
|
unprocessedKeys: result.UnprocessedKeys ?? {},
|
|
});
|
|
}
|
|
|
|
if (request.method === "POST" && pathname === "/transact-write") {
|
|
const body =
|
|
(yield* request.json) as unknown as DynamoDB.TransactWriteItemsRequest;
|
|
const result = yield* transactWriteItems(body);
|
|
return yield* HttpServerResponse.json({
|
|
success: true,
|
|
result,
|
|
});
|
|
}
|
|
|
|
if (request.method === "POST" && pathname === "/transact-get") {
|
|
const body =
|
|
(yield* request.json) as unknown as DynamoDB.TransactGetItemsRequest;
|
|
const result = yield* transactGetItems(body);
|
|
return yield* HttpServerResponse.json({
|
|
responses: result.Responses ?? [],
|
|
});
|
|
}
|
|
|
|
if (request.method === "POST" && pathname === "/execute-transaction") {
|
|
const tableName = yield* TableName;
|
|
const result = yield* executeTransaction({
|
|
TransactStatements: [
|
|
{
|
|
Statement: `SELECT * FROM "${tableName}" WHERE pk=? AND sk=?`,
|
|
Parameters: [{ S: "tx#1" }, { S: "item1" }],
|
|
},
|
|
{
|
|
Statement: `SELECT * FROM "${tableName}" WHERE pk=? AND sk=?`,
|
|
Parameters: [{ S: "tx#1" }, { S: "item2" }],
|
|
},
|
|
],
|
|
});
|
|
return yield* HttpServerResponse.json({
|
|
responses: result.Responses,
|
|
});
|
|
}
|
|
|
|
if (request.method === "POST" && pathname === "/execute-statement") {
|
|
const body = (yield* request.json) as unknown as {
|
|
pk: string;
|
|
sk: string;
|
|
};
|
|
const tableName = yield* TableName;
|
|
const result = yield* executeStatement({
|
|
Statement: `SELECT * FROM "${tableName}" WHERE pk=? AND sk=?`,
|
|
Parameters: [{ S: body.pk }, { S: body.sk }],
|
|
});
|
|
return yield* HttpServerResponse.json({
|
|
items: result.Items ?? [],
|
|
});
|
|
}
|
|
|
|
if (
|
|
request.method === "POST" &&
|
|
pathname === "/batch-execute-statement"
|
|
) {
|
|
const body = (yield* request.json) as unknown as {
|
|
first: { pk: string; sk: string };
|
|
second: { pk: string; sk: string };
|
|
};
|
|
const sourceTableName = yield* TableName;
|
|
const result = yield* batchExecuteStatement({
|
|
Statements: [
|
|
{
|
|
Statement: `SELECT * FROM "${sourceTableName}" WHERE pk=? AND sk=?`,
|
|
Parameters: [{ S: body.first.pk }, { S: body.first.sk }],
|
|
},
|
|
{
|
|
Statement: `SELECT * FROM "${sourceTableName}" WHERE pk=? AND sk=?`,
|
|
Parameters: [{ S: body.second.pk }, { S: body.second.sk }],
|
|
},
|
|
],
|
|
});
|
|
return yield* HttpServerResponse.json({
|
|
responses: result.Responses ?? [],
|
|
});
|
|
}
|
|
|
|
if (request.method === "POST" && pathname === "/restore-table") {
|
|
const result = yield* restoreTableToPointInTime({
|
|
UseLatestRestorableTime: true,
|
|
}).pipe(
|
|
Effect.map((result) => ({
|
|
ok: true as const,
|
|
result,
|
|
})),
|
|
Effect.catch((error) =>
|
|
Effect.succeed({
|
|
ok: false as const,
|
|
error:
|
|
typeof error === "object" && error !== null && "_tag" in error
|
|
? (error as { _tag: string })._tag
|
|
: `${error}`,
|
|
}),
|
|
),
|
|
);
|
|
return yield* HttpServerResponse.json(result);
|
|
}
|
|
|
|
if (request.method === "GET" && pathname === "/query") {
|
|
const pk = url.searchParams.get("pk");
|
|
if (!pk) {
|
|
return HttpServerResponse.text("Missing pk", { status: 400 });
|
|
}
|
|
const result = yield* query({
|
|
KeyConditionExpression: "pk = :pk",
|
|
ExpressionAttributeValues: { ":pk": { S: pk } },
|
|
});
|
|
return yield* HttpServerResponse.json({
|
|
items: result.Items,
|
|
count: result.Count,
|
|
});
|
|
}
|
|
|
|
if (request.method === "GET" && pathname === "/query-multi") {
|
|
const category = url.searchParams.get("category");
|
|
const subcategory = url.searchParams.get("subcategory");
|
|
const rank = url.searchParams.get("rank");
|
|
if (!category || !subcategory) {
|
|
return HttpServerResponse.text("Missing category or subcategory", {
|
|
status: 400,
|
|
});
|
|
}
|
|
// Multi-attribute-key GSI query: every partition attribute must be
|
|
// an equality condition; sort attributes narrow left-to-right.
|
|
const result = yield* query({
|
|
IndexName: "MultiAttrIndex",
|
|
KeyConditionExpression: rank
|
|
? "#c = :c AND #s = :s AND #r = :r"
|
|
: "#c = :c AND #s = :s",
|
|
ExpressionAttributeNames: {
|
|
"#c": "category",
|
|
"#s": "subcategory",
|
|
...(rank ? { "#r": "rank" } : {}),
|
|
},
|
|
ExpressionAttributeValues: {
|
|
":c": { S: category },
|
|
":s": { S: subcategory },
|
|
...(rank ? { ":r": { S: rank } } : {}),
|
|
},
|
|
});
|
|
return yield* HttpServerResponse.json({
|
|
items: result.Items,
|
|
count: result.Count,
|
|
});
|
|
}
|
|
|
|
if (request.method === "GET" && pathname === "/list-tables") {
|
|
const result = yield* listTables();
|
|
return yield* HttpServerResponse.json({
|
|
tableNames: result.TableNames,
|
|
});
|
|
}
|
|
|
|
if (request.method === "GET" && pathname === "/list-tags") {
|
|
const result = yield* listTagsOfResource();
|
|
return yield* HttpServerResponse.json({
|
|
tags: result.Tags,
|
|
});
|
|
}
|
|
|
|
if (request.method === "POST" && pathname === "/create-backup") {
|
|
const body = (yield* request.json) as unknown as { name: string };
|
|
const result = yield* createBackup({ BackupName: body.name });
|
|
return yield* HttpServerResponse.json({
|
|
backupArn: result.BackupDetails?.BackupArn,
|
|
status: result.BackupDetails?.BackupStatus,
|
|
});
|
|
}
|
|
|
|
if (request.method === "GET" && pathname === "/describe-backup") {
|
|
const arn = url.searchParams.get("arn");
|
|
if (!arn) {
|
|
return HttpServerResponse.text("Missing arn", { status: 400 });
|
|
}
|
|
const result = yield* describeBackup({ BackupArn: arn });
|
|
return yield* HttpServerResponse.json({
|
|
status: result.BackupDescription?.BackupDetails?.BackupStatus,
|
|
});
|
|
}
|
|
|
|
if (request.method === "GET" && pathname === "/list-backups") {
|
|
const result = yield* listBackups();
|
|
return yield* HttpServerResponse.json({
|
|
backupArns: (result.BackupSummaries ?? []).map(
|
|
(summary) => summary.BackupArn,
|
|
),
|
|
});
|
|
}
|
|
|
|
if (request.method === "DELETE" && pathname === "/delete-backup") {
|
|
const body = (yield* request.json) as unknown as { arn: string };
|
|
const result = yield* deleteBackup({ BackupArn: body.arn }).pipe(
|
|
Effect.map((output) => ({
|
|
ok: true as const,
|
|
status: output.BackupDescription?.BackupDetails?.BackupStatus,
|
|
})),
|
|
Effect.catch((error) =>
|
|
Effect.succeed({ ok: false as const, error: error._tag }),
|
|
),
|
|
);
|
|
return yield* HttpServerResponse.json(result);
|
|
}
|
|
|
|
if (
|
|
request.method === "GET" &&
|
|
pathname === "/describe-continuous-backups"
|
|
) {
|
|
const result = yield* describeContinuousBackups();
|
|
return yield* HttpServerResponse.json({
|
|
continuousBackupsStatus:
|
|
result.ContinuousBackupsDescription?.ContinuousBackupsStatus,
|
|
pitrStatus:
|
|
result.ContinuousBackupsDescription
|
|
?.PointInTimeRecoveryDescription?.PointInTimeRecoveryStatus,
|
|
});
|
|
}
|
|
|
|
if (request.method === "POST" && pathname === "/restore-from-backup") {
|
|
const body = (yield* request.json) as unknown as { arn: string };
|
|
const result = yield* restoreTableFromBackup({
|
|
BackupArn: body.arn,
|
|
}).pipe(
|
|
Effect.map((output) => ({
|
|
ok: true as const,
|
|
status: output.TableDescription?.TableStatus,
|
|
})),
|
|
Effect.catch((error) =>
|
|
Effect.succeed({ ok: false as const, error: error._tag }),
|
|
),
|
|
);
|
|
return yield* HttpServerResponse.json(result);
|
|
}
|
|
|
|
if (request.method === "POST" && pathname === "/export-table") {
|
|
const result = yield* exportTableToPointInTime({
|
|
ExportFormat: "DYNAMODB_JSON",
|
|
}).pipe(
|
|
Effect.map((output) => ({
|
|
ok: true as const,
|
|
exportArn: output.ExportDescription?.ExportArn,
|
|
})),
|
|
Effect.catch((error) =>
|
|
Effect.succeed({ ok: false as const, error: error._tag }),
|
|
),
|
|
);
|
|
return yield* HttpServerResponse.json(result);
|
|
}
|
|
|
|
if (request.method === "GET" && pathname === "/list-exports") {
|
|
const result = yield* listExports();
|
|
return yield* HttpServerResponse.json({
|
|
exportArns: (result.ExportSummaries ?? []).map(
|
|
(summary) => summary.ExportArn,
|
|
),
|
|
});
|
|
}
|
|
|
|
if (request.method === "GET" && pathname === "/describe-export") {
|
|
const arn = url.searchParams.get("arn");
|
|
if (!arn) {
|
|
return HttpServerResponse.text("Missing arn", { status: 400 });
|
|
}
|
|
const result = yield* describeExport({ ExportArn: arn }).pipe(
|
|
Effect.map((output) => ({
|
|
ok: true as const,
|
|
status: output.ExportDescription?.ExportStatus,
|
|
})),
|
|
Effect.catch((error) =>
|
|
Effect.succeed({ ok: false as const, error: error._tag }),
|
|
),
|
|
);
|
|
return yield* HttpServerResponse.json(result);
|
|
}
|
|
|
|
if (request.method === "GET" && pathname === "/scan") {
|
|
const result = yield* scan({});
|
|
return yield* HttpServerResponse.json({
|
|
items: result.Items,
|
|
count: result.Count,
|
|
});
|
|
}
|
|
|
|
return yield* HttpServerResponse.json(
|
|
{
|
|
error: "Not found",
|
|
method: request.method,
|
|
pathname,
|
|
url: request.url,
|
|
},
|
|
{ status: 404 },
|
|
);
|
|
}).pipe(Effect.orDie),
|
|
};
|
|
}).pipe(
|
|
Effect.provide(
|
|
Layer.mergeAll(
|
|
DynamoDB.BatchExecuteStatementHttp,
|
|
DynamoDB.BatchGetItemHttp,
|
|
DynamoDB.BatchWriteItemHttp,
|
|
DynamoDB.CreateBackupHttp,
|
|
DynamoDB.DeleteBackupHttp,
|
|
DynamoDB.DescribeBackupHttp,
|
|
DynamoDB.DescribeContinuousBackupsHttp,
|
|
DynamoDB.DescribeExportHttp,
|
|
DynamoDB.ExportTableToPointInTimeHttp,
|
|
DynamoDB.ListBackupsHttp,
|
|
DynamoDB.ListExportsHttp,
|
|
DynamoDB.RestoreTableFromBackupHttp,
|
|
DynamoDB.DescribeTableHttp,
|
|
DynamoDB.DescribeTimeToLiveHttp,
|
|
DynamoDB.ExecuteStatementHttp,
|
|
DynamoDB.ExecuteTransactionHttp,
|
|
DynamoDB.GetItemHttp,
|
|
DynamoDB.ListTablesHttp,
|
|
DynamoDB.ListTagsOfResourceHttp,
|
|
DynamoDB.PutItemHttp,
|
|
DynamoDB.DeleteItemHttp,
|
|
DynamoDB.UpdateItemHttp,
|
|
DynamoDB.UpdateTimeToLiveHttp,
|
|
DynamoDB.QueryHttp,
|
|
DynamoDB.RestoreTableToPointInTimeHttp,
|
|
DynamoDB.ScanHttp,
|
|
DynamoDB.TransactGetItemsHttp,
|
|
DynamoDB.TransactWriteItemsHttp,
|
|
),
|
|
),
|
|
),
|
|
);
|