-
Notifications
You must be signed in to change notification settings - Fork 1
feat: liquid sampler command #236
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,259 @@ | ||||||||||||||||||||||||
| const { UrlHandler } = require("./utils/urlHandler"); | ||||||||||||||||||||||||
| const errorUtils = require("./utils/errorUtils"); | ||||||||||||||||||||||||
| const { spinner } = require("./cli/spinner"); | ||||||||||||||||||||||||
| const SF = require("./api/sfApi"); | ||||||||||||||||||||||||
| const fsUtils = require("./utils/fsUtils"); | ||||||||||||||||||||||||
| const { consola } = require("consola"); | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| const { ReconciliationText } = require("./templates/reconciliationText"); | ||||||||||||||||||||||||
| const { AccountTemplate } = require("./templates/accountTemplate"); | ||||||||||||||||||||||||
| const { SharedPart } = require("./templates/sharedPart"); | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||
| * Class to run liquid samplers for partner templates | ||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||
| class LiquidSamplerRunner { | ||||||||||||||||||||||||
| constructor(partnerId) { | ||||||||||||||||||||||||
| this.partnerId = partnerId; | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||
| * Run liquid sampler for partner templates | ||||||||||||||||||||||||
| * @param {Object} templateHandles - Object containing arrays of template identifiers | ||||||||||||||||||||||||
| * @param {Array<string>} templateHandles.reconciliationTexts - Array of reconciliation text handles | ||||||||||||||||||||||||
| * @param {Array<string>} templateHandles.accountTemplates - Array of account template names | ||||||||||||||||||||||||
| * @param {Array<string>} templateHandles.sharedParts - Array of shared part names | ||||||||||||||||||||||||
| * @param {Array<number>} firmIds - Array of firm IDs to use in the sampler | ||||||||||||||||||||||||
| * @returns {Promise<void>} | ||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||
| async run(templateHandles = {}, firmIds = []) { | ||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||
| // Validate at least one template specified | ||||||||||||||||||||||||
| const { reconciliationTexts = [], accountTemplates = [], sharedParts = [] } = templateHandles; | ||||||||||||||||||||||||
| if (reconciliationTexts.length === 0 && accountTemplates.length === 0 && sharedParts.length === 0) { | ||||||||||||||||||||||||
| consola.error("You need to specify at least one template using -h, -at, or -s"); | ||||||||||||||||||||||||
| process.exit(1); | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| // Build payload | ||||||||||||||||||||||||
| const samplerParams = await this.#buildSamplerParams(templateHandles, firmIds); | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| consola.info(`Starting sampler run with ${samplerParams.templates.length} template(s)...`); | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| // Start sampler run | ||||||||||||||||||||||||
| const samplerResponse = await SF.createSamplerRun(this.partnerId, samplerParams); | ||||||||||||||||||||||||
| const samplerId = samplerResponse.data.id || samplerResponse.data; | ||||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Suspicious fallback logic for samplerId extraction. The expression Recommended fixIf the API can return either - const samplerId = samplerResponse.data.id || samplerResponse.data;
+ const samplerId = typeof samplerResponse.data === 'object'
+ ? samplerResponse.data.id
+ : samplerResponse.data;Otherwise, if - const samplerId = samplerResponse.data.id || samplerResponse.data;
+ const samplerId = samplerResponse.data.id;
if (!samplerId) {📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| if (!samplerId) { | ||||||||||||||||||||||||
| consola.error("Failed to start sampler run - no ID returned"); | ||||||||||||||||||||||||
| process.exit(1); | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| consola.info(`Sampler run started with ID: ${samplerId}`); | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| // Poll for completion | ||||||||||||||||||||||||
| const samplerRun = await this.#fetchAndWaitSamplerResult(samplerId); | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| // Process results | ||||||||||||||||||||||||
| await this.#handleSamplerResponse(samplerRun); | ||||||||||||||||||||||||
| } catch (error) { | ||||||||||||||||||||||||
| errorUtils.errorHandler(error); | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||
| * Fetch the status of an existing sampler run | ||||||||||||||||||||||||
| * @param {string} samplerId - The sampler run ID | ||||||||||||||||||||||||
| * @returns {Promise<void>} | ||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||
| async checkStatus(samplerId) { | ||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||
| consola.info(`Fetching status for sampler run ID: ${samplerId}`); | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| const response = await SF.readSamplerRun(this.partnerId, samplerId); | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| if (!response || !response.data) { | ||||||||||||||||||||||||
| consola.error("Failed to fetch sampler run status. Is staging running?"); | ||||||||||||||||||||||||
| process.exit(1); | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| await this.#handleSamplerResponse(response.data); | ||||||||||||||||||||||||
| } catch (error) { | ||||||||||||||||||||||||
| errorUtils.errorHandler(error); | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||
| * Build sampler parameters from local template files | ||||||||||||||||||||||||
| * @param {Object} templateHandles - Object containing arrays of template identifiers | ||||||||||||||||||||||||
| * @param {Array<string>} templateHandles.reconciliationTexts - Array of reconciliation text handles | ||||||||||||||||||||||||
| * @param {Array<string>} templateHandles.accountTemplates - Array of account template names | ||||||||||||||||||||||||
| * @param {Array<string>} templateHandles.sharedParts - Array of shared part names | ||||||||||||||||||||||||
| * @param {Array<number>} firmIds - Array of firm IDs to use in the sampler | ||||||||||||||||||||||||
| * @returns {Object} Sampler payload with templates array | ||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||
| async #buildSamplerParams(templateHandles = {}, firmIds = []) { | ||||||||||||||||||||||||
| const templates = []; | ||||||||||||||||||||||||
| const { reconciliationTexts = [], accountTemplates = [], sharedParts = [] } = templateHandles; | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| // Process reconciliation texts | ||||||||||||||||||||||||
| for (const handle of reconciliationTexts) { | ||||||||||||||||||||||||
| const templateType = "reconciliationText"; | ||||||||||||||||||||||||
| const configPresent = fsUtils.configExists(templateType, handle); | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| if (!configPresent) { | ||||||||||||||||||||||||
| consola.error(`Config file for reconciliation text "${handle}" not found`); | ||||||||||||||||||||||||
| process.exit(1); | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| const config = fsUtils.readConfig(templateType, handle); | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| // Validate partner_id exists in config | ||||||||||||||||||||||||
| if (!config.partner_id || !config.partner_id[this.partnerId]) { | ||||||||||||||||||||||||
| consola.error(`Template '${handle}' has no partner_id entry for partner ${this.partnerId}. Import the template to this partner first.`); | ||||||||||||||||||||||||
| process.exit(1); | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| const templateId = config.partner_id[this.partnerId]; | ||||||||||||||||||||||||
| const templateContent = ReconciliationText.read(handle); | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| templates.push({ | ||||||||||||||||||||||||
| type: "Global::Partner::ReconciliationText", | ||||||||||||||||||||||||
| id: String(templateId), | ||||||||||||||||||||||||
| text: templateContent.text, | ||||||||||||||||||||||||
| text_parts: templateContent.text_parts, | ||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| // Process account templates | ||||||||||||||||||||||||
| for (const name of accountTemplates) { | ||||||||||||||||||||||||
| const templateType = "accountTemplate"; | ||||||||||||||||||||||||
| const configPresent = fsUtils.configExists(templateType, name); | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| if (!configPresent) { | ||||||||||||||||||||||||
| consola.error(`Config file for account template "${name}" not found`); | ||||||||||||||||||||||||
| process.exit(1); | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| const config = fsUtils.readConfig(templateType, name); | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| // Validate partner_id exists in config | ||||||||||||||||||||||||
| if (!config.partner_id || !config.partner_id[this.partnerId]) { | ||||||||||||||||||||||||
| consola.error(`Template '${name}' has no partner_id entry for partner ${this.partnerId}. Import the template to this partner first.`); | ||||||||||||||||||||||||
| process.exit(1); | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| const templateId = config.partner_id[this.partnerId]; | ||||||||||||||||||||||||
| const templateContent = AccountTemplate.read(name); | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| templates.push({ | ||||||||||||||||||||||||
| type: "Global::Partner::AccountDetailTemplate", | ||||||||||||||||||||||||
| id: String(templateId), | ||||||||||||||||||||||||
| text: templateContent.text, | ||||||||||||||||||||||||
| text_parts: templateContent.text_parts, | ||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||
|
Comment on lines
+117
to
+154
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Await template reads (and handle falsy content) to avoid Promise deref.
🛠️ Proposed fix- const templateContent = ReconciliationText.read(handle);
+ const templateContent = await ReconciliationText.read(handle);
+ if (!templateContent) {
+ consola.error(`Reconciliation text "${handle}" wasn't found`);
+ process.exit(1);
+ }
@@
- const templateContent = AccountTemplate.read(name);
+ const templateContent = await AccountTemplate.read(name);
+ if (!templateContent) {
+ consola.error(`Account template "${name}" wasn't found`);
+ process.exit(1);
+ }🤖 Prompt for AI Agents |
||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| // Process shared parts | ||||||||||||||||||||||||
| for (const name of sharedParts) { | ||||||||||||||||||||||||
| const templateType = "sharedPart"; | ||||||||||||||||||||||||
| const configPresent = fsUtils.configExists(templateType, name); | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| if (!configPresent) { | ||||||||||||||||||||||||
| consola.error(`Config file for shared part "${name}" not found`); | ||||||||||||||||||||||||
| process.exit(1); | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| const config = fsUtils.readConfig(templateType, name); | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| // Validate partner_id exists in config | ||||||||||||||||||||||||
| if (!config.partner_id || !config.partner_id[this.partnerId]) { | ||||||||||||||||||||||||
| consola.error(`Shared part '${name}' has no partner_id entry for partner ${this.partnerId}. Import the shared part to this partner first.`); | ||||||||||||||||||||||||
| process.exit(1); | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| const templateId = config.partner_id[this.partnerId]; | ||||||||||||||||||||||||
| const templateContent = await SharedPart.read(name); | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| templates.push({ | ||||||||||||||||||||||||
| type: "Global::Partner::SharedPart", | ||||||||||||||||||||||||
| id: String(templateId), | ||||||||||||||||||||||||
| text: templateContent.text, | ||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| return { templates, firm_ids: firmIds }; | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||
| * Poll for sampler run completion | ||||||||||||||||||||||||
| * @param {number} partnerId - The partner ID | ||||||||||||||||||||||||
| * @param {string} samplerId - The sampler run ID | ||||||||||||||||||||||||
| * @returns {Promise<Object>} The completed sampler run | ||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||
|
Comment on lines
+188
to
+193
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fix stale JSDoc param in The doc block lists a ✏️ Suggested doc fix /**
* Poll for sampler run completion
- * `@param` {number} partnerId - The partner ID
* `@param` {string} samplerId - The sampler run ID
* `@returns` {Promise<Object>} The completed sampler run
*/📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||
| async #fetchAndWaitSamplerResult(samplerId) { | ||||||||||||||||||||||||
| let samplerRun = { status: "pending" }; | ||||||||||||||||||||||||
| const pollingDelay = 15000; // 15 seconds | ||||||||||||||||||||||||
| const waitingLimit = 3600000; // 1 hour | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| spinner.spin("Running sampler..."); | ||||||||||||||||||||||||
| let waitingTime = 0; | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| while (samplerRun.status === "pending" || samplerRun.status === "running") { | ||||||||||||||||||||||||
| await new Promise((resolve) => setTimeout(resolve, pollingDelay)); | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| const response = await SF.readSamplerRun(this.partnerId, samplerId); | ||||||||||||||||||||||||
| samplerRun = response.data; | ||||||||||||||||||||||||
|
coderabbitai[bot] marked this conversation as resolved.
|
||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| waitingTime += pollingDelay; | ||||||||||||||||||||||||
| // pollingDelay *= 1.05; | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| if (waitingTime >= waitingLimit) { | ||||||||||||||||||||||||
| spinner.stop(); | ||||||||||||||||||||||||
| consola.error("Timeout. Try to fetch the status by using the --id flag, if not run your sampler again"); | ||||||||||||||||||||||||
| process.exit(1); | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| spinner.stop(); | ||||||||||||||||||||||||
| return samplerRun; | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||
| * Process and display sampler run results | ||||||||||||||||||||||||
| * @param {Object} response - The sampler run response | ||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||
| async #handleSamplerResponse(response) { | ||||||||||||||||||||||||
| if (!response || !response.status) { | ||||||||||||||||||||||||
| consola.error("Invalid sampler response"); | ||||||||||||||||||||||||
| process.exit(1); | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| switch (response.status) { | ||||||||||||||||||||||||
| case "failed": | ||||||||||||||||||||||||
| consola.error(`Sampler run failed: ${response.error_message || "Unknown error"}`); | ||||||||||||||||||||||||
| break; | ||||||||||||||||||||||||
|
Comment on lines
+233
to
+235
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Failed sampler runs should exit non-zero. Line 234 logs the failure and then Suggested patch case "failed":
consola.error(`Sampler run failed: ${response.error_message || "Unknown error"}`);
- break;
+ process.exit(1);🤖 Prompt for AI Agents |
||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| case "completed": | ||||||||||||||||||||||||
| consola.success("Sampler run completed successfully"); | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| if (response && response.result_url) { | ||||||||||||||||||||||||
| await new UrlHandler(response.result_url).openFile(); | ||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||
| consola.warn("No URL returned"); | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
| break; | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| case "pending": | ||||||||||||||||||||||||
| case "running": | ||||||||||||||||||||||||
| consola.info(`Sampler run is still in progress. Current status: "${response.status}". Please check again later.`); | ||||||||||||||||||||||||
| break; | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| default: | ||||||||||||||||||||||||
| consola.error(`Unexpected sampler status: ${response.status}`); | ||||||||||||||||||||||||
| process.exit(1); | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| module.exports = { LiquidSamplerRunner }; | ||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Normalize
firmIdsto numbers before calling the runner.options.firmIdsfrom Commander are strings, butLiquidSamplerRunnerexpectsArray<number>(and likely the API does too). Convert and validate before passing.🛠️ Proposed fix
🤖 Prompt for AI Agents