Skip to content

Instantly share code, notes, and snippets.

@mostlylikeable
Created August 18, 2022 02:53
Show Gist options
  • Save mostlylikeable/c0b7d3a7b9147a3e6a004c3cdf81c4d7 to your computer and use it in GitHub Desktop.
Save mostlylikeable/c0b7d3a7b9147a3e6a004c3cdf81c4d7 to your computer and use it in GitHub Desktop.
DynamoDb TypeScript Example
import { DynamoDB } from '@aws-sdk/client-dynamodb';
import { DynamoDBDocument } from '@aws-sdk/lib-dynamodb';
const tableName = 'discography';
const artistAlbumsIndex = 'artist-albums';
const table = {
TableName: tableName,
BillingMode: 'PAY_PER_REQUEST',
AttributeDefinitions: [
{ AttributeName: 'pk', AttributeType: 'S' },
{ AttributeName: 'sk', AttributeType: 'S' },
{ AttributeName: 'artist_key', AttributeType: 'S' }, // case-insensitive artist for lookups
{ AttributeName: 'album', AttributeType: 'S' },
],
KeySchema: [
{ AttributeName: 'pk', KeyType: 'HASH' },
{ AttributeName: 'sk', KeyType: 'RANGE' },
],
GlobalSecondaryIndexes: [ // so we can look up albums by artist
{
IndexName: artistAlbumsIndex,
KeySchema: [
{ AttributeName: 'artist_key', KeyType: 'HASH' },
{ AttributeName: 'album', KeyType: 'RANGE' },
],
Projection: {
ProjectionType: 'ALL',
},
},
],
};
const createClient = (): DynamoDB => {
return new DynamoDB({
region: 'us-east-1',
endpoint: 'http://localhost:4566',
credentials: {
accessKeyId: 'DUMMY',
secretAccessKey: 'DUMMY',
},
});
};
export const runExample = async () => {
const client = createClient();
try {
await client.createTable(table);
try {
const dynamo = await DynamoDBDocument.from(client);
await seed(dynamo);
await run(dynamo);
} finally {
await client.deleteTable({ TableName: tableName });
}
} finally {
client.destroy();
}
};
const seed = async (dynamo: DynamoDBDocument) => {
const promises = albums.map(async (it) => await saveAlbum(dynamo, it));
await Promise.all(promises);
};
const run = async (dynamo: DynamoDBDocument) => {
// get all albums by artist
const discography = await albumsByArtist(dynamo, 'The Weekend');
console.log('Discography', discography);
const album = await albumDetail(dynamo, 'The Weekend', 'Starboy');
console.log('The Weekend - Starboy', album);
};
const albumsByArtist = async (dynamo: DynamoDBDocument, artist: string): Promise<Album[]> => {
const items = await dynamo.query({
TableName: tableName,
IndexName: artistAlbumsIndex,
KeyConditionExpression: 'artist_key = :artist',
ExpressionAttributeValues: {
':artist': artist.toLowerCase(),
},
ScanIndexForward: false,
});
return items?.Items?.map(i => new Album(i)) || [];
};
const albumDetail = async (dynamo: DynamoDBDocument, artist: string, album: string): Promise<AlbumDetail> => {
const items = await dynamo.query({
TableName: tableName,
KeyConditionExpression: 'pk = :pk',
ExpressionAttributeValues: {
':pk': `ALBUM#${artist.toLowerCase()}#${album.toLowerCase()}`,
},
ScanIndexForward: false,
});
const found = { songs: [] } as { album?: Album, songs: Song[] };
items?.Items?.forEach(i => {
if (i.sk.startsWith('ALBUM#')) {
found.album = new Album(i);
} else {
found.songs.push(new Song(i));
}
});
required(found.album);
return {
...found.album!,
tracks: found.songs.sort((s1, s2) => s1.track - s2.track),
};
};
const saveAlbum = async (dynamo: DynamoDBDocument, album: AlbumDetail) => {
await dynamo.transactWrite({
TransactItems: [
{
Put: {
TableName: tableName,
Item: {
pk: `ALBUM#${album.artist.toLowerCase()}#${album.album.toLowerCase()}`,
sk: `ALBUM#${album.artist.toLowerCase()}#${album.album.toLowerCase()}`,
artist: album.artist,
artist_key: album.artist.toLowerCase(),
album: album.album,
year: album.year,
label: album.label,
type: album.type,
} as AlbumItem,
},
},
...album.tracks.map((song) => ({
Put: {
TableName: tableName,
Item: {
pk: `ALBUM#${album.artist.toLowerCase()}#${album.album.toLowerCase()}`,
sk: `SONG#${song.track}`,
track: song.track,
title: song.title,
} as SongItem,
},
})),
],
});
};
class Album {
artist: string;
album: string;
year: number;
label: string;
type: 'LP' | 'EP' | 'S';
constructor(partial: Partial<Album>) {
this.artist = required(partial.artist);
this.album = required(partial.album);
this.year = required(partial.year);
this.label = required(partial.label);
this.type = required(partial.type);
}
}
class Song {
track: number;
title: string;
constructor(partial: Partial<Song>) {
this.track = required(partial.track);
this.title = required(partial.title);
}
}
type AlbumDetail = Album & {
tracks: Song[];
};
type DynamoItem = { pk: string; sk: string };
type AlbumItem = Omit<Album, 'songs'> & DynamoItem;
type SongItem = Song & DynamoItem;
const required = <T = any>(value: T | null | undefined): T => {
if (value === null || value === undefined) {
throw new Error('Value is required');
}
return value as T;
};
const albums: AlbumDetail[] = [
{
artist: 'The Weekend',
album: 'Starboy',
year: 2016,
label: 'Republic',
type: 'LP',
tracks: [
{ track: 1, title: 'Starboy' },
{ track: 2, title: 'Party Monster' },
{ track: 3, title: 'False Alarm' },
{ track: 4, title: 'Reminder' },
{ track: 5, title: "Rockin'" },
{ track: 6, title: 'Secrets' },
{ track: 7, title: 'True Colors' },
{ track: 8, title: 'Stargirl (Interlude)' },
{ track: 9, title: 'Sidewalks' },
{ track: 10, title: 'Six Feet Under' },
{ track: 11, title: 'Love to Lay' },
{ track: 12, title: 'A Lonely Night' },
{ track: 13, title: 'Attention' },
{ track: 14, title: 'Orderly Life' },
{ track: 15, title: 'Nothing Without You' },
{ track: 16, title: 'All I Know' },
{ track: 17, title: 'Die for You' },
{ track: 18, title: 'I Feel It Coming' },
],
},
{
artist: 'The Weekend',
album: 'The Hills',
year: 2015,
label: 'Universal',
type: 'S',
tracks: [
{ track: 1, title: 'The Hills' },
],
},
];
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment