Skip to content

Commit

Permalink
Merge branch 'referrals-tokens' of https://github.com/dubinc/dub into…
Browse files Browse the repository at this point in the history
… referrals-tokens
  • Loading branch information
steven-tey committed Nov 22, 2024
2 parents c0e9ee3 + f3a4696 commit 40a8574
Show file tree
Hide file tree
Showing 2 changed files with 130 additions and 61 deletions.
173 changes: 118 additions & 55 deletions apps/web/app/api/cron/import/csv/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,24 @@ import { sendCsvImportEmails } from "./utils";

export const dynamic = "force-dynamic";

// Type for mapper return value
type MapperResult =
| {
success: true;
data: {
domain: string;
key: string;
createdAt?: Date;
tags?: string[];
[key: string]: any;
};
}
| {
success?: false;
error: string;
link: { domain: string; key: string };
};

export async function POST(req: Request) {
try {
const body = await req.json();
Expand All @@ -34,37 +52,65 @@ export async function POST(req: Request) {

if (!id || !url) throw new Error("Missing ID or URL for the import file");

const mapper = (row: Record<string, string>) => {
const actualKey = normalizeString(mapping.link);
const linkUrl = actualKey ? getPrettyUrl(row[actualKey]) : "";
const mapper = (row: Record<string, string>): MapperResult => {
const getValueByNormalizedKey = (targetKey: string): string => {
const key = Object.keys(row).find(
(k) => normalizeString(k) === normalizeString(targetKey),
);
return key ? row[key].trim() : "";
};

return {
...Object.fromEntries(
Object.entries(mapping).map(([key, value]) => {
// Find the actual key for each mapped field
const csvKey = Object.keys(row).find(
(k) => normalizeString(k) === normalizeString(value),
);
return [key, csvKey ? row[csvKey] : undefined];
}),
),
domain: linkUrl.split("/")[0],
// domain.com/path/to/page => path/to/page
key: linkUrl.split("/").slice(1).join("/") || "_root",
createdAt: mapping.createdAt
? parseDateTime(row[mapping.createdAt])
: undefined,
tags: mapping.tags
? [
...new Set(
row[mapping.tags]
?.split(",")
// Validate required fields
const linkValue = getValueByNormalizedKey(mapping.link);
if (!linkValue) {
return {
error: "Missing link value",
link: { domain: "unknown", key: "unknown" },
};
}

const linkUrl = getPrettyUrl(linkValue);
if (!linkUrl) {
return {
error: "Invalid link format",
link: { domain: "unknown", key: "unknown" },
};
}

const domain = linkUrl.split("/")[0];
const key = linkUrl.split("/").slice(1).join("/") || "_root";

try {
return {
success: true,
data: {
...Object.fromEntries(
Object.entries(mapping).map(([key, value]) => [
key,
getValueByNormalizedKey(value),
]),
),
domain,
key,
createdAt: mapping.createdAt
? parseDateTime(getValueByNormalizedKey(mapping.createdAt)) ||
undefined
: undefined,
tags: mapping.tags
? getValueByNormalizedKey(mapping.tags)
.split(",")
.map((tag) => tag.trim())
.filter(Boolean),
),
]
: undefined,
};
.filter(Boolean)
.map((tag) => normalizeString(tag))
: undefined,
},
};
} catch (error) {
return {
error: error.message || "Error processing row",
link: { domain, key },
};
}
};

let cursor = parseInt(
Expand Down Expand Up @@ -117,14 +163,19 @@ export async function POST(req: Request) {
return;
}

// Find links that already exist in the workspace (we check matching of *both* domain and key below)
// Find links that already exist in the workspace
const alreadyCreatedLinks = await prisma.link.findMany({
where: {
domain: {
in: domains.map((domain) => domain.slug),
},
key: {
in: data.map((row) => mapper(row).key),
in: data.map((row) => {
const result = mapper(row);
return "success" in result && result.success
? result.data.key
: result.link.key;
}),
},
},
select: {
Expand All @@ -133,24 +184,31 @@ export async function POST(req: Request) {
},
});

// Find which links still need to be created
// Fix the linksToCreate typing and filtering
const linksToCreate = data
.map((row) => mapper(row))
.filter(
(link) =>
(result): result is Extract<MapperResult, { success: true }> =>
"success" in result &&
result.success === true &&
!alreadyCreatedLinks.some(
(l) => l.domain === link.domain && l.key === link.key,
) && link.key !== "_root",
);

(l) =>
l.domain === result.data.domain &&
l.key === result.data.key,
) &&
result.data.key !== "_root",
)
.map((result) => result.data);

// Fix the selectedTags extraction
const selectedTags = [
...new Set(
linksToCreate
.map(({ tags }) => tags)
.map(({ tags }) => tags || [])
.flat()
.filter(Boolean),
.filter((tag): tag is string => Boolean(tag)),
),
] as string[];
];

// Find tags that need to be added to the workspace
const tagsNotInWorkspace = selectedTags.filter(
Expand Down Expand Up @@ -218,22 +276,27 @@ export async function POST(req: Request) {

addedDomains.push(...domainsNotInWorkspace);

// Process all links
// Fix the processedLinks typing
type ProcessedLink = {
error: string | null;
link: ProcessedLinkProps;
};

const processedLinks = await Promise.all(
linksToCreate.map(({ createdAt, tags, ...link }) =>
processLink({
payload: {
...createLinkBodySchema.parse({
...link,
tagNames: tags || undefined,
}),
// 'createdAt' is not a valid field in createLinkBodySchema – but is valid for CSV imports
createdAt: createdAt?.toISOString(),
},
workspace: workspace as WorkspaceProps,
userId,
bulk: true,
}),
linksToCreate.map(
({ createdAt, tags, ...link }) =>
processLink({
payload: {
...createLinkBodySchema.parse({
...link,
tagNames: tags || undefined,
}),
createdAt: createdAt?.toISOString(),
},
workspace,
userId,
bulk: true,
}) as Promise<ProcessedLink>,
),
);

Expand Down
18 changes: 12 additions & 6 deletions packages/utils/src/functions/normalize-string.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,31 @@
export const normalizeString = (key: string) => {
if (!key) return "";

const original = key;
const normalized = key
// Remove BOM and other special characters
.replace(/^\uFEFF/, "")
.replace(/^\uFFFE/, "")
.replace(/^\uEFBBBF/, "")
.replace(/^\u0000\uFEFF/, "")
.replace(/^\uFFFE0000/, "")
.replace(/^\u2028/, "")
.replace(/^\u2029/, "")
.trim();
// Remove any non-printable characters
.replace(/[\x00-\x1F\x7F-\x9F]/g, "")
// Normalize whitespace
.replace(/\s+/g, " ")
.trim()
// Optional: normalize case
.toLowerCase();

if (original !== normalized) {
// Optional: Add logging in development
if (process.env.NODE_ENV === "development" && original !== normalized) {
console.log(`Normalized key: "${original}" -> "${normalized}"`);
console.log(
"Original char codes:",
Array.from(original).map((c) => c.charCodeAt(0)),
);
console.log(
"Normalized char codes:",
Array.from(normalized).map((c) => c.charCodeAt(0)),
);
}

return normalized;
Expand Down

0 comments on commit 40a8574

Please sign in to comment.