diff --git a/backend/backend/backend-src/lib/aws.ts b/backend/backend/backend-src/lib/aws.ts index 1317654..0b7b8ab 100644 --- a/backend/backend/backend-src/lib/aws.ts +++ b/backend/backend/backend-src/lib/aws.ts @@ -1,5 +1,6 @@ import fs from 'fs' import { + DeleteItemCommand, DynamoDBClient, GetItemCommand, PutItemCommand, @@ -9,11 +10,14 @@ import { TransactGetItemsCommand, TransactWriteItem, TransactWriteItemsCommand, + UpdateItemCommand, } from '@aws-sdk/client-dynamodb' import {marshall, unmarshall} from '@aws-sdk/util-dynamodb' import {DeleteObjectCommand, PutObjectCommand, S3Client} from '@aws-sdk/client-s3' import {getSignedUrl} from '@aws-sdk/s3-request-presigner' +import slug from 'slug' + import * as IFace from './interfaces' const BUCKET = 's3m-contracts-dev' @@ -368,83 +372,107 @@ export async function getAddressEventsDB(address: string) { else return response.Items?.map(item => unmarshall(item)) ?? [] } -export async function getRelatedDB(address: string): Promise[]> { +export async function listRelatedDB(pkgaddress: string) { + const command = new QueryCommand({ + TableName: RELATED_TABLE, + KeyConditionExpression: 'address = :address', + ExpressionAttributeValues: { + ':address': {S: pkgaddress}, + }, + ProjectionExpression: 'slug, label, wallet_address', + }) + + const response = await dbclient.send(command) + + if (response.Items === undefined) return [] + else return response.Items?.map(item => unmarshall(item)) ?? [] +} + +export async function getRelatedDB(pkgaddress: string, slug: string) { const command = new GetItemCommand({ TableName: RELATED_TABLE, Key: { - address: {S: address}, + address: {S: pkgaddress}, + slug: {S: slug}, }, - ProjectionExpression: 'related', }) const response = await dbclient.send(command) - if (response.Item === undefined) return [] + if (response.Item === undefined) return null - return (unmarshall(response.Item) ?? {}).related + return unmarshall(response.Item) } export async function createRelatedDB(address: string, data: IFace.IRelatedCreate) { // get existing item, if any - const existing: Record[] = await getRelatedDB(address) - - const already = existing.filter(item => item.label == data.label) - if (already.length != 0) { - return {error: `label '${data.label}' already exists for address: ${already[0].address}`} - } + const label_slug = slug(data.label) + const existing = await getRelatedDB(address, label_slug) + if (existing !== null) return {error: `label '${data.label}' already exists for address: ${existing.address}`} const command = new PutItemCommand({ TableName: RELATED_TABLE, Item: marshall({ address: address, - related: existing.concat({label: data.label, address: data.address}), + slug: label_slug, + label: data.label, + wallet_address: data.address, }), }) return await dbclient.send(command) } -export async function deleteRelatedDB(address: string, data: IFace.IRelatedDelete) { +export async function deleteRelatedDB(address: string, slug: string) { // get existing item, if any - const existing = await getRelatedDB(address) - if (existing.length == 0) return - - const filtered = existing.filter(item => item.label != data.label) + const existing = await getRelatedDB(address, slug) + if (existing === null) return - const command = new PutItemCommand({ + const command = new DeleteItemCommand({ TableName: RELATED_TABLE, - Item: marshall({ - address: address, - related: filtered, - }), + Key: { + address: {S: address}, + slug: {S: slug}, + }, }) return await dbclient.send(command) } -export async function modifyRelatedDB(address: string, data: IFace.IRelatedModify) { - // get existing item, if any - const existing = await getRelatedDB(address) - if (existing.length == 0) return - - let item_address = '' - let item_idx = 0 - for (let idx = 0; idx < existing.length; idx++) { - if (existing[idx].label == data.label) { - item_address = existing[idx].address - item_idx = idx - } else if (existing[idx].label == data.new_label) { - return {error: `label '${data.new_label}' already exists for address: ${existing[idx].address}`} - } - } - existing[item_idx].label = data.new_label +export async function modifyRelatedDB(address: string, existing_slug: string, data: IFace.IRelatedModify) { + // check if the new label already exists + const new_slug = slug(data.label) + const maybe = await getRelatedDB(address, new_slug) + if (maybe !== null) return {error: `label '${data.label}' already exists for address: ${address}`} - const command = new PutItemCommand({ - TableName: RELATED_TABLE, - Item: marshall({ - address: address, - related: existing, - }), + const existing = await getRelatedDB(address, existing_slug) + if (existing == null) return {error: `entry '${existing_slug}' does not exist for address: ${address}`} + + let items: TransactWriteItem[] = [ + { + Delete: { + TableName: RELATED_TABLE, + Key: { + address: {S: address}, + slug: {S: existing_slug}, + }, + }, + }, + { + Put: { + TableName: RELATED_TABLE, + Item: marshall({ + address: address, + slug: new_slug, + label: data.label, + wallet_address: existing.wallet_address, + }), + }, + }, + ] + + const txWriteCommand = new TransactWriteItemsCommand({ + TransactItems: items, }) - return await dbclient.send(command) + await dbclient.send(txWriteCommand) } diff --git a/backend/backend/backend-src/lib/checks.ts b/backend/backend/backend-src/lib/checks.ts index 192ca72..54deaf7 100644 --- a/backend/backend/backend-src/lib/checks.ts +++ b/backend/backend/backend-src/lib/checks.ts @@ -268,19 +268,9 @@ export async function validRelatedDelete(data: IFace.IRelatedDelete): Promise { const stringFields = ['label', 'new_label'] - for (const field of stringFields) { - if (!(field in data)) { - console.log(`missing field: ${field}`) - return {error: `missing field: ${field}`} - } - } - - if (data.label.trim() == '' || data.new_label.trim() == '') { - console.log('missing field') - return {error: 'missing field'} - } else { - data.label = data.label.trim() - data.new_label = data.new_label.trim() + if (!('label' in data) || data.label.trim() == '') { + console.log(`missing field: label`) + return {error: `missing field: label`} } return {error: '', data: data} diff --git a/backend/backend/backend-src/lib/interfaces.ts b/backend/backend/backend-src/lib/interfaces.ts index c88f800..63af539 100644 --- a/backend/backend/backend-src/lib/interfaces.ts +++ b/backend/backend/backend-src/lib/interfaces.ts @@ -60,7 +60,6 @@ export interface IRelatedDelete { export interface IRelatedModify { label: string - new_label: string } export function reqToCreated(data: ICreatePackageRequest, s3key: string | undefined): IPackageCreated { diff --git a/backend/backend/backend-src/server.ts b/backend/backend/backend-src/server.ts index 6632a60..bbe45b1 100644 --- a/backend/backend/backend-src/server.ts +++ b/backend/backend/backend-src/server.ts @@ -145,26 +145,26 @@ app.get('/address-events/:address', async (req, res) => { } }) -app.get('/related/:address', async (req, res) => { - const {address} = req.params - if (Checks.isValidAddress(address)) { +app.get('/related/:pkgaddress', async (req, res) => { + const {pkgaddress} = req.params + if (Checks.isValidPackage(pkgaddress)) { res.status(200).json({ status: 'ok', - related: await AWS.getRelatedDB(address), + related: await AWS.listRelatedDB(pkgaddress), }) } else { res.status(400).json({ status: 'error', - message: `invalid address: ${address}`, + message: `invalid package address: ${pkgaddress}`, }) } }) -app.post('/related/:address', async (req, res) => { - const {address} = req.params +app.post('/related/:pkgaddress', async (req, res) => { + const {pkgaddress} = req.params const v = await Checks.validRelatedCreate(req.body) - if (Checks.isValidAddress(address) && v.error === '') { - const ret = await AWS.createRelatedDB(address, v.data as IFace.IRelatedCreate) + if (Checks.isValidPackage(pkgaddress) && v.error === '') { + const ret = await AWS.createRelatedDB(pkgaddress, v.data as IFace.IRelatedCreate) if (ret !== undefined && 'error' in ret) { res.status(400).json({ status: 'error', @@ -183,37 +183,31 @@ app.post('/related/:address', async (req, res) => { } else { res.status(400).json({ status: 'error', - message: `invalid address: ${address}`, + message: `invalid address: ${pkgaddress}`, }) } }) -app.post('/related/:address/delete', async (req, res) => { - const {address} = req.params - const v = await Checks.validRelatedDelete(req.body) - if (Checks.isValidAddress(address) && v.error === '') { - await AWS.deleteRelatedDB(address, v.data as IFace.IRelatedDelete) +app.delete('/related/:pkgaddress/:slug', async (req, res) => { + const {pkgaddress, slug} = req.params + if (Checks.isValidPackage(pkgaddress) && slug.trim() != '') { + await AWS.deleteRelatedDB(pkgaddress, slug) res.status(200).json({ status: 'ok', }) - } else if (v.error != '') { - res.status(400).json({ - status: 'error', - message: v.error, - }) } else { res.status(400).json({ status: 'error', - message: `invalid address: ${address}`, + message: `invalid address: ${pkgaddress}`, }) } }) -app.post('/related/:address/modify', async (req, res) => { - const {address} = req.params +app.patch('/related/:pkgaddress/:slug', async (req, res) => { + const {pkgaddress, slug} = req.params const v = await Checks.validRelatedModify(req.body) - if (Checks.isValidAddress(address) && v.error === '') { - const ret = await AWS.modifyRelatedDB(address, v.data as IFace.IRelatedModify) + if (Checks.isValidPackage(pkgaddress) && v.error === '') { + const ret = await AWS.modifyRelatedDB(pkgaddress, slug, v.data as IFace.IRelatedModify) if (ret !== undefined && 'error' in ret) { res.status(400).json({ status: 'error', @@ -232,7 +226,7 @@ app.post('/related/:address/modify', async (req, res) => { } else { res.status(400).json({ status: 'error', - message: `invalid address: ${address}`, + message: `invalid address: ${pkgaddress}`, }) } }) diff --git a/backend/backend/package.json b/backend/backend/package.json index 85ce31d..a8408f2 100644 --- a/backend/backend/package.json +++ b/backend/backend/package.json @@ -21,6 +21,7 @@ "cors": "^2.8.5", "ejs": "^3.1.9", "express": "^4.18.2", + "slug": "^9.0.0", "zip-lib": "^1.0.3" }, "devDependencies": { @@ -29,6 +30,7 @@ "@types/ejs": "^3.1.5", "@types/express": "^4.17.21", "@types/node": "^20.11.17", + "@types/slug": "^5.0.8", "nodemon": "^3.0.3", "prettier": "^3.2.5", "ts-loader": "^9.5.1", diff --git a/backend/backend/pnpm-lock.yaml b/backend/backend/pnpm-lock.yaml index 073fd4b..5e07e88 100644 --- a/backend/backend/pnpm-lock.yaml +++ b/backend/backend/pnpm-lock.yaml @@ -29,6 +29,9 @@ dependencies: express: specifier: ^4.18.2 version: 4.18.3 + slug: + specifier: ^9.0.0 + version: 9.0.0 zip-lib: specifier: ^1.0.3 version: 1.0.3 @@ -49,6 +52,9 @@ devDependencies: '@types/node': specifier: ^20.11.17 version: 20.11.29 + '@types/slug': + specifier: ^5.0.8 + version: 5.0.8 nodemon: specifier: ^3.0.3 version: 3.1.0 @@ -1539,6 +1545,10 @@ packages: '@types/node': 20.11.29 dev: true + /@types/slug@5.0.8: + resolution: {integrity: sha512-mblTWR1OST257k1gZ3QvqG+ERSr8Ea6dyM1FH6Jtm4jeXi0/r0/95VNctofuiywPxCVQuE8AuFoqmvJ4iVUlXQ==} + dev: true + /@webassemblyjs/ast@1.12.1: resolution: {integrity: sha512-EKfMUOPRRUTy5UII4qJDGPpqfwjOmZ5jeGFwid9mnoqIFK+e0vqoi1qH56JpmZSzEL53jKnNzScdmftJyG5xWg==} dependencies: @@ -2861,6 +2871,11 @@ packages: semver: 7.6.0 dev: true + /slug@9.0.0: + resolution: {integrity: sha512-ixytnHlpHPWM56heaGgYe/M8tDAcpJcsg/zBuyElbFDOORzMGOeP3Te6iJBRVYu3WQEiWLQPb70Gh9ig/sZgGQ==} + hasBin: true + dev: false + /source-map-support@0.5.21: resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} dependencies: