Skip to content
Open
Show file tree
Hide file tree
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
2 changes: 2 additions & 0 deletions shared/src/industry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export interface Industry {
readonly canHavePermanentLocation: boolean;
readonly additionalSearchTerms?: string;
readonly defaultSectorId?: string;
readonly webflowId?: string;
readonly roadmapSteps: AddOn[];
readonly nonEssentialQuestionsIds: string[];
readonly modifications?: TaskModification[];
Expand Down Expand Up @@ -62,6 +63,7 @@ export const LookupIndustryById = (id: string | undefined): Industry => {
roadmapSteps: [],
nonEssentialQuestionsIds: [],
naicsCodes: "",
webflowId: "",
isEnabled: false,
industryOnboardingQuestions: {
isProvidesStaffingServicesApplicable: undefined,
Expand Down
1 change: 1 addition & 0 deletions web/.dependency-cruiser.js
Original file line number Diff line number Diff line change
Expand Up @@ -379,6 +379,7 @@ module.exports = {
"(^|/)tsconfig\\.json$", // TypeScript config
"(^|/)(babel|webpack)\\.config\\.(js|cjs|mjs|ts|json)$", // other configs
"src/pages/healthz.tsx", // healthcheck page
"src/scripts/webflow/.*\\.ts$", // webflow scripts have both .mjs and .ts versions
],
},
to: {},
Expand Down
1 change: 1 addition & 0 deletions web/jest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export default {
testPathIgnorePatterns: ["<rootDir>/.next/", "<rootDir>/node_modules/", "<rootDir>/cypress/"],
rootDir: "./",
moduleDirectories: ["node_modules", "<rootDir>"],
moduleFileExtensions: ["ts", "tsx", "js", "jsx", "mjs", "json", "node"],
moduleNameMapper: {
"\\.(scss|sass|css)$": "identity-obj-proxy",
"@/components/(.*)": "<rootDir>/src/components/$1",
Expand Down
194 changes: 194 additions & 0 deletions web/src/scripts/fundingExport.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
import fs from "fs";
import matter from "gray-matter";
import { exportFundings, loadAllFundings, Funding } from "./fundingExport";

jest.mock("fs");
jest.mock("gray-matter");

const mockFs = fs as jest.Mocked<typeof fs>;
const mockMatter = matter as jest.MockedFunction<typeof matter>;

const generateMockFundingExportData = (overrides?: Partial<Omit<Funding, "contentMd">>): Omit<Funding, "contentMd"> => ({
id: "test-id",
name: "Test Funding",
filename: "funding1",
urlSlug: "test-funding",
callToActionLink: "https://example.com",
callToActionText: "Apply Now",
fundingType: "Grant",
programPurpose: "Business Support",
agency: ["Test Agency"],
agencyContact: "contact@test.com",
publishStageArchive: "",
openDate: "2024-01-01",
dueDate: "2024-12-31",
status: "Open",
programFrequency: "Annual",
businessStage: "Early Stage",
employeesRequired: "0",
homeBased: "Yes",
certifications: [],
preferenceForOpportunityZone: "",
county: "",
sector: [],
...overrides,
});

describe("fundingExport", () => {
beforeEach(() => {
jest.clearAllMocks();
});

describe("loadAllFundings", () => {
it("loads all funding files from the directory", () => {
const mockFileNames = ["funding1.md", "funding2.md"];
const mockFundingData = generateMockFundingExportData();

(mockFs.readdirSync as jest.Mock).mockReturnValue(mockFileNames);
mockFs.readFileSync.mockReturnValue("mock file content");
mockMatter.mockReturnValue({
data: mockFundingData,
content: "Test content",
excerpt: "",
matter: "",
stringify: jest.fn(),
orig: Buffer.from(""),
language: "",
});

const fundings = loadAllFundings();

expect(fundings).toHaveLength(2);
expect(mockFs.readdirSync).toHaveBeenCalledTimes(1);
expect(mockFs.readFileSync).toHaveBeenCalledTimes(2);
});

it("converts funding markdown with escaped quotes", () => {
const mockFileNames = ["funding1.md"];
const mockFundingData = generateMockFundingExportData({
id: "test-id",
name: 'Test "Funding"',
filename: "funding1",
});

(mockFs.readdirSync as jest.Mock).mockReturnValue(mockFileNames);
mockFs.readFileSync.mockReturnValue("mock file content");
mockMatter.mockReturnValue({
data: mockFundingData,
content: 'Content with "quotes"',
excerpt: "",
matter: "",
stringify: jest.fn(),
orig: Buffer.from(""),
language: "",
});

const fundings = loadAllFundings();

expect(fundings[0].contentMd).toBe('Content with ""quotes""');
expect(fundings[0].filename).toBe("funding1");
});
});

describe("exportFundings", () => {
it("exports fundings to CSV file", () => {
const mockFileNames = ["funding1.md"];
const mockFundingData = generateMockFundingExportData({
id: "test-id",
name: "Test Funding",
filename: "funding1",
urlSlug: "test-funding",
callToActionLink: "https://example.com",
callToActionText: "Apply Now",
fundingType: "Grant",
programPurpose: "Business Support",
agency: ["Test Agency"],
agencyContact: "contact@test.com",
publishStageArchive: "",
openDate: "2024-01-01",
dueDate: "2024-12-31",
status: "Open",
programFrequency: "Annual",
businessStage: "Early Stage",
employeesRequired: "0",
homeBased: "Yes",
certifications: [],
preferenceForOpportunityZone: "",
county: "",
sector: [],
});

(mockFs.readdirSync as jest.Mock).mockReturnValue(mockFileNames);
mockFs.readFileSync.mockReturnValue("test content");
mockMatter.mockReturnValue({
data: mockFundingData,
content: "Test content",
excerpt: "",
matter: "",
stringify: jest.fn(),
orig: Buffer.from(""),
language: "",
});
mockFs.writeFileSync.mockImplementation(() => {
// Mock implementation
});

exportFundings();

expect(mockFs.writeFileSync).toHaveBeenCalledWith("fundings.csv", expect.stringContaining("id,name,filename"));
expect(mockFs.writeFileSync).toHaveBeenCalledWith(
"fundings.csv",
expect.stringContaining('"test-id","Test Funding"')
);
});

it("trims content markdown in CSV output", () => {
const mockFileNames = ["funding1.md"];
const mockFundingData = generateMockFundingExportData({
id: "test-id",
name: "Test Funding",
filename: "funding1",
urlSlug: "test-funding",
callToActionLink: "",
callToActionText: "",
fundingType: "",
programPurpose: "",
agency: [],
agencyContact: "",
publishStageArchive: "",
openDate: "",
dueDate: "",
status: "",
programFrequency: "",
businessStage: "",
employeesRequired: "",
homeBased: "",
certifications: [],
preferenceForOpportunityZone: "",
county: "",
sector: [],
});

(mockFs.readdirSync as jest.Mock).mockReturnValue(mockFileNames);
mockFs.readFileSync.mockReturnValue("test content");
mockMatter.mockReturnValue({
data: mockFundingData,
content: " Test content with whitespace ",
excerpt: "",
matter: "",
stringify: jest.fn(),
orig: Buffer.from(""),
language: "",
});
mockFs.writeFileSync.mockImplementation(() => {
// Mock implementation
});

exportFundings();

const csvCall = mockFs.writeFileSync.mock.calls[0][1] as string;
expect(csvCall).toContain('Test content with whitespace"');
expect(csvCall).not.toContain(' Test content with whitespace "');
});
});
});
87 changes: 87 additions & 0 deletions web/src/scripts/fundingExport.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import fs from "fs";
import matter from "gray-matter";
import path from "path";

const fundingDir = path.resolve(`${__dirname}/../../../content/src/fundings`);

export interface Funding {
id: string;
name: string;
filename: string;
urlSlug: string;
callToActionLink: string;
callToActionText: string;
fundingType: string;
programPurpose: string;
agency: string[];
agencyContact: string;
publishStageArchive: string;
openDate: string;
dueDate: string;
status: string;
programFrequency: string;
businessStage: string;
employeesRequired: string;
homeBased: string;
certifications?: string[];
preferenceForOpportunityZone: string;
county: string;
sector: string[];
contentMd: string;
[key: string]: unknown;
}

const convertFundingMd = (oppMdContents: string, filename: string): Funding => {
const matterResult = matter(oppMdContents);
const oppGrayMatter = matterResult.data;

return {
contentMd: matterResult.content.replaceAll('"', '""'),
filename,
...oppGrayMatter,
} as Funding;
};

export const loadAllFundings = (): Funding[] => {
const fileNames = fs.readdirSync(fundingDir);
return fileNames.map((fileName) => {
return loadFundingByFileName(fileName);
});
};

const loadFundingByFileName = (fileName: string): Funding => {
const fullPath = path.join(fundingDir, `${fileName}`);
const fileContents = fs.readFileSync(fullPath, "utf8");

const fileNameWithoutMd = fileName.split(".md")[0];
return convertFundingMd(fileContents, fileNameWithoutMd);
};

export const exportFundings = (): void => {
const fundings = loadAllFundings();
let csvContent = `id,name,filename,urlSlug,callToActionLink,callToActionText,fundingType,programPurpose,agency,agencyContact,publishStageArchive,openDate,dueDate,status,programFrequency,businessStage,employeesRequired,homeBased,certifications,preferenceForOpportunityZone,county,sector,contentMd\n`;

for (const funding of fundings) {
csvContent += `"${funding.id}","${funding.name}","${funding.filename}","${funding.urlSlug}","${
funding.callToActionLink
}","${funding.callToActionText}","${funding.fundingType}","${funding.programPurpose}","${
funding.agency
}","${funding.agencyContact}","${funding.publishStageArchive}","${funding.openDate}","${
funding.dueDate
}","${funding.status}","${funding.programFrequency}","${funding.businessStage}","${
funding.employeesRequired
}","${funding.homeBased}","${funding.certifications}","${funding.preferenceForOpportunityZone}","${
funding.county
}","${funding.sector}","${funding.contentMd.trim()}"\n`;
}
fs.writeFileSync("fundings.csv", csvContent);
};

if (!process.argv.some((i) => i.includes("fundingExport")) || process.env.NODE_ENV === "test") {
// Skip execution
} else if (process.argv.some((i) => i.includes("--export"))) {
exportFundings();
} else {
console.log("Expected at least one argument! Use one of the following: ");
console.log("--export = exports fundings as csv");
}
Loading