Skip to content

Commit 4c55b18

Browse files
committed
refactor: sidecar data into separate function
1 parent 4c8b4e7 commit 4c55b18

File tree

6 files changed

+201
-49
lines changed

6 files changed

+201
-49
lines changed

packages/email/docsoc-mail-merge/src/cli.ts

Lines changed: 11 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@ import { TemplatePreviews } from "./engines/types";
1111
import { getFileNameSchemeInteractively } from "./interactivity/getFileNameSchemeInteractively";
1212
import getRunNameInteractively from "./interactivity/getRunNameInteractively";
1313
import mapCSVFieldsInteractive from "./interactivity/mapCSVFieldsInteractive";
14-
import { getRecordPreviewPrefixForIndividual, getRecordPreviewPrefixForMetadata } from "./sideCardData";
15-
import { SidecardData } from "./sideCardData/types";
14+
import { getRecordPreviewPrefixForIndividual, getRecordPreviewPrefixForMetadata, writeMetadata } from "./sideCarData";
15+
import { SidecardData } from "./sideCarData/types";
1616
import { stopIfCriticalFsError } from "./util/files";
1717
import createLogger from "./util/logger";
1818
import { CliOptions, CSVRecord } from "./util/types";
@@ -106,28 +106,18 @@ async function main(opts: CliOptions) {
106106
const operations = previews.map(async (preview) => {
107107
const fileName = getRecordPreviewPrefixForIndividual(record, fileNamer, opts.templateEngine, preview);
108108
logger.debug(`Writing ${fileName}__${opts.templateEngine}__${preview.name}`);
109-
await fs.writeFile(
110-
join(previewsRoot, `${fileName}__${opts.templateEngine}__${preview.name}`),
111-
preview.content,
109+
await stopIfCriticalFsError(
110+
fs.writeFile(
111+
join(previewsRoot, `${fileName}__${opts.templateEngine}__${preview.name}`),
112+
preview.content,
113+
),
112114
);
113115
});
114116

115-
const sidecar: SidecardData = {
116-
record: record,
117-
engine: opts.templateEngine,
118-
engineOptions: opts.templateOptions,
119-
files: previews.map((preview) => ({
120-
filename: getRecordPreviewPrefixForIndividual(record, fileNamer, opts.templateEngine, preview),
121-
engineData: {
122-
...preview,
123-
content: undefined,
124-
},
125-
})),
126-
};
127-
128-
const metadataFile = getRecordPreviewPrefixForMetadata(record, fileNamer);
129-
logger.debug(`Writing metadata for ${fileNamer(record)} to ${metadataFile}`);
130-
operations.push(fs.writeFile(join(previewsRoot, metadataFile), JSON.stringify(sidecar, null, 4)));
117+
// Add metadata write operation
118+
operations.push(
119+
writeMetadata(record, opts.templateEngine, opts.templateOptions, previews, fileNamer, previewsRoot),
120+
);
131121

132122
return operations;
133123
}),

packages/email/docsoc-mail-merge/src/engines/index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,9 @@
55
import nunjucksEngine from "./nunjucks-md";
66
import { TemplateEngineConstructor } from "./types";
77

8-
export type TEMPLATE_ENGINE = "nunjucks";
8+
export type TEMPLATE_ENGINES = "nunjucks";
99

1010
/** Map of engine names (provided on the CLI) to constructors for those engines */
11-
export const ENGINES_MAP: Record<TEMPLATE_ENGINE, TemplateEngineConstructor> = {
11+
export const ENGINES_MAP: Record<TEMPLATE_ENGINES, TemplateEngineConstructor> = {
1212
nunjucks: nunjucksEngine,
1313
};
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
/**
2+
* Handle "sidecar" data - these are JSON files that sit next to rendered template previews,
3+
* containing metadata about the preview, so that we can re-render them later.
4+
*
5+
* @packageDocumentation
6+
*/
7+
import fs from "fs/promises";
8+
import { join } from "path";
9+
10+
import { TEMPLATE_ENGINES } from "../engines";
11+
import { TemplatePreview, TemplatePreviews } from "../engines/types";
12+
import { stopIfCriticalFsError } from "../util/files";
13+
import createLogger from "../util/logger";
14+
import { CliOptions, CSVRecord } from "../util/types";
15+
import { SidecardData } from "./types";
16+
17+
const PARTS_SEPARATOR = "__";
18+
const logger = createLogger("docsoc.sidecar");
19+
20+
/**
21+
* Generate the first part of a filename for a record's previews - this part
22+
* identifies the record itself via the fileNamer function.
23+
* @param record The record to generate a prefix for
24+
* @param fileNamer A function that generates a filename for a record
25+
* @returns
26+
*/
27+
export const getRecordPreviewPrefix = (record: CSVRecord, fileNamer: (record: CSVRecord) => string) =>
28+
`${fileNamer(record)}`;
29+
30+
/**
31+
* Generate predicable prefixes for preview names (including record specific part
32+
*
33+
* @example
34+
* const record = { id: "1", name: "Test Record" };
35+
* const fileNamer = (record: CSVRecord) => `file_${record["id"]}`;
36+
* getRecordPreviewPrefixForIndividual(record, fileNamer, "nunjucks", { name: "preview1.txt", content: "content1", metadata: { key: "value" } })
37+
* // => "file_1__nunjucks__preview1.txt"
38+
*/
39+
export const getRecordPreviewPrefixForIndividual = (
40+
record: CSVRecord,
41+
fileNamer: (record: CSVRecord) => string,
42+
templateEngine: string,
43+
preview: TemplatePreview,
44+
) => [getRecordPreviewPrefix(record, fileNamer), templateEngine, preview.name].join(PARTS_SEPARATOR);
45+
46+
/**
47+
* Generate the filename for the metadata file for a record
48+
* @param record The record to generate a metadata filename for
49+
* @param fileNamer A function that generates a filename for a record
50+
* @example
51+
* const record = { id: "1", name: "Test Record" };
52+
* const fileNamer = (record: CSVRecord) => `file_${record["id"]}`;
53+
* getRecordPreviewPrefixForMetadata(record, fileNamer)
54+
* // => "file_1-metadata.json"
55+
*/
56+
export const getRecordPreviewPrefixForMetadata = (record: CSVRecord, fileNamer: (record: CSVRecord) => string) =>
57+
`${getRecordPreviewPrefix(record, fileNamer)}-metadata.json`;
58+
59+
/**
60+
* Write the metadata for a record & its associated previews to a JSON file.
61+
* @param record The record to write metadata for
62+
* @param templateEngine The engine used to render the previews
63+
* @param templateOptions The options given to the engine
64+
* @param previews The previews rendered for the record
65+
* @param fileNamer A function that generates a filename for a record
66+
* @param previewsRoot The root directory to write the metadata to
67+
* @returns
68+
*/
69+
export async function writeMetadata(
70+
record: CSVRecord,
71+
templateEngine: TEMPLATE_ENGINES,
72+
templateOptions: CliOptions["templateOptions"],
73+
previews: TemplatePreviews,
74+
fileNamer: (record: CSVRecord) => string,
75+
previewsRoot: string,
76+
): Promise<void> {
77+
const sidecar: SidecardData = {
78+
record: record,
79+
engine: templateEngine,
80+
engineOptions: templateOptions,
81+
files: previews.map((preview) => ({
82+
filename: getRecordPreviewPrefixForIndividual(record, fileNamer, templateEngine, preview),
83+
engineData: {
84+
...preview,
85+
content: undefined,
86+
},
87+
})),
88+
};
89+
90+
const metadataFile = getRecordPreviewPrefixForMetadata(record, fileNamer);
91+
logger.debug(`Writing metadata for ${fileNamer(record)} to ${metadataFile}`);
92+
await stopIfCriticalFsError(fs.writeFile(join(previewsRoot, metadataFile), JSON.stringify(sidecar, null, 4)));
93+
return Promise.resolve();
94+
}

packages/email/docsoc-mail-merge/src/sideCardData/index.ts

Lines changed: 0 additions & 26 deletions
This file was deleted.
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
// index.test.ts
2+
import fs from "fs/promises";
3+
import { join } from "path";
4+
5+
import { TEMPLATE_ENGINES } from "../../src/engines";
6+
import { TemplatePreviews } from "../../src/engines/types";
7+
import {
8+
getRecordPreviewPrefix,
9+
getRecordPreviewPrefixForIndividual,
10+
getRecordPreviewPrefixForMetadata,
11+
writeMetadata,
12+
} from "../../src/sideCarData/index";
13+
import { SidecardData } from "../../src/sideCarData/types";
14+
import { stopIfCriticalFsError } from "../../src/util/files";
15+
import { CliOptions, CSVRecord } from "../../src/util/types";
16+
17+
jest.mock("fs/promises");
18+
jest.mock("../../src/util/logger", () => {
19+
const logger = {
20+
info: jest.fn(),
21+
debug: jest.fn(),
22+
warn: jest.fn(),
23+
error: jest.fn(),
24+
};
25+
return () => logger;
26+
});
27+
jest.mock("../../src/util/files");
28+
29+
describe("Sidecar Data Functions", () => {
30+
const mockRecord: CSVRecord = { id: "1", name: "Test Record" };
31+
const mockFileNamer = (record: CSVRecord) => `file_${record["id"]}`;
32+
const mockTemplateEngine = "nunjucks" as TEMPLATE_ENGINES;
33+
const mockTemplateOptions: CliOptions["templateOptions"] = {};
34+
const mockPreviews: TemplatePreviews = [{ name: "preview1", content: "content1", metadata: { key: "value" } }];
35+
const mockPreviewsRoot = "/mock/previews/root";
36+
37+
beforeEach(() => {
38+
jest.clearAllMocks();
39+
});
40+
41+
test("getRecordPreviewPrefix should return correct prefix", () => {
42+
const result = getRecordPreviewPrefix(mockRecord, mockFileNamer);
43+
expect(result).toBe("file_1");
44+
});
45+
46+
test("getRecordPreviewPrefixForIndividual should return correct prefix", () => {
47+
const result = getRecordPreviewPrefixForIndividual(
48+
mockRecord,
49+
mockFileNamer,
50+
mockTemplateEngine,
51+
mockPreviews[0],
52+
);
53+
expect(result).toBe(`file_1__${mockTemplateEngine}__preview1`);
54+
});
55+
56+
test("getRecordPreviewPrefixForMetadata should return correct metadata filename", () => {
57+
const result = getRecordPreviewPrefixForMetadata(mockRecord, mockFileNamer);
58+
expect(result).toBe("file_1-metadata.json");
59+
});
60+
61+
test("writeMetadata should write metadata to a JSON file", async () => {
62+
const mockSidecar: SidecardData = {
63+
record: mockRecord,
64+
engine: mockTemplateEngine,
65+
engineOptions: mockTemplateOptions,
66+
files: [
67+
{
68+
filename: `file_1__${mockTemplateEngine}__preview1`,
69+
engineData: {
70+
name: "preview1",
71+
content: undefined,
72+
metadata: { key: "value" },
73+
},
74+
},
75+
],
76+
};
77+
78+
(stopIfCriticalFsError as jest.Mock).mockImplementation((promise) => promise);
79+
80+
await writeMetadata(
81+
mockRecord,
82+
mockTemplateEngine,
83+
mockTemplateOptions,
84+
mockPreviews,
85+
mockFileNamer,
86+
mockPreviewsRoot,
87+
);
88+
89+
expect(fs.writeFile).toHaveBeenCalledWith(
90+
join(mockPreviewsRoot, "file_1-metadata.json"),
91+
JSON.stringify(mockSidecar, null, 4),
92+
);
93+
});
94+
});

0 commit comments

Comments
 (0)