@shopify/shopify-app-session-storage-dynamodb
Version:
Shopify App Session Storage for DynamoDB
131 lines (109 loc) • 3.79 kB
text/typescript
import {
AttributeValue,
DeleteItemCommand,
DynamoDBClient,
DynamoDBClientConfig,
GetItemCommand,
PutItemCommand,
QueryCommand,
} from '@aws-sdk/client-dynamodb';
import {marshall, unmarshall} from '@aws-sdk/util-dynamodb';
import {Session, SessionParams} from '@shopify/shopify-api';
import {SessionStorage} from '@shopify/shopify-app-session-storage';
export interface DynamoDBSessionStorageOptions {
sessionTableName: string;
shopIndexName: string;
config?: DynamoDBClientConfig;
}
const defaultDynamoDBSessionStorageOptions: DynamoDBSessionStorageOptions = {
sessionTableName: 'shopify_sessions',
shopIndexName: 'shop_index',
};
export class DynamoDBSessionStorage implements SessionStorage {
private client: DynamoDBClient;
private options: DynamoDBSessionStorageOptions;
constructor(opts?: DynamoDBSessionStorageOptions) {
this.options = {...defaultDynamoDBSessionStorageOptions, ...opts};
this.client = new DynamoDBClient({...this.options.config});
}
public async storeSession(session: Session): Promise<boolean> {
await this.client.send(
new PutItemCommand({
TableName: this.options.sessionTableName,
Item: this.serializeSession(session),
}),
);
return true;
}
public async loadSession(id: string): Promise<Session | undefined> {
if (!id) return undefined;
const result = await this.client.send(
new GetItemCommand({
TableName: this.options.sessionTableName,
Key: this.serializeId(id),
}),
);
return result.Item ? this.deserializeSession(result.Item) : undefined;
}
public async deleteSession(id: string): Promise<boolean> {
await this.client.send(
new DeleteItemCommand({
TableName: this.options.sessionTableName,
Key: this.serializeId(id),
}),
);
return true;
}
public async deleteSessions(ids: string[]): Promise<boolean> {
await Promise.all(ids.map((id) => this.deleteSession(id)));
return true;
}
public async findSessionsByShop(shop: string): Promise<Session[]> {
const result = await this.client.send(
new QueryCommand({
TableName: this.options.sessionTableName,
IndexName: this.options.shopIndexName,
KeyConditionExpression: 'shop = :shop',
ExpressionAttributeValues: marshall({
':shop': shop,
}),
ProjectionExpression: 'id, shop',
}),
);
const sessions = await Promise.all(
result.Items?.map((item) => this.loadSession(this.deserializeId(item))) ||
[],
);
return sessions.filter(
(session): session is Session => session !== undefined,
);
}
private serializeId(id: string): Record<string, AttributeValue> {
return marshall({id});
}
private deserializeId(id: Record<string, AttributeValue>): string {
return unmarshall(id).id;
}
private serializeSession(session: Session): Record<string, AttributeValue> {
// DynamoDB doesn't support Date objects, so we need to convert them to ISO strings
const rawSession = {
...session.toObject(),
expires: session.expires?.toISOString(),
refreshTokenExpires: session.refreshTokenExpires?.toISOString(),
};
return marshall(rawSession, {
removeUndefinedValues: true,
});
}
private deserializeSession(session: Record<string, AttributeValue>): Session {
const rawSession = unmarshall(session) as SessionParams;
// Convert ISO strings back to Date objects
return new Session({
...rawSession,
expires: rawSession.expires ? new Date(rawSession.expires) : undefined,
refreshTokenExpires: rawSession.refreshTokenExpires
? new Date(rawSession.refreshTokenExpires as unknown as string)
: undefined,
});
}
}