shared/database/local/runDatabaseMigration.ts (122 lines of code) (raw):
import { DynamoDBDocument } from "@aws-sdk/lib-dynamodb";
import { DynamoDB } from "@aws-sdk/client-dynamodb";
import { NativeAttributeValue } from "@aws-sdk/util-dynamodb";
import { standardAwsConfig } from "../../awsIntegration";
import { createDatabaseTunnel } from "./databaseTunnel";
import { getDatabaseConnection } from "../databaseConnection";
import { Sql } from "../types";
const dynamo = DynamoDBDocument.from(new DynamoDB(standardAwsConfig));
type AttributeMap = Record<string, NativeAttributeValue>;
async function getDynamoRows(TableName: string): Promise<AttributeMap[]> {
const getRows = async (startKey?: AttributeMap): Promise<AttributeMap[]> => {
const userResults = await dynamo.scan({
TableName,
ExclusiveStartKey: startKey,
});
const storedUsers = userResults.Items || [];
if (userResults.LastEvaluatedKey) {
return [...storedUsers, ...(await getRows(userResults.LastEvaluatedKey))];
} else {
return storedUsers;
}
};
return getRows();
}
export async function migrateUsers(sql: Sql, tableName: string) {
const users = await getDynamoRows(tableName);
console.log("NUMBER TO WRITE", users.length);
return Promise.allSettled(
users
.filter(({ firstName, lastName }) => firstName && lastName)
.map(
// eslint-disable-next-line @typescript-eslint/no-unused-vars
({ ttlEpochSeconds, manuallyOpenedPinboardIds, ...user }) =>
sql`
INSERT INTO "User" ${sql({
...user,
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
//@ts-ignore
manuallyOpenedPinboardIds: manuallyOpenedPinboardIds?.values || null,
})}
ON CONFLICT ("email") DO NOTHING`
)
);
}
export async function migrateItemsAndLastItemSeenByUser(
sql: Sql,
itemTableName: string,
lastItemSeenByUserTableName: string
) {
const items = await getDynamoRows(itemTableName);
const lastItemSeenByUsers = await getDynamoRows(lastItemSeenByUserTableName);
if ((await sql`SELECT COUNT(*) AS count FROM "Item"`)[0].count > 0) {
throw new Error("The 'Item' table is not empty. Aborting migration.");
}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
//@ts-ignore
items.sort((a, b) => a.timestamp - b.timestamp);
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
//@ts-ignore
// eslint-disable-next-line @typescript-eslint/no-unused-vars -- see https://github.com/eslint/eslint/issues/4880
for (const { id, timestamp, user, seenBy, ...item } of items) {
if (!item.userEmail) {
continue;
}
const newStyleID = (
await sql`
INSERT INTO "Item" ${sql({
...item,
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
//@ts-ignore
timestamp: new Date(timestamp * 1000),
})}
ON CONFLICT ("id") DO NOTHING
RETURNING "id"
`
)[0].id;
console.log(
"migrated Item with id",
id,
"- now has new style id",
newStyleID
);
const matchingLastItemSeenByUsers = lastItemSeenByUsers.filter(
({ itemID }) => itemID === id
);
console.log(
await Promise.all(
matchingLastItemSeenByUsers.map(
(lastItemSeenByUser) =>
sql`
INSERT INTO "LastItemSeenByUser" ${sql({
...lastItemSeenByUser,
seenAt: new Date(lastItemSeenByUser.seenAt * 1000),
itemID: newStyleID,
})}
`
)
)
);
}
}
(async () => {
const DYNAMO_TABLE_NAMES = {
CODE: {
User: "pinboard-CODE-pinboardusertable2621B03F-16NOYN7UMQ1RS",
Item: "pinboard-CODE-pinboarditemtable83382753-1QVZDRAX9CZ3I",
LastItemSeenByUser:
"pinboard-CODE-pinboardlastitemseenbyusertable132BE99C-15W8MHP4HJB1N",
},
PROD: {
User: "pinboard-PROD-pinboardusertable2621B03F-108EXGU72O7BW",
Item: "pinboard-PROD-pinboarditemtable83382753-1LQARAGXCSLI8",
LastItemSeenByUser:
"pinboard-PROD-pinboardlastitemseenbyusertable132BE99C-DFE4L38U1XBV",
},
};
const stage: "CODE" | "PROD" = await createDatabaseTunnel();
const sql = await getDatabaseConnection();
const dynamoTableNames = DYNAMO_TABLE_NAMES[stage];
console.log(await migrateUsers(sql, dynamoTableNames.User));
console.log(
await migrateItemsAndLastItemSeenByUser(
sql,
dynamoTableNames.Item,
dynamoTableNames.LastItemSeenByUser
)
);
})();