Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
214 changes: 198 additions & 16 deletions scripts/ingestPartnerRegistrations.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
import fs from "fs";
import csv from "csv-parser";
import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
import { DynamoDBDocumentClient, TransactWriteCommand } from "@aws-sdk/lib-dynamodb";
import {
DynamoDBDocumentClient,
GetCommand,
PutCommand,
TransactWriteCommand,
UpdateCommand

Check warning on line 9 in scripts/ingestPartnerRegistrations.js

View workflow job for this annotation

GitHub Actions / eslint

'UpdateCommand' is defined but never used
} from "@aws-sdk/lib-dynamodb";
import { humanId } from "human-id";
import dotenv from "dotenv";

Expand All @@ -23,8 +29,154 @@
const PROFILES_TABLE = "biztechProfiles" + (process.env.ENVIRONMENT || "");
const MEMBERS2026_TABLE = "biztechMembers2026" + (process.env.ENVIRONMENT || "");
const USER_REGISTRATIONS_TABLE = "biztechRegistrations" + (process.env.ENVIRONMENT || "");
const EVENT_ID_AND_YEAR = "blueprint;2026";

// TODO
// NOTE: ADJUSTED FOR BLUEPRINT 2026

// Fallback in case email already exists
async function upsertPartnerEntries(user) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't need a fallback here, just a PutCommand will handle creating new if it doesn't exist, or updating it in case it does

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes we do
image
In the first pass, you have ConditionExpression: attribute_not_exists(id). I lowkey don't really know what that means but chatgpt is saying that means if user email already exists in any of those tables, DynamoDB throws ConditionalCheckFailedException

Copy link
Member Author

@elijahzhao24 elijahzhao24 Jan 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will just automatically throw and without fallback, won't update partner profile or sign them up for blueprint.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can't we just remove the condition expression then?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

overwriting stuff may not be smart

const timestamp = new Date().getTime();
const email = user.email.toLowerCase();

try {
await docClient.send(
new PutCommand({
TableName: USERS_TABLE,
Item: {
id: email,
fname: user.fname,
lname: user.lname,
isMember: true,
createdAt: timestamp,
updatedAt: timestamp
},
ConditionExpression: "attribute_not_exists(id)"
})
);
} catch (error) {
if (error.name !== "ConditionalCheckFailedException") {
throw error;
}
}

let member = null;
const memberResult = await docClient.send(
new GetCommand({
TableName: MEMBERS2026_TABLE,
Key: {
id: email
}
})
);
member = memberResult.Item || null;

// get biztechMembers2026 record to see if memeber exists, and get profileId if it exists
// else generate a new profileID
let profileID = member?.profileID;
if (!profileID) {
profileID = humanId();
}

// upsert new member with profileID
if (!member) {
try {
await docClient.send(
new PutCommand({
TableName: MEMBERS2026_TABLE,
Item: {
id: email,
profileID,
firstName: user.fname,
lastName: user.lname,
createdAt: timestamp,
updatedAt: timestamp
},
ConditionExpression: "attribute_not_exists(id)"
})
);
} catch (error) {
if (error.name !== "ConditionalCheckFailedException") {
throw error;
}
}
}

// Only update missing fields in biztechProfiles
try {
await docClient.send(
new PutCommand({
TableName: PROFILES_TABLE,
Item: {
fname: user.fname,
lname: user.lname,
pronouns: user.pronouns,
type: "PROFILE",
compositeID: `PROFILE#${profileID}`,
createdAt: timestamp,
updatedAt: timestamp,
profileType: "PARTNER",
linkedIn: user.linkedin,
company: user.company,
position: user.position,
viewableMap: {
fname: true,
lname: true,
pronouns: true,
major: false,
year: false,
profileType: true,
hobby1: false,
hobby2: false,
funQuestion1: false,
funQuestion2: false,
linkedIn: true,
profilePictureURL: false,
additionalLink: false,
description: false,
company: true,
position: true
}
},
ConditionExpression: "attribute_not_exists(compositeID)"
})
);
} catch (error) {
if (error.name !== "ConditionalCheckFailedException") {
throw error;
}
}

// write registration if missing
try {
await docClient.send(
new PutCommand({
TableName: USER_REGISTRATIONS_TABLE,
Item: {
id: email,
["eventID;year"]: EVENT_ID_AND_YEAR,
registrationStatus: "registered",
isPartner: true,
fname: user.fname,
createdAt: timestamp,
updatedAt: timestamp
},
ExpressionAttributeNames: {
"#eventIDYear": "eventID;year"
},
ConditionExpression:
"attribute_not_exists(id) and attribute_not_exists(#eventIDYear)"
})
);
} catch (error) {
if (error.name !== "ConditionalCheckFailedException") {
throw error;
}
}

return true;
}

// NOTE: Usage of this is mainly for kickstart;2025 event
async function updateTables(user) {
const timestamp = new Date().getTime();
const profileID = humanId();
Expand Down Expand Up @@ -69,13 +221,13 @@
Item: {
fname: user.fname,
lname: user.lname,
pronouns: user.pronouns,
type: "PROFILE",
compositeID: `PROFILE#${profileID}`,
createdAt: timestamp,
updatedAt: timestamp,
profileType: "PARTNER",
linkedIn: user.linkedin,
pronouns: user.pronouns,
company: user.company,
position: user.position,
viewableMap: {
Expand All @@ -100,20 +252,24 @@
ConditionExpression: "attribute_not_exists(id)"
}
},
// 4. Create a registration for kickstart so they can invest (partner-flagged investment)
// 4. Create a registration for blueprint
{
Put: {
TableName: USER_REGISTRATIONS_TABLE,
Item: {
id: user.email.toLowerCase(),
["eventID;year"]: "kickstart;2025",
registrationStatus: "acceptedComplete",
["eventID;year"]: EVENT_ID_AND_YEAR,
registrationStatus: "registered",
isPartner: true, // flag as partner investment
fname: user.fname,
createdAt: timestamp,
updatedAt: timestamp,
},
ConditionExpression: "attribute_not_exists(id)"
ExpressionAttributeNames: {
"#eventIDYear": "eventID;year"
},
ConditionExpression:
"attribute_not_exists(id) and attribute_not_exists(#eventIDYear)"
}
}
]
Expand All @@ -127,7 +283,15 @@
return true;
} catch (error) {
if (error.name === "ConditionalCheckFailedException") {
console.log(`Entry already exists for ${user.email}`);
console.log(`Entry already exists for ${user.email}, attempting profile upsert`);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above, we should not need to have additional logic in the catch clause

try {
await upsertPartnerEntries(user);
return true;
} catch (upsertError) {
console.error(`Upsert failed for ${user.email}:`, upsertError.message);
console.error("Error details:", upsertError);
return false;
}
} else {
console.error(`Error processing ${user.email}:`, error.message);
console.error("Error details:", error);
Expand Down Expand Up @@ -161,18 +325,36 @@
.on("end", async () => {
console.log(`Found ${results.length} records to process`);

const getValue = (row, key) => (row[key] || "").trim();

const getFirstLastName = (fullName) => {
const cleaned = fullName.trim();
if (!cleaned) return ["", ""];
const parts = cleaned.split(/\s+/);
if (parts.length === 1) return [parts[0], ""];
const last = parts.pop();
return [parts.join(" "), last];
};

// Process each record
for (const [index, row] of results.entries()) {
try {
// ADJUST BASED ON CSV
// Mapped to current partner CSV headers
const displayName =
getValue(row, "Full Name");
const [firstname, lastname] = getFirstLastName(displayName);

const user = {
email: row["Email Address"]?.trim().toLowerCase() || "", // sanitize compendium emails
fname: row["First Name"],
lname: row["Last Name"],
pronouns: row["Pronouns"],
linkedin: row["LinkedIn"],
company: row["What organization will you be representing?"], // adjust
position: row["What is your current role?"] // adjust
email: getValue(row, "Email Address").toLowerCase(),
fname: firstname,
lname: lastname,
pronouns: getValue(row, "Pronouns"),
linkedin: getValue(
row,
"Please include your LinkedIn URL if your profile is not public"
),
company: getValue(row, "What company do you work for?"),
position: getValue(row, "What is your current role?")
};

const success = await updateTables(user);
Expand Down Expand Up @@ -204,7 +386,7 @@

if (errors.length > 0) {
console.log("\nErrors encountered:");
errors.forEach((err, idx) => {

Check warning on line 389 in scripts/ingestPartnerRegistrations.js

View workflow job for this annotation

GitHub Actions / eslint

'idx' is defined but never used
console.log(`\nRow ${err.row}: ${err.error}`);
console.log("Data:", JSON.stringify(err.data, null, 2));
});
Expand Down
Loading