Skip to content

Commit 990bf5c

Browse files
feat: liquid sampler command
1 parent 10eef46 commit 990bf5c

3 files changed

Lines changed: 309 additions & 0 deletions

File tree

bin/cli.js

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ const toolkit = require("../index");
44
const liquidTestGenerator = require("../lib/liquidTestGenerator");
55
const liquidTestRunner = require("../lib/liquidTestRunner");
66
const { ExportFileInstanceGenerator } = require("../lib/exportFileInstanceGenerator");
7+
const { LiquidSamplerRunner } = require("../lib/liquidSamplerRunner");
78
const stats = require("../lib/cli/stats");
89
const { Command, Option } = require("commander");
910
const pkg = require("../package.json");
@@ -487,6 +488,41 @@ program
487488
}
488489
});
489490

491+
// Run Liquid Sampler
492+
program
493+
.command("run-sampler")
494+
.description("Run Liquid Sampler for partner templates (reconciliation texts, account detail templates, and/or shared parts)")
495+
.requiredOption("-p, --partner <partner-id>", "Specify the partner to be used")
496+
.option("-h, --handle <handles...>", "Specify reconciliation text handle(s) - can specify multiple")
497+
.option("-at, --account-template <names...>", "Specify account detail template name(s) - can specify multiple")
498+
.option("-s, --shared-part <names...>", "Specify shared part name(s) - can specify multiple")
499+
.option("-i, --id <sampler-id>", "Specify an existing sampler ID to fetch results for (optional)")
500+
.action(async (options) => {
501+
// If an existing sampler ID is provided, fetch and display results
502+
if (options.id) {
503+
await new LiquidSamplerRunner(options.partner).checkStatus(options.id);
504+
return;
505+
}
506+
507+
// Validate: at least one template specified
508+
const handles = options.handle || [];
509+
const accountTemplates = options.accountTemplate || [];
510+
const sharedParts = options.sharedPart || [];
511+
512+
if (handles.length === 0 && accountTemplates.length === 0 && sharedParts.length === 0) {
513+
consola.error("You need to specify at least one template using -h, -at, or -s");
514+
process.exit(1);
515+
}
516+
517+
const templateHandles = {
518+
reconciliationTexts: handles,
519+
accountTemplates: accountTemplates,
520+
sharedParts: sharedParts,
521+
};
522+
523+
await new LiquidSamplerRunner(options.partner).run(templateHandles);
524+
});
525+
490526
// Create Liquid Test
491527
program
492528
.command("create-test")

lib/api/sfApi.js

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -665,6 +665,30 @@ async function getExportFileInstance(firmId, companyId, periodId, exportFileInst
665665
}
666666
}
667667

668+
async function createSamplerRun(partnerId, attributes) {
669+
const instance = AxiosFactory.createInstance("partner", partnerId);
670+
try {
671+
const response = await instance.post("liquid_sampler/run", attributes);
672+
apiUtils.responseSuccessHandler(response);
673+
return response;
674+
} catch (error) {
675+
const response = await apiUtils.responseErrorHandler(error);
676+
return response;
677+
}
678+
}
679+
680+
async function readSamplerRun(partnerId, samplerId) {
681+
const instance = AxiosFactory.createInstance("partner", partnerId);
682+
try {
683+
const response = await instance.get(`liquid_sampler/${samplerId}`);
684+
apiUtils.responseSuccessHandler(response);
685+
return response;
686+
} catch (error) {
687+
const response = await apiUtils.responseErrorHandler(error);
688+
return response;
689+
}
690+
}
691+
668692
module.exports = {
669693
authorizeFirm,
670694
refreshFirmTokens,
@@ -716,4 +740,6 @@ module.exports = {
716740
getFirmDetails,
717741
createExportFileInstance,
718742
getExportFileInstance,
743+
createSamplerRun,
744+
readSamplerRun,
719745
};

lib/liquidSamplerRunner.js

Lines changed: 247 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,247 @@
1+
const { UrlHandler } = require("./utils/urlHandler");
2+
const errorUtils = require("./utils/errorUtils");
3+
const { spinner } = require("./cli/spinner");
4+
const SF = require("./api/sfApi");
5+
const fsUtils = require("./utils/fsUtils");
6+
const { consola } = require("consola");
7+
8+
const { ReconciliationText } = require("./templates/reconciliationText");
9+
const { AccountTemplate } = require("./templates/accountTemplate");
10+
const { SharedPart } = require("./templates/sharedPart");
11+
12+
/**
13+
* Class to run liquid samplers for partner templates
14+
*/
15+
class LiquidSamplerRunner {
16+
constructor(partnerId) {
17+
this.partnerId = partnerId;
18+
}
19+
20+
/**
21+
* Run liquid sampler for partner templates
22+
* @param {Object} templateHandles - Object containing arrays of template identifiers
23+
* @param {Array<string>} templateHandles.reconciliationTexts - Array of reconciliation text handles
24+
* @param {Array<string>} templateHandles.accountTemplates - Array of account template names
25+
* @param {Array<string>} templateHandles.sharedParts - Array of shared part names
26+
* @returns {Promise<void>}
27+
*/
28+
async run(templateHandles = {}) {
29+
try {
30+
// Validate at least one template specified
31+
const { reconciliationTexts = [], accountTemplates = [], sharedParts = [] } = templateHandles;
32+
if (reconciliationTexts.length === 0 && accountTemplates.length === 0 && sharedParts.length === 0) {
33+
consola.error("You need to specify at least one template using -h, -at, or -s");
34+
process.exit(1);
35+
}
36+
37+
// Build payload
38+
const samplerParams = await this.#buildSamplerParams(templateHandles);
39+
40+
consola.info(`Starting sampler run with ${samplerParams.templates.length} template(s)...`);
41+
42+
// Start sampler run
43+
const samplerResponse = await SF.createSamplerRun(this.partnerId, samplerParams);
44+
const samplerId = samplerResponse.data.id || samplerResponse.data;
45+
46+
if (!samplerId) {
47+
consola.error("Failed to start sampler run - no ID returned");
48+
process.exit(1);
49+
}
50+
51+
consola.info(`Sampler run started with ID: ${samplerId}`);
52+
53+
// Poll for completion
54+
const samplerRun = await this.#fetchAndWaitSamplerResult(samplerId);
55+
56+
// Process results
57+
await this.#handleSamplerResponse(samplerRun);
58+
} catch (error) {
59+
errorUtils.errorHandler(error);
60+
}
61+
}
62+
63+
/**
64+
* Fetch the status of an existing sampler run
65+
* @param {string} samplerId - The sampler run ID
66+
* @returns {Promise<void>}
67+
*/
68+
async checkStatus(samplerId) {
69+
try {
70+
consola.info(`Fetching status for sampler run ID: ${samplerId}`);
71+
72+
const response = await SF.readSamplerRun(this.partnerId, samplerId);
73+
74+
await this.#handleSamplerResponse(response.data);
75+
} catch (error) {
76+
errorUtils.errorHandler(error);
77+
}
78+
}
79+
80+
/**
81+
* Build sampler parameters from local template files
82+
* @param {Object} templateHandles - Object containing arrays of template identifiers
83+
* @param {Array<string>} templateHandles.reconciliationTexts - Array of reconciliation text handles
84+
* @param {Array<string>} templateHandles.accountTemplates - Array of account template names
85+
* @param {Array<string>} templateHandles.sharedParts - Array of shared part names
86+
* @returns {Object} Sampler payload with templates array
87+
*/
88+
async #buildSamplerParams(templateHandles = {}) {
89+
const templates = [];
90+
const { reconciliationTexts = [], accountTemplates = [], sharedParts = [] } = templateHandles;
91+
92+
// Process reconciliation texts
93+
for (const handle of reconciliationTexts) {
94+
const templateType = "reconciliationText";
95+
const configPresent = fsUtils.configExists(templateType, handle);
96+
97+
if (!configPresent) {
98+
consola.error(`Config file for reconciliation text "${handle}" not found`);
99+
process.exit(1);
100+
}
101+
102+
const config = fsUtils.readConfig(templateType, handle);
103+
104+
// Validate partner_id exists in config
105+
if (!config.partner_id || !config.partner_id[this.partnerId]) {
106+
consola.error(`Template '${handle}' has no partner_id entry for partner ${this.partnerId}. Import the template to this partner first.`);
107+
process.exit(1);
108+
}
109+
110+
const templateId = config.partner_id[this.partnerId];
111+
const templateContent = ReconciliationText.read(handle);
112+
113+
templates.push({
114+
type: "Global::Partner::ReconciliationText",
115+
id: String(templateId),
116+
text: templateContent.text,
117+
text_parts: templateContent.text_parts,
118+
});
119+
}
120+
121+
// Process account templates
122+
for (const name of accountTemplates) {
123+
const templateType = "accountTemplate";
124+
const configPresent = fsUtils.configExists(templateType, name);
125+
126+
if (!configPresent) {
127+
consola.error(`Config file for account template "${name}" not found`);
128+
process.exit(1);
129+
}
130+
131+
const config = fsUtils.readConfig(templateType, name);
132+
133+
// Validate partner_id exists in config
134+
if (!config.partner_id || !config.partner_id[this.partnerId]) {
135+
consola.error(`Template '${name}' has no partner_id entry for partner ${this.partnerId}. Import the template to this partner first.`);
136+
process.exit(1);
137+
}
138+
139+
const templateId = config.partner_id[this.partnerId];
140+
const templateContent = AccountTemplate.read(name);
141+
142+
templates.push({
143+
type: "Global::Partner::AccountDetailTemplate",
144+
id: String(templateId),
145+
text: templateContent.text,
146+
text_parts: templateContent.text_parts,
147+
});
148+
}
149+
150+
// Process shared parts
151+
for (const name of sharedParts) {
152+
const templateType = "sharedPart";
153+
const configPresent = fsUtils.configExists(templateType, name);
154+
155+
if (!configPresent) {
156+
consola.error(`Config file for shared part "${name}" not found`);
157+
process.exit(1);
158+
}
159+
160+
const config = fsUtils.readConfig(templateType, name);
161+
162+
// Validate partner_id exists in config
163+
if (!config.partner_id || !config.partner_id[this.partnerId]) {
164+
consola.error(`Shared part '${name}' has no partner_id entry for partner ${this.partnerId}. Import the shared part to this partner first.`);
165+
process.exit(1);
166+
}
167+
168+
const templateId = config.partner_id[this.partnerId];
169+
const templateContent = await SharedPart.read(name);
170+
171+
templates.push({
172+
type: "Global::Partner::SharedPart",
173+
id: String(templateId),
174+
text: templateContent.text,
175+
});
176+
}
177+
178+
return { templates };
179+
}
180+
181+
/**
182+
* Poll for sampler run completion
183+
* @param {number} partnerId - The partner ID
184+
* @param {string} samplerId - The sampler run ID
185+
* @returns {Promise<Object>} The completed sampler run
186+
*/
187+
async #fetchAndWaitSamplerResult(samplerId) {
188+
let samplerRun = { status: "pending" };
189+
const pollingDelay = 10000; // 10 seconds
190+
const waitingLimit = 2000000; // 2000 seconds
191+
192+
spinner.spin("Running sampler...");
193+
let waitingTime = 0;
194+
195+
while (samplerRun.status === "pending" || samplerRun.status === "running") {
196+
await new Promise((resolve) => setTimeout(resolve, pollingDelay));
197+
198+
const response = await SF.readSamplerRun(this.partnerId, samplerId);
199+
samplerRun = response.data;
200+
201+
waitingTime += pollingDelay;
202+
// pollingDelay *= 1.05;
203+
204+
if (waitingTime >= waitingLimit) {
205+
spinner.stop();
206+
consola.error("Timeout. Try to run your sampler again");
207+
process.exit(1);
208+
}
209+
}
210+
211+
spinner.stop();
212+
return samplerRun;
213+
}
214+
215+
/**
216+
* Process and display sampler run results
217+
* @param {Object} response - The sampler run response
218+
*/
219+
async #handleSamplerResponse(response) {
220+
if (!response || !response.status) {
221+
consola.error("Invalid sampler response");
222+
process.exit(1);
223+
}
224+
225+
switch (response.status) {
226+
case "failed":
227+
consola.error(`Sampler run failed: ${response.error_message || "Unknown error"}`);
228+
break;
229+
230+
case "completed":
231+
consola.success("Sampler run completed successfully");
232+
233+
if (response && response.result_url) {
234+
await new UrlHandler(response.content_url).openFile();
235+
} else {
236+
consola.warn("No URL returned");
237+
}
238+
break;
239+
240+
default:
241+
consola.error(`Unexpected sampler status: ${response.status}`);
242+
process.exit(1);
243+
}
244+
}
245+
}
246+
247+
module.exports = { LiquidSamplerRunner };

0 commit comments

Comments
 (0)