-
Notifications
You must be signed in to change notification settings - Fork 2
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
Merged
Merged
Changes from all commits
Commits
Show all changes
8 commits
Select commit
Hold shift + click to select a range
f56fd36
feat: liquid sampler command
AgustinSilverfin 3da58a4
Gate `--id` option + add validation to --id
Toby-Masters-SF dffd8df
Remove duplicated guard and simplify private method buildSamplerParams
Toby-Masters-SF cba14f8
Minor updates to error handling of handleSamplerResponse
Toby-Masters-SF e665ebb
Update package version
Toby-Masters-SF 47ca5e4
Make new sampler command hidden
Toby-Masters-SF 7649bc3
fixup! Minor updates to error handling of handleSamplerResponse
Toby-Masters-SF e81fdc0
Update per Claude review
Toby-Masters-SF File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,263 @@ | ||
| 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 { | ||
| // Build payload | ||
| const samplerParams = await this.#buildSamplerParams(templateHandles, firmIds); | ||
|
|
||
|
Toby-Masters-SF marked this conversation as resolved.
|
||
| consola.info(`Starting sampler run with ${samplerParams.templates.length} template(s)...`); | ||
|
|
||
| // Start sampler run | ||
| const samplerResponse = await SF.createSamplerRun(this.partnerId, samplerParams); | ||
|
|
||
| if (!samplerResponse?.data?.id) { | ||
| consola.error("Failed to start sampler run - no ID returned"); | ||
| if (samplerResponse?.data) { | ||
| consola.error(`Response data: ${JSON.stringify(samplerResponse.data)}`); | ||
|
michieldegezelle marked this conversation as resolved.
|
||
| } | ||
| process.exit(1); | ||
| } | ||
|
|
||
| const samplerId = samplerResponse.data.id; | ||
|
|
||
| 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?.data?.status) { | ||
| 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 templateId = this.#resolveTemplateId("reconciliationText", handle, "reconciliation text"); | ||
| const { text, text_parts } = await this.#readTemplateContent(ReconciliationText, handle, "reconciliation text"); | ||
|
|
||
| templates.push({ | ||
| type: "reconciliation_text", | ||
| id: templateId, | ||
| text, | ||
| text_parts, | ||
| }); | ||
| } | ||
|
|
||
| // Process account templates | ||
| for (const name of accountTemplates) { | ||
| const templateId = this.#resolveTemplateId("accountTemplate", name, "account template"); | ||
| const { text, text_parts } = await this.#readTemplateContent(AccountTemplate, name, "account template"); | ||
|
|
||
| templates.push({ | ||
| type: "account_detail_template", | ||
| id: templateId, | ||
| text, | ||
| text_parts, | ||
| }); | ||
| } | ||
|
|
||
| // Process shared parts | ||
| for (const name of sharedParts) { | ||
| const templateId = this.#resolveTemplateId("sharedPart", name, "shared part"); | ||
| const { text } = await this.#readTemplateContent(SharedPart, name, "shared part"); | ||
|
|
||
| templates.push({ | ||
| type: "shared_part", | ||
| id: templateId, | ||
| text, | ||
| }); | ||
| } | ||
|
|
||
| return { templates, firm_ids: firmIds }; | ||
| } | ||
|
|
||
| /** | ||
| * Resolve a template's partner-specific ID from its local config. | ||
| * Exits the process with a helpful message if the config is missing or has | ||
| * no partner_id entry for the current partner. | ||
| * @param {string} templateType - Config type ("reconciliationText" | "accountTemplate" | "sharedPart") | ||
| * @param {string} handle - Template handle/name | ||
| * @param {string} label - Human-readable label used in error messages | ||
| * @returns {string} The partner template ID | ||
| */ | ||
| #resolveTemplateId(templateType, handle, label) { | ||
| if (!fsUtils.configExists(templateType, handle)) { | ||
|
Toby-Masters-SF marked this conversation as resolved.
|
||
| consola.error(`Config file for ${label} "${handle}" not found`); | ||
| process.exit(1); | ||
| } | ||
|
|
||
| const config = fsUtils.readConfig(templateType, handle); | ||
|
|
||
| if (!config.partner_id || !config.partner_id[this.partnerId]) { | ||
|
michieldegezelle marked this conversation as resolved.
|
||
| consola.error(`${label} '${handle}' has no partner_id entry for partner ${this.partnerId}. Import it to this partner first.`); | ||
| process.exit(1); | ||
| } | ||
|
|
||
| return String(config.partner_id[this.partnerId]); | ||
| } | ||
|
|
||
| /** | ||
| * Read a template's local content, exiting with a clear message if it | ||
| * can't be read (e.g. an invalid handle makes `read` return false). | ||
| * Awaiting is safe for both the synchronous (ReconciliationText, | ||
| * AccountTemplate) and asynchronous (SharedPart) read implementations. | ||
| * @param {Object} TemplateClass - Template class exposing a static `read` | ||
| * @param {string} handle - Template handle/name | ||
| * @param {string} label - Human-readable label used in error messages | ||
| * @returns {Promise<Object>} The template content ({ text, text_parts? }) | ||
| */ | ||
| async #readTemplateContent(TemplateClass, handle, label) { | ||
| const templateContent = await TemplateClass.read(handle); | ||
|
|
||
| if (!templateContent) { | ||
| consola.error(`Could not read ${label} "${handle}"`); | ||
| process.exit(1); | ||
| } | ||
|
|
||
| return templateContent; | ||
| } | ||
|
|
||
| /** | ||
| * Poll for sampler run completion | ||
| * @param {string} samplerId - The sampler run ID | ||
| * @returns {Promise<Object>} The completed sampler run | ||
| */ | ||
|
Toby-Masters-SF marked this conversation as resolved.
|
||
| async #fetchAndWaitSamplerResult(samplerId) { | ||
| let samplerRun = { status: "pending" }; | ||
| const pollingDelay = 15000; // 15 seconds | ||
| const waitingLimit = 3600000; // 1 hour | ||
|
|
||
| spinner.spin("Running sampler..."); | ||
| let waitingTime = 0; | ||
|
|
||
| try { | ||
| while (samplerRun.status === "pending" || samplerRun.status === "running") { | ||
| // Pause the loop before polling again (setTimeout wrapped in a Promise so it can be awaited) | ||
| await new Promise((resolve) => setTimeout(resolve, pollingDelay)); | ||
|
|
||
| // Poll for the sampler run status | ||
| const response = await SF.readSamplerRun(this.partnerId, samplerId); | ||
| samplerRun = response?.data; | ||
|
|
||
| if (!samplerRun?.status) { | ||
| // process.exit() bypasses the finally block, so stop the spinner explicitly here. | ||
| spinner.stop(); | ||
| consola.error("Failed to fetch sampler run status. Is staging running?"); | ||
| process.exit(1); | ||
| } | ||
|
|
||
| waitingTime += pollingDelay; | ||
|
|
||
| if (waitingTime >= waitingLimit) { | ||
| // process.exit() bypasses the finally block, so stop the spinner explicitly here. | ||
| spinner.stop(); | ||
| consola.error("Timeout. Try to fetch the status by using the --id flag, if not run your sampler again"); | ||
| process.exit(1); | ||
| } | ||
| } | ||
|
|
||
| return samplerRun; | ||
| } finally { | ||
| spinner.stop(); | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Process and display sampler run results | ||
| * @param {Object} response - The sampler run response | ||
| */ | ||
| async #handleSamplerResponse(response) { | ||
| switch (response.status) { | ||
| case "failed": | ||
| consola.error(`Sampler run failed: ${response.error_message || "Unknown error"}`); | ||
| process.exit(1); | ||
| break; // eslint-disable-line no-unreachable | ||
|
|
||
| case "completed": | ||
| consola.success("Sampler run completed successfully"); | ||
|
|
||
| if (response && response.result_url) { | ||
| await new UrlHandler(response.result_url).openFile(); | ||
| } else { | ||
| consola.error("Sampler completed but no result URL was returned"); | ||
| process.exit(1); | ||
| break; // eslint-disable-line no-unreachable | ||
| } | ||
| break; | ||
|
|
||
| case "pending": | ||
| case "running": | ||
|
Toby-Masters-SF marked this conversation as resolved.
|
||
| 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 }; | ||
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.