Conversation
release: promote main
…es--release chore: release release
release: promote main
release: promote main
release: promote main
release: promote main
release: promote main
release: promote main
release: promote main
release: v1.8.1
release: v1.8.2
release: v1.9.0
release: v1.10.0
release: v1.11.0
release: v1.12.0
release: v1.13.0
release: v1.14.0
release: v1.15.0
release: v1.15.1
release: v1.15.2
release: v1.15.3
Add a CSV import flow for Companies: upload/parse a .csv file, map/preview columns, and create Company records via a new import wizard page and API endpoint. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Iterate with rows.entries() instead of manual index access so TypeScript doesn't flag row as possibly undefined under noUncheckedIndexedAccess. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Guard on the destructured first row directly instead of checking records.length beforehand, which TypeScript couldn't narrow through. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
@R0CH3X is attempting to deploy a commit to the Comp AI - PoC Team on Vercel. A member of the Team first needs to authorize it. |
|
Retargeted this onto
Nothing is wrong with your branch. If the diff now shows commits that are already on git fetch origin main
git rebase origin/main
git push --force-with-lease |
There was a problem hiding this comment.
10 issues found across 9 files
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="apps/app/app/(app)/[slug]/companies/import/csv-parse.ts">
<violation number="1" location="apps/app/app/(app)/[slug]/companies/import/csv-parse.ts:7">
P2: When an empty or blank-line-only file is uploaded, `parseCsv` reports one empty header instead of no headers, so the wizard enters column mapping rather than showing the invalid-file error. Treat a sole empty header as no headers.</violation>
</file>
<file name="apps/app/app/(app)/[slug]/companies/import/import-wizard.tsx">
<violation number="1" location="apps/app/app/(app)/[slug]/companies/import/import-wizard.tsx:120">
P2: When a CSV contains duplicate header labels, this header-keyed mapping drops later columns and `indexOf` always reads the first one. Reject duplicate headers or key mappings by column index.</violation>
<violation number="2" location="apps/app/app/(app)/[slug]/companies/import/import-wizard.tsx:161">
P1: When two CSV columns map to the same CRM field, this loop overwrites the earlier value with the later one. Reject duplicate destinations before building rows.</violation>
<violation number="3" location="apps/app/app/(app)/[slug]/companies/import/import-wizard.tsx:174">
P2: For files with more than 5,000 data rows, this sends an array rejected by `companyImportInput.max(5000)`, so the wizard only shows an error. Validate the limit before mutating or chunk the import.</violation>
<violation number="4" location="apps/app/app/(app)/[slug]/companies/import/import-wizard.tsx:330">
P2: For a header-only CSV, this button remains enabled when Name is mapped and submits zero rows, which the API rejects. Disable Import when `parsed.rows.length === 0` and show an empty-data message.</violation>
</file>
<file name="apps/api/src/companies/companies.service.ts">
<violation number="1" location="apps/api/src/companies/companies.service.ts:731">
P3: Skipped-row numbers are off by one because `input.rows` excludes the CSV header. Add the header offset when assigning `s.row` so users can locate skipped records in the original file.</violation>
<violation number="2" location="apps/api/src/companies/companies.service.ts:741">
P2: When a CSV contains a website or domain, the importer leaves `Company.domain` null and stores the value only in `website`. This makes imported companies unavailable to the domain-based research and enrichment flows; normalize the imported URL and persist the resulting domain as well.</violation>
<violation number="3" location="apps/api/src/companies/companies.service.ts:749">
P1: Imported companies never emit `company.created`, so live agents configured for that CRM event do not run for CSV-created records. Create the event tasks for each imported company as part of the bulk transaction, or otherwise preserve the standard company-creation event lifecycle.</violation>
<violation number="4" location="apps/api/src/companies/companies.service.ts:749">
P2: CSV-created companies skip agent-filled custom-field backfill, leaving those fields blank even though they are new records. Queue backfill for the inserted company IDs after the bulk insert, using a bulk-safe path if necessary.</violation>
<violation number="5" location="apps/api/src/companies/companies.service.ts:749">
P2: The bulk import bypasses company deduplication because it never derives `domain` from `website` or checks existing records before `createMany`. Re-importing the same CSV therefore silently creates duplicate companies; normalize the website/domain and apply the same conflict or deduplication behavior as `create()`.</violation>
</file>
Tip: instead of fixing issues one by one fix them all with cubic
Re-trigger cubic
| for (const [header, field] of Object.entries(mapping)) { | ||
| if (field === "_skip") continue; | ||
| const idx = parsed.headers.indexOf(header); | ||
| obj[field] = row[idx] ?? ""; |
There was a problem hiding this comment.
P1: When two CSV columns map to the same CRM field, this loop overwrites the earlier value with the later one. Reject duplicate destinations before building rows.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/app/app/(app)/[slug]/companies/import/import-wizard.tsx, line 161:
<comment>When two CSV columns map to the same CRM field, this loop overwrites the earlier value with the later one. Reject duplicate destinations before building rows.</comment>
<file context>
@@ -0,0 +1,463 @@
+ for (const [header, field] of Object.entries(mapping)) {
+ if (field === "_skip") continue;
+ const idx = parsed.headers.indexOf(header);
+ obj[field] = row[idx] ?? "";
+ }
+ return obj as {
</file context>
| } | ||
|
|
||
| if (toCreate.length > 0) { | ||
| await this.db.company.createMany({ data: toCreate }); |
There was a problem hiding this comment.
P1: Imported companies never emit company.created, so live agents configured for that CRM event do not run for CSV-created records. Create the event tasks for each imported company as part of the bulk transaction, or otherwise preserve the standard company-creation event lifecycle.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/api/src/companies/companies.service.ts, line 749:
<comment>Imported companies never emit `company.created`, so live agents configured for that CRM event do not run for CSV-created records. Create the event tasks for each imported company as part of the bulk transaction, or otherwise preserve the standard company-creation event lifecycle.</comment>
<file context>
@@ -720,6 +721,43 @@ export class CompaniesService {
+ }
+
+ if (toCreate.length > 0) {
+ await this.db.company.createMany({ data: toCreate });
+ }
+
</file context>
| const normalized = text.replace(/\r\n/g, "\n").replace(/\r/g, "\n"); | ||
| const records = parseRecords(normalized); | ||
| const [first, ...rest] = records; | ||
| if (!first) return { headers: [], rows: [] }; |
There was a problem hiding this comment.
P2: When an empty or blank-line-only file is uploaded, parseCsv reports one empty header instead of no headers, so the wizard enters column mapping rather than showing the invalid-file error. Treat a sole empty header as no headers.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/app/app/(app)/[slug]/companies/import/csv-parse.ts, line 7:
<comment>When an empty or blank-line-only file is uploaded, `parseCsv` reports one empty header instead of no headers, so the wizard enters column mapping rather than showing the invalid-file error. Treat a sole empty header as no headers.</comment>
<file context>
@@ -0,0 +1,74 @@
+ const normalized = text.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
+ const records = parseRecords(normalized);
+ const [first, ...rest] = records;
+ if (!first) return { headers: [], rows: [] };
+ return { headers: first, rows: rest.filter((r) => r.some(Boolean)) };
+}
</file context>
| toast.error("The CSV file has no headers."); | ||
| return; | ||
| } | ||
| const initial: Record<string, CrmField> = {}; |
There was a problem hiding this comment.
P2: When a CSV contains duplicate header labels, this header-keyed mapping drops later columns and indexOf always reads the first one. Reject duplicate headers or key mappings by column index.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/app/app/(app)/[slug]/companies/import/import-wizard.tsx, line 120:
<comment>When a CSV contains duplicate header labels, this header-keyed mapping drops later columns and `indexOf` always reads the first one. Reject duplicate headers or key mappings by column index.</comment>
<file context>
@@ -0,0 +1,463 @@
+ toast.error("The CSV file has no headers.");
+ return;
+ }
+ const initial: Record<string, CrmField> = {};
+ for (const h of result.headers) initial[h] = guessMapping(h);
+ setParsed(result);
</file context>
| description?: string; | ||
| }; | ||
| }); | ||
| importMutation.mutate({ rows }); |
There was a problem hiding this comment.
P2: For files with more than 5,000 data rows, this sends an array rejected by companyImportInput.max(5000), so the wizard only shows an error. Validate the limit before mutating or chunk the import.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/app/app/(app)/[slug]/companies/import/import-wizard.tsx, line 174:
<comment>For files with more than 5,000 data rows, this sends an array rejected by `companyImportInput.max(5000)`, so the wizard only shows an error. Validate the limit before mutating or chunk the import.</comment>
<file context>
@@ -0,0 +1,463 @@
+ description?: string;
+ };
+ });
+ importMutation.mutate({ rows });
+ }, [parsed, mapping, importMutation]);
+
</file context>
| <Button | ||
| onClick={confirm} | ||
| disabled={ | ||
| importMutation.isPending || !mappedFields.includes("name") |
There was a problem hiding this comment.
P2: For a header-only CSV, this button remains enabled when Name is mapped and submits zero rows, which the API rejects. Disable Import when parsed.rows.length === 0 and show an empty-data message.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/app/app/(app)/[slug]/companies/import/import-wizard.tsx, line 330:
<comment>For a header-only CSV, this button remains enabled when Name is mapped and submits zero rows, which the API rejects. Disable Import when `parsed.rows.length === 0` and show an empty-data message.</comment>
<file context>
@@ -0,0 +1,463 @@
+ <Button
+ onClick={confirm}
+ disabled={
+ importMutation.isPending || !mappedFields.includes("name")
+ }
+ >
</file context>
| } | ||
|
|
||
| if (toCreate.length > 0) { | ||
| await this.db.company.createMany({ data: toCreate }); |
There was a problem hiding this comment.
P2: CSV-created companies skip agent-filled custom-field backfill, leaving those fields blank even though they are new records. Queue backfill for the inserted company IDs after the bulk insert, using a bulk-safe path if necessary.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/api/src/companies/companies.service.ts, line 749:
<comment>CSV-created companies skip agent-filled custom-field backfill, leaving those fields blank even though they are new records. Queue backfill for the inserted company IDs after the bulk insert, using a bulk-safe path if necessary.</comment>
<file context>
@@ -720,6 +721,43 @@ export class CompaniesService {
+ }
+
+ if (toCreate.length > 0) {
+ await this.db.company.createMany({ data: toCreate });
+ }
+
</file context>
| stateCode: row.stateCode?.trim() || null, | ||
| industry: row.industry?.trim() || null, | ||
| subIndustry: row.subIndustry?.trim() || null, | ||
| website: row.website?.trim() || null, |
There was a problem hiding this comment.
P2: When a CSV contains a website or domain, the importer leaves Company.domain null and stores the value only in website. This makes imported companies unavailable to the domain-based research and enrichment flows; normalize the imported URL and persist the resulting domain as well.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/api/src/companies/companies.service.ts, line 741:
<comment>When a CSV contains a website or domain, the importer leaves `Company.domain` null and stores the value only in `website`. This makes imported companies unavailable to the domain-based research and enrichment flows; normalize the imported URL and persist the resulting domain as well.</comment>
<file context>
@@ -720,6 +721,43 @@ export class CompaniesService {
+ stateCode: row.stateCode?.trim() || null,
+ industry: row.industry?.trim() || null,
+ subIndustry: row.subIndustry?.trim() || null,
+ website: row.website?.trim() || null,
+ description: row.description?.trim() || null,
+ source: RecordSource.IMPORT,
</file context>
| } | ||
|
|
||
| if (toCreate.length > 0) { | ||
| await this.db.company.createMany({ data: toCreate }); |
There was a problem hiding this comment.
P2: The bulk import bypasses company deduplication because it never derives domain from website or checks existing records before createMany. Re-importing the same CSV therefore silently creates duplicate companies; normalize the website/domain and apply the same conflict or deduplication behavior as create().
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/api/src/companies/companies.service.ts, line 749:
<comment>The bulk import bypasses company deduplication because it never derives `domain` from `website` or checks existing records before `createMany`. Re-importing the same CSV therefore silently creates duplicate companies; normalize the website/domain and apply the same conflict or deduplication behavior as `create()`.</comment>
<file context>
@@ -720,6 +721,43 @@ export class CompaniesService {
+ }
+
+ if (toCreate.length > 0) {
+ await this.db.company.createMany({ data: toCreate });
+ }
+
</file context>
| for (const [i, row] of input.rows.entries()) { | ||
| const name = row.name.trim(); | ||
| if (!name) { | ||
| skips.push({ row: i + 1, reason: "No name" }); |
There was a problem hiding this comment.
P3: Skipped-row numbers are off by one because input.rows excludes the CSV header. Add the header offset when assigning s.row so users can locate skipped records in the original file.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/api/src/companies/companies.service.ts, line 731:
<comment>Skipped-row numbers are off by one because `input.rows` excludes the CSV header. Add the header offset when assigning `s.row` so users can locate skipped records in the original file.</comment>
<file context>
@@ -720,6 +721,43 @@ export class CompaniesService {
+ for (const [i, row] of input.rows.entries()) {
+ const name = row.name.trim();
+ if (!name) {
+ skips.push({ row: i + 1, reason: "No name" });
+ continue;
+ }
</file context>
| skips.push({ row: i + 1, reason: "No name" }); | |
| skips.push({ row: i + 2, reason: "No name" }); |
Summary
Adds a CSV import flow for Companies:
companies.importAPI endpoint that creates Company records in bulk, skipping rows without a nameNotes
bun run check-typespasses clean.trycompai/crm) with--no-verifyon the pre-push lint hook — that failure is a pre-existing, repo-wide CRLF/line-ending issue on Windows checkouts (verified it also fails on an untouched checkout ofrelease), unrelated to this change.🤖 Generated with Claude Code
Summary by cubic
Adds a CSV import flow for Companies. Users can now upload a
.csvfile, map columns to company fields, preview the first rows, and bulk create companies through a new import wizard andcompanies.importendpoint; rows without a name are skipped and reported.New Features
companies.importmutation accepts up to 5,000 rows and returns created/skipped counts with per-row skip reasons.IMPORTand enrichment statusSKIPPED.Written for commit 53f421a. Summary will update on new commits.