From 8827de06d43463e9517374ca87ca0b3a67da243b Mon Sep 17 00:00:00 2001 From: delphine-demeulenaere Date: Tue, 8 Apr 2025 16:47:03 +0200 Subject: [PATCH 1/7] add more tests for AT's (WIP) --- tests/lib/templates/accountTemplates.test.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/lib/templates/accountTemplates.test.js b/tests/lib/templates/accountTemplates.test.js index 3503be0..fa1651b 100644 --- a/tests/lib/templates/accountTemplates.test.js +++ b/tests/lib/templates/accountTemplates.test.js @@ -13,6 +13,8 @@ describe("AccountTemplate", () => { const textParts = { part_1: "Part 1: updated content" }; const template = { name_nl: "name_nl", + name_en: "name_nl", + name_fr: "name_nl", id: 808080, text: "Main liquid content", text_parts: [ From 1b2e2077d6756c1edac1d0a2090cb69635272874 Mon Sep 17 00:00:00 2001 From: Michiel Degezelle Date: Mon, 28 Jul 2025 11:34:32 +0200 Subject: [PATCH 2/7] Add update to changelog and bump version --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 33d9e2d..f2abfa7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -69,4 +69,4 @@ For example: `silverfin update-reconciliation --id 12345` - Add tests for the exportFile class ## [1.38.0] (04/07/2025) -- Added a changelog.md file and logic to display the changes when updating to latest version +- Added a changelog.md file and logic to display the changes when updating to latest version \ No newline at end of file From 63daea4717868f9727df8e5d439782d2b76b2c45 Mon Sep 17 00:00:00 2001 From: Michiel Degezelle Date: Wed, 17 Dec 2025 13:25:32 +0100 Subject: [PATCH 3/7] Activate create-test command for account templates --- lib/api/sfApi.js | 54 +++++ lib/liquidTestGenerator.js | 278 ++++++++++++++++---------- lib/utils/liquidTestUtils.js | 76 +++++-- tests/lib/liquidTestGenerator.test.js | 20 +- 4 files changed, 297 insertions(+), 131 deletions(-) diff --git a/lib/api/sfApi.js b/lib/api/sfApi.js index 4a8454b..2d24f55 100644 --- a/lib/api/sfApi.js +++ b/lib/api/sfApi.js @@ -404,6 +404,30 @@ async function removeSharedPartFromAccountTemplate(type, envId, sharedPartId, ac } } +async function getAccountTemplateCustom(type, envId, companyId, periodId, accountTemplateId, page = 1) { + const instance = AxiosFactory.createInstance(type, envId); + try { + const response = await instance.get(`/companies/${companyId}/periods/${periodId}/accounts/${accountTemplateId}/custom`, { params: { page: page, per_page: 200 } }); + apiUtils.responseSuccessHandler(response); + return response; + } catch (error) { + const response = await apiUtils.responseErrorHandler(error); + return response; + } +} + +async function getAccountTemplateResults(type, envId, companyId, periodId, accountTemplateId) { + const instance = AxiosFactory.createInstance(type, envId); + try { + const response = await instance.get(`/companies/${companyId}/periods/${periodId}/accounts/${accountTemplateId}/results`); + apiUtils.responseSuccessHandler(response); + return response; + } catch (error) { + const response = await apiUtils.responseErrorHandler(error); + return response; + } +} + async function createTestRun(firmId, attributes, templateType) { const instance = AxiosFactory.createInstance("firm", firmId); let response; @@ -615,6 +639,33 @@ async function getAccountDetails(firmId, companyId, periodId, accountId) { } } +async function findAccountByNumber(firmId, companyId, periodId, accountNumber, page = 1) { + const instance = AxiosFactory.createInstance("firm", firmId); + try { + const response = await instance.get(`companies/${companyId}/periods/${periodId}/accounts`, { + params: { page: page }, + }); + + const accounts = response.data; + + // No data - end of pagination + if (accounts.length === 0) { + return null; + } + + // Look for the account in this page + const account = accounts.find((acc) => acc.account.number === accountNumber); + if (account) { + return account; + } + + // Not found in this page, try next page + return findAccountByNumber(firmId, companyId, periodId, accountNumber, page + 1); + } catch (error) { + apiUtils.responseErrorHandler(error); + } +} + // Liquid Linter // attributes should be JSON async function verifyLiquid(firmId, attributes) { @@ -698,6 +749,8 @@ module.exports = { findAccountTemplateByName, addSharedPartToAccountTemplate, removeSharedPartFromAccountTemplate, + getAccountTemplateCustom, + getAccountTemplateResults, readTestRun, createTestRun, createPreviewRun, @@ -712,6 +765,7 @@ module.exports = { findReconciliationInWorkflow, findReconciliationInWorkflows, getAccountDetails, + findAccountByNumber, verifyLiquid, getFirmDetails, createExportFileInstance, diff --git a/lib/liquidTestGenerator.js b/lib/liquidTestGenerator.js index ac497da..f4f6ced 100644 --- a/lib/liquidTestGenerator.js +++ b/lib/liquidTestGenerator.js @@ -3,15 +3,17 @@ const { firmCredentials } = require("../lib/api/firmCredentials"); const Utils = require("./utils/liquidTestUtils"); const { consola } = require("consola"); const { ReconciliationText } = require("./templates/reconciliationText"); +const { AccountTemplate } = require("./templates/accountTemplate"); const { SharedPart } = require("./templates/sharedPart"); // MainProcess async function testGenerator(url, testName, reconciledStatus = true) { - // Liquid Test Object - const liquidTestObject = Utils.createBaseLiquidTest(testName); - - // Get parameters from URL provided + // Get parameters from URL provided and determine template type const parameters = Utils.extractURL(url); + const templateType = parameters.templateType; + + // Create appropriate base test structure + const liquidTestObject = Utils.createBaseLiquidTest(testName, templateType); // Check if firm is authorized if (!Object.hasOwn(firmCredentials.data, parameters.firmId)) { @@ -22,14 +24,50 @@ async function testGenerator(url, testName, reconciledStatus = true) { // Reconciled Status (CLI argument. True by default) liquidTestObject[testName].expectation.reconciled = reconciledStatus; - // Get Reconciliation Details - const responseDetails = await SF.readReconciliationTextDetails("firm", parameters.firmId, parameters.companyId, parameters.ledgerId, parameters.reconciliationId); - const reconciliationHandle = responseDetails.data.handle; + let responseDetails, templateHandle; + switch (templateType) { + case "reconciliationText": { + // Get Reconciliation Details + responseDetails = await SF.readReconciliationTextDetails("firm", parameters.firmId, parameters.companyId, parameters.ledgerId, parameters.reconciliationId); + templateHandle = responseDetails.data.handle; + break; + } + case "accountTemplate": { + try { + // Get account data (includes template ID reference) + responseDetails = await SF.findAccountByNumber(parameters.firmId, parameters.companyId, parameters.ledgerId, parameters.accountId); + + // Extract the template ID from the account data + const accountTemplateId = responseDetails.account_reconciliation_template?.id; + if (!accountTemplateId) { + throw new Error(`No account template associated with account ${parameters.accountId}`); + } + + // Get the actual template details using the ID + const templateDetails = await SF.readAccountTemplateById("firm", parameters.firmId, accountTemplateId); + templateHandle = templateDetails.name_nl; + + liquidTestObject[testName].context.current_account = responseDetails.account.number; + } catch (error) { + consola.error(`Failed to get account template details: ${error.message}`); + process.exit(1); + } + break; + } + } // Get Workflow Information - const starredStatus = { - ...SF.findReconciliationInWorkflow(parameters.firmId, reconciliationHandle, parameters.companyId, parameters.ledgerId, parameters.workflowId), - }.starred; + let starredStatus; + switch (templateType) { + case "reconciliationText": + starredStatus = { + ...SF.findReconciliationInWorkflow(parameters.firmId, templateHandle, parameters.companyId, parameters.ledgerId, parameters.workflowId), + }.starred; + break; + case "accountTemplate": + starredStatus = responseDetails.starred; // Already present in the response of the Account + break; + } // Get period data const responsePeriods = await SF.getPeriods(parameters.firmId, parameters.companyId); @@ -59,123 +97,157 @@ async function testGenerator(url, testName, reconciledStatus = true) { } process.exit; - // Get all the text properties (Customs from current template) - const responseCustom = await SF.getReconciliationCustom("firm", parameters.firmId, parameters.companyId, parameters.ledgerId, parameters.reconciliationId); - const currentReconCustom = Utils.processCustom(responseCustom.data); - liquidTestObject[testName].data.periods[currentPeriodData.fiscal_year.end_date].reconciliations[reconciliationHandle] = { - starred: starredStatus, - custom: currentReconCustom, - }; + // Get all the text properties (customs) and results from current template + switch (templateType) { + case "reconciliationText": { + const responseCustom = await SF.getReconciliationCustom("firm", parameters.firmId, parameters.companyId, parameters.ledgerId, parameters.reconciliationId); + const currentReconCustom = Utils.processCustom(responseCustom.data); + liquidTestObject[testName].data.periods[currentPeriodData.fiscal_year.end_date].reconciliations[templateHandle] = { + starred: starredStatus, + custom: currentReconCustom, + }; + + // Get all the results generated in current template + const responseResults = await SF.getReconciliationResults("firm", parameters.firmId, parameters.companyId, parameters.ledgerId, parameters.reconciliationId); + liquidTestObject[testName].expectation.results = responseResults.data; + + break; + } + case "accountTemplate": { + const responseCustom = await SF.getAccountTemplateCustom("firm", parameters.firmId, parameters.companyId, parameters.ledgerId, responseDetails.account.id); + const currentAccountTemplateCustom = Utils.processCustom(responseCustom.data); + liquidTestObject[testName].data.periods[currentPeriodData.fiscal_year.end_date].accounts = { + [responseDetails.account.number]: { + name: responseDetails.account.name, + value: Number(responseDetails.value), + custom: currentAccountTemplateCustom, + }, + }; + + // Get all the results generated in current template + const responseResults = await SF.getAccountTemplateResults("firm", parameters.firmId, parameters.companyId, parameters.ledgerId, responseDetails.account.id); + liquidTestObject[testName].expectation.results = responseResults.data; - // Get all the results generated in current template - const responseResults = await SF.getReconciliationResults("firm", parameters.firmId, parameters.companyId, parameters.ledgerId, parameters.reconciliationId); - liquidTestObject[testName].expectation.results = responseResults.data; + break; + } + } // Get the code of the template - const reconciliationTextCode = await ReconciliationText.read(reconciliationHandle); - if (!reconciliationTextCode) { - consola.warn(`Reconciliation "${reconciliationHandle}" wasn't found`); + let templateCode; + switch (templateType) { + case "reconciliationText": + templateCode = await ReconciliationText.read(templateHandle); + break; + case "accountTemplate": + templateCode = await AccountTemplate.read(templateHandle); + break; + } + + if (!templateCode) { + consola.warn(`Template "${templateHandle}" wasn't found`); process.exit(); } - // Search for results from other reconciliations used in the liquid code (main and text_parts) - let resultsObj; - resultsObj = Utils.searchForResultsFromDependenciesInLiquid(reconciliationTextCode, reconciliationHandle); - - // Search for custom drops from other reconcilations used in the liquid code (main and text_parts) - let customsObj; - customsObj = Utils.searchForCustomsFromDependenciesInLiquid(reconciliationTextCode, reconciliationHandle); - - // Search for shared parts in the liquid code (main and text_parts) - const sharedPartsUsed = Utils.lookForSharedPartsInLiquid(reconciliationTextCode, reconciliationHandle); - if (sharedPartsUsed && sharedPartsUsed.length != 0) { - for (const sharedPartName of sharedPartsUsed) { - const sharedPartCode = await SharedPart.read(sharedPartName); - if (!sharedPartCode) { - consola.warn(`Shared part "${sharedPartName}" wasn't found`); - return; - } + if (templateType === "reconciliationText") { + // Search for results from other reconciliations used in the liquid code (main and text_parts) + let resultsObj; + resultsObj = Utils.searchForResultsFromDependenciesInLiquid(templateCode, templateHandle); - // Look for nested shared parts (in that case, add them to this same loop) - const nestedSharedParts = Utils.lookForSharedPartsInLiquid(sharedPartCode); - for (const nested of nestedSharedParts) { - if (!sharedPartsUsed.includes(nested)) { - sharedPartsUsed.push(nested); + // Search for custom drops from other reconcilations used in the liquid code (main and text_parts) + let customsObj; + customsObj = Utils.searchForCustomsFromDependenciesInLiquid(templateCode, templateHandle); + + // Search for shared parts in the liquid code (main and text_parts) + const sharedPartsUsed = Utils.lookForSharedPartsInLiquid(templateCode, templateHandle); + if (sharedPartsUsed && sharedPartsUsed.length != 0) { + for (const sharedPartName of sharedPartsUsed) { + const sharedPartCode = await SharedPart.read(sharedPartName); + if (!sharedPartCode) { + consola.warn(`Shared part "${sharedPartName}" wasn't found`); + return; } - } - // Search for results from other reconciliations in shared part (we append to existing collection) - resultsObj = Utils.searchForResultsFromDependenciesInLiquid(sharedPartCode, sharedPartCode.name, resultsObj); + // Look for nested shared parts (in that case, add them to this same loop) + const nestedSharedParts = Utils.lookForSharedPartsInLiquid(sharedPartCode); + for (const nested of nestedSharedParts) { + if (!sharedPartsUsed.includes(nested)) { + sharedPartsUsed.push(nested); + } + } - // Search for custom drops from other reconcilations in shared parts (we append to existing collection) - customsObj = Utils.searchForCustomsFromDependenciesInLiquid(sharedPartCode, sharedPartCode.name, customsObj); + // Search for results from other reconciliations in shared part (we append to existing collection) + resultsObj = Utils.searchForResultsFromDependenciesInLiquid(sharedPartCode, sharedPartCode.name, resultsObj); + + // Search for custom drops from other reconcilations in shared parts (we append to existing collection) + customsObj = Utils.searchForCustomsFromDependenciesInLiquid(sharedPartCode, sharedPartCode.name, customsObj); + } } - } - // Get results from dependencies reconciliations - if (Object.keys(resultsObj).length !== 0) { - // Search in each reconciliation - for (const [handle, resultsArray] of Object.entries(resultsObj)) { - try { - // Find reconciliation in Workflow to get id (depdeency template can be in a different Workflow) - const reconciliation = await SF.findReconciliationInWorkflows(parameters.firmId, handle, parameters.companyId, parameters.ledgerId); - if (reconciliation) { - // Fetch results - const reconciliationResults = await SF.getReconciliationResults("firm", parameters.firmId, parameters.companyId, parameters.ledgerId, reconciliation.id); - // Add handle and results block to Liquid Test - liquidTestObject[testName].data.periods[currentPeriodData.fiscal_year.end_date].reconciliations[handle] = - liquidTestObject[testName].data.periods[currentPeriodData.fiscal_year.end_date].reconciliations[handle] || {}; - liquidTestObject[testName].data.periods[currentPeriodData.fiscal_year.end_date].reconciliations[handle].results = - liquidTestObject[testName].data.periods[currentPeriodData.fiscal_year.end_date].reconciliations[handle].results || {}; - // Search for results - for (const resultTag of resultsArray) { - // Add result to Liquid Test - liquidTestObject[testName].data.periods[currentPeriodData.fiscal_year.end_date].reconciliations[handle].results[resultTag] = reconciliationResults.data[resultTag]; + // Get results from dependencies reconciliations + if (Object.keys(resultsObj).length !== 0) { + // Search in each reconciliation + for (const [handle, resultsArray] of Object.entries(resultsObj)) { + try { + // Find reconciliation in Workflow to get id (depdeency template can be in a different Workflow) + const reconciliation = await SF.findReconciliationInWorkflows(parameters.firmId, handle, parameters.companyId, parameters.ledgerId); + if (reconciliation) { + // Fetch results + const reconciliationResults = await SF.getReconciliationResults("firm", parameters.firmId, parameters.companyId, parameters.ledgerId, reconciliation.id); + // Add handle and results block to Liquid Test + liquidTestObject[testName].data.periods[currentPeriodData.fiscal_year.end_date].reconciliations[handle] = + liquidTestObject[testName].data.periods[currentPeriodData.fiscal_year.end_date].reconciliations[handle] || {}; + liquidTestObject[testName].data.periods[currentPeriodData.fiscal_year.end_date].reconciliations[handle].results = + liquidTestObject[testName].data.periods[currentPeriodData.fiscal_year.end_date].reconciliations[handle].results || {}; + // Search for results + for (const resultTag of resultsArray) { + // Add result to Liquid Test + liquidTestObject[testName].data.periods[currentPeriodData.fiscal_year.end_date].reconciliations[handle].results[resultTag] = reconciliationResults.data[resultTag]; + } } + } catch (err) { + consola.error(err); } - } catch (err) { - consola.error(err); } } - } - // We already got the text properties from current reconciliation - if (Object.hasOwn(customsObj, reconciliationHandle)) { - delete customsObj[reconciliationHandle]; - } + // We already got the text properties from current reconciliation + if (Object.hasOwn(customsObj, templateHandle)) { + delete customsObj[templateHandle]; + } - // Get custom drops from dependency reconciliations - if (Object.keys(customsObj).length !== 0) { - // Search in each reconciliation - for (const [handle, customsArray] of Object.entries(customsObj)) { - try { - // Find reconciliation in Workflow to get id (depdeency template can be in a different Workflow) - const reconciliation = await SF.findReconciliationInWorkflows(parameters.firmId, handle, parameters.companyId, parameters.ledgerId); - if (reconciliation) { - // Fetch test properties - const reconciliationCustomResponse = await SF.getReconciliationCustom("firm", parameters.firmId, parameters.companyId, parameters.ledgerId, reconciliation.id); - const reconciliationCustomDrops = Utils.processCustom(reconciliationCustomResponse.data); - // Filter Customs - const dropsKeys = Object.keys(reconciliationCustomDrops); - const matchingKeys = dropsKeys.filter((key) => customsArray.indexOf(key) !== -1); - const filteredCustomDrops = {}; - for (const key of matchingKeys) { - filteredCustomDrops[key] = reconciliationCustomDrops[key]; + // Get custom drops from dependency reconciliations + if (Object.keys(customsObj).length !== 0) { + // Search in each reconciliation + for (const [handle, customsArray] of Object.entries(customsObj)) { + try { + // Find reconciliation in Workflow to get id (depdeency template can be in a different Workflow) + const reconciliation = await SF.findReconciliationInWorkflows(parameters.firmId, handle, parameters.companyId, parameters.ledgerId); + if (reconciliation) { + // Fetch test properties + const reconciliationCustomResponse = await SF.getReconciliationCustom("firm", parameters.firmId, parameters.companyId, parameters.ledgerId, reconciliation.id); + const reconciliationCustomDrops = Utils.processCustom(reconciliationCustomResponse.data); + // Filter Customs + const dropsKeys = Object.keys(reconciliationCustomDrops); + const matchingKeys = dropsKeys.filter((key) => customsArray.indexOf(key) !== -1); + const filteredCustomDrops = {}; + for (const key of matchingKeys) { + filteredCustomDrops[key] = reconciliationCustomDrops[key]; + } + // Add handle to Liquid Test + liquidTestObject[testName].data.periods[currentPeriodData.fiscal_year.end_date].reconciliations[handle] = + liquidTestObject[testName].data.periods[currentPeriodData.fiscal_year.end_date].reconciliations[handle] || {}; + // Add custom drops + liquidTestObject[testName].data.periods[currentPeriodData.fiscal_year.end_date].reconciliations[handle].custom = filteredCustomDrops; } - // Add handle to Liquid Test - liquidTestObject[testName].data.periods[currentPeriodData.fiscal_year.end_date].reconciliations[handle] = - liquidTestObject[testName].data.periods[currentPeriodData.fiscal_year.end_date].reconciliations[handle] || {}; - // Add custom drops - liquidTestObject[testName].data.periods[currentPeriodData.fiscal_year.end_date].reconciliations[handle].custom = filteredCustomDrops; + } catch (err) { + consola.error(err); } - } catch (err) { - consola.error(err); } } } // Get company drop used in the liquid code (main and text_parts) - const companyObj = Utils.getCompanyDependencies(reconciliationTextCode, reconciliationHandle); + const companyObj = Utils.getCompanyDependencies(templateCode, templateHandle); if (companyObj.standardDropElements.length !== 0 || companyObj.customDropElements.length !== 0) { liquidTestObject[testName].data.company = {}; @@ -242,7 +314,7 @@ async function testGenerator(url, testName, reconciledStatus = true) { } // Save YAML - Utils.exportYAML(reconciliationHandle, liquidTestObject); + Utils.exportYAML(templateHandle, liquidTestObject, templateType); } module.exports = { diff --git a/lib/utils/liquidTestUtils.js b/lib/utils/liquidTestUtils.js index e3ae9f6..ba66902 100644 --- a/lib/utils/liquidTestUtils.js +++ b/lib/utils/liquidTestUtils.js @@ -4,8 +4,8 @@ const fsUtils = require("./fsUtils"); const { consola } = require("consola"); // Create base Liquid Test object -function createBaseLiquidTest(testName) { - return { +function createBaseLiquidTest(testName, templateType = "reconciliationText") { + const baseStructure = { [testName]: { context: { period: "#Replace with period", @@ -13,38 +13,50 @@ function createBaseLiquidTest(testName) { data: { periods: { replace_period_name: { - reconciliations: {}, + reconciliations: {}, // Only for reconciliation texts }, }, }, expectation: { reconciled: "#Replace with reconciled status", results: {}, + rollforward: {}, }, }, }; + + // Add current_account for account templates + if (templateType === "accountTemplate") { + baseStructure[testName].context.current_account = "#Replace with current account"; + delete baseStructure[testName].data.periods.replace_period_name.reconciliations; // Remove reconciliations + } + + return baseStructure; } -// Provide a link to reconciliation in Silverfin -// Extract firm id, company id, period id, reconciliation id +// Provide a link to reconciliation or account template in Silverfin +// Extract template type, firm id, company id, period id, template id function extractURL(url) { try { const parts = url.split("?")[0].split("/f/")[1].split("/"); - let type; + let idType, templateType; if (parts.indexOf("reconciliation_texts") !== -1) { - type = "reconciliationId"; + idType = "reconciliationId"; + templateType = "reconciliationText"; } else if (parts.indexOf("account_entry") !== -1) { - type = "accountId"; + idType = "accountId"; + templateType = "accountTemplate"; } else { consola.error("Not possible to identify if it's a reconciliation text or account entry."); process.exit(1); } return { + templateType, firmId: parts[0], companyId: parts[1], ledgerId: parts[3], workflowId: parts[5], - [type]: parts[7], + [idType]: parts[7], }; } catch (err) { consola.error("The URL provided is not correct. Double check it and run the command again."); @@ -52,23 +64,45 @@ function extractURL(url) { } } -function generateFileName(handle, counter = 0) { +function generateFileName(handle, templateType, counter = 0) { let fileName = `${handle}_liquid_test.yml`; if (counter != 0) { fileName = `${handle}_${counter}_liquid_test.yml`; } - const filePath = `./reconciliation_texts/${handle}/tests/${fileName}`; + let filePath; + switch (templateType) { + case "reconciliationText": + filePath = `./reconciliation_texts/${handle}/tests/${fileName}`; + break; + case "accountTemplate": + filePath = `./account_templates/${handle}/tests/${fileName}`; + break; + default: + consola.error("Invalid template type"); + process.exit(1); + } if (fs.existsSync(filePath)) { - return generateFileName(handle, counter + 1); + return generateFileName(handle, templateType, counter + 1); } return filePath; } // Create YAML -function exportYAML(handle, liquidTestObject) { - fsUtils.createFolder(`./reconciliation_texts`); - fsUtils.createTemplateFolders("reconciliationText", handle, true); - const filePath = generateFileName(handle); +function exportYAML(handle, liquidTestObject, templateType) { + switch (templateType) { + case "reconciliationText": + fsUtils.createFolder(`./reconciliation_texts`); + fsUtils.createTemplateFolders("reconciliationText", handle, true); + break; + case "accountTemplate": + fsUtils.createFolder(`./account_templates`); + fsUtils.createTemplateFolders("accountTemplate", handle, true); + break; + default: + consola.error("Invalid template type"); + process.exit(1); + } + const filePath = generateFileName(handle, templateType); fs.writeFile( filePath, YAML.stringify(liquidTestObject, { @@ -138,20 +172,20 @@ function processCustom(customArray) { } // Company Drop used -function getCompanyDependencies(reconcilationObject, reconciliationHandle) { +function getCompanyDependencies(templateCode, templateHandle) { const reCompanySearch = RegExp(/company\.\w+(?:\.\w+\.\w+)?/g); // company.foo or company.custom.foo.bar // No main part ? - if (!reconcilationObject || !reconcilationObject.text) { - consola.warn(`Reconciliation "${reconciliationHandle}": no liquid code found`); + if (!templateCode || !templateCode.text) { + consola.warn(`Template "${templateHandle}": no liquid code found`); return { standardDropElements: [], customDropElements: [] }; } // Main Part - let companyFound = reconcilationObject.text.match(reCompanySearch) || []; + let companyFound = templateCode.text.match(reCompanySearch) || []; // Parts - for (const part of reconcilationObject.text_parts) { + for (const part of templateCode.text_parts) { const companyPart = part.content.match(reCompanySearch) || []; if (companyPart) { companyFound = companyFound.concat(companyPart); diff --git a/tests/lib/liquidTestGenerator.test.js b/tests/lib/liquidTestGenerator.test.js index f522755..e12c8bd 100644 --- a/tests/lib/liquidTestGenerator.test.js +++ b/tests/lib/liquidTestGenerator.test.js @@ -28,6 +28,7 @@ describe("liquidTestGenerator", () => { const mockUrl = "https://live.getsilverfin.com/f/123/456/ledgers/789/workflows/101/reconciliation_texts/202"; const mockTestName = "unit_1_test_1"; const mockParameters = { + templateType: "reconciliationText", firmId: "123", companyId: "456", ledgerId: "789", @@ -167,7 +168,8 @@ describe("liquidTestGenerator", () => { }), }), }), - }) + }), + "reconciliationText" ); }); @@ -175,7 +177,7 @@ describe("liquidTestGenerator", () => { ReconciliationText.read.mockResolvedValue(false); await expect(testGenerator(mockUrl, mockTestName)).rejects.toThrow("Process.exit called with code undefined"); - expect(consola.warn).toHaveBeenCalledWith(`Reconciliation "${mockReconciliationHandle}" wasn't found`); + expect(consola.warn).toHaveBeenCalledWith(`Template "${mockReconciliationHandle}" wasn't found`); }); it("should read shared parts correctly", async () => { @@ -225,7 +227,8 @@ describe("liquidTestGenerator", () => { }), }), }), - }) + }), + "reconciliationText" ); }); @@ -247,7 +250,8 @@ describe("liquidTestGenerator", () => { }), }), }), - }) + }), + "reconciliationText" ); }); }); @@ -265,7 +269,7 @@ describe("liquidTestGenerator", () => { ReconciliationText.read.mockResolvedValue(false); await expect(testGenerator(mockUrl, mockTestName)).rejects.toThrow("Process.exit called with code undefined"); - expect(consola.warn).toHaveBeenCalledWith(`Reconciliation "${mockReconciliationHandle}" wasn't found`); + expect(consola.warn).toHaveBeenCalledWith(`Template "${mockReconciliationHandle}" wasn't found`); }); it("should warn and return gracefully for missing shared parts", async () => { @@ -305,7 +309,8 @@ describe("liquidTestGenerator", () => { }), }), }), - }) + }), + "reconciliationText" ); }); @@ -330,7 +335,8 @@ describe("liquidTestGenerator", () => { }), }), }), - }) + }), + "reconciliationText" ); }); }); From 96ede086cdf232a6c3059f96771076b8f530e15c Mon Sep 17 00:00:00 2001 From: Michiel Degezelle Date: Wed, 17 Dec 2025 13:27:30 +0100 Subject: [PATCH 4/7] Fix tests for AT --- tests/lib/templates/accountTemplates.test.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/lib/templates/accountTemplates.test.js b/tests/lib/templates/accountTemplates.test.js index fa1651b..3863c50 100644 --- a/tests/lib/templates/accountTemplates.test.js +++ b/tests/lib/templates/accountTemplates.test.js @@ -13,8 +13,8 @@ describe("AccountTemplate", () => { const textParts = { part_1: "Part 1: updated content" }; const template = { name_nl: "name_nl", - name_en: "name_nl", - name_fr: "name_nl", + name_en: "", + name_fr: "", id: 808080, text: "Main liquid content", text_parts: [ From b58780d88efc8f8841fe09c90ebaf794040e71b1 Mon Sep 17 00:00:00 2001 From: Michiel Degezelle Date: Wed, 17 Dec 2025 13:31:29 +0100 Subject: [PATCH 5/7] Add tests for account templates liquid tests --- tests/lib/liquidTestGenerator.test.js | 276 ++++++++++++++++++++++++++ 1 file changed, 276 insertions(+) diff --git a/tests/lib/liquidTestGenerator.test.js b/tests/lib/liquidTestGenerator.test.js index e12c8bd..09bdb44 100644 --- a/tests/lib/liquidTestGenerator.test.js +++ b/tests/lib/liquidTestGenerator.test.js @@ -3,6 +3,7 @@ const SF = require("../../lib/api/sfApi"); const { firmCredentials } = require("../../lib/api/firmCredentials"); const Utils = require("../../lib/utils/liquidTestUtils"); const { ReconciliationText } = require("../../lib/templates/reconciliationText"); +const { AccountTemplate } = require("../../lib/templates/accountTemplate"); const { SharedPart } = require("../../lib/templates/sharedPart"); const { consola } = require("consola"); @@ -10,6 +11,7 @@ const { consola } = require("consola"); jest.mock("../../lib/api/sfApi"); jest.mock("../../lib/api/firmCredentials"); jest.mock("../../lib/templates/reconciliationText"); +jest.mock("../../lib/templates/accountTemplate"); jest.mock("../../lib/templates/sharedPart"); jest.mock("consola"); @@ -340,5 +342,279 @@ describe("liquidTestGenerator", () => { ); }); }); + + describe("account template test generation", () => { + const mockAccountUrl = "https://live.getsilverfin.com/f/123/456/ledgers/789/workflows/101/account_entry/5000"; + const mockAccountParameters = { + templateType: "accountTemplate", + firmId: "123", + companyId: "456", + ledgerId: "789", + workflowId: "101", + accountId: "5000", + }; + const mockAccountTemplateHandle = "test_account_template"; + const mockAccountResponse = { + account: { + id: 1001, + number: "5000", + name: "Test Account", + }, + value: "12345.67", + starred: false, + account_reconciliation_template: { + id: 9999, + }, + }; + const mockAccountTemplateDetails = { + name_nl: "test_account_template", + name_en: "Test Account Template", + id: 9999, + }; + const mockAccountTemplate = { + name_nl: "test_account_template", + id: 9999, + text: "Main liquid content for account template", + text_parts: [{ name: "part_1", content: "Part 1 content" }], + externally_managed: true, + }; + + beforeEach(() => { + // Override extractURL for account template tests + Utils.extractURL.mockReturnValue(mockAccountParameters); + Utils.createBaseLiquidTest.mockReturnValue({ + [mockTestName]: { + context: { period: "2024-12-31" }, + data: { + periods: { + replace_period_name: { + accounts: {}, + }, + }, + }, + expectation: { + reconciled: true, + results: {}, + }, + }, + }); + + // Mock AccountTemplate.read + AccountTemplate.read.mockResolvedValue(mockAccountTemplate); + + // Mock SF API calls for account templates + SF.findAccountByNumber = jest.fn().mockResolvedValue(mockAccountResponse); + SF.readAccountTemplateById = jest.fn().mockResolvedValue(mockAccountTemplateDetails); + SF.getAccountTemplateCustom = jest.fn().mockResolvedValue({ + data: [ + { + namespace: "account_namespace", + key: "account_key", + value: "account_value", + }, + ], + }); + SF.getAccountTemplateResults = jest.fn().mockResolvedValue({ + data: { account_result1: "value1", account_result2: "value2" }, + }); + }); + + it("should read account template correctly", async () => { + await testGenerator(mockAccountUrl, mockTestName); + + // Verify account lookup + expect(SF.findAccountByNumber).toHaveBeenCalledWith( + mockAccountParameters.firmId, + mockAccountParameters.companyId, + mockAccountParameters.ledgerId, + mockAccountParameters.accountId + ); + + // Verify template details fetch + expect(SF.readAccountTemplateById).toHaveBeenCalledWith("firm", mockAccountParameters.firmId, 9999); + + // Verify AccountTemplate.read was called with the correct handle + expect(AccountTemplate.read).toHaveBeenCalledWith(mockAccountTemplateHandle); + }); + + it("should set current_account in context", async () => { + await testGenerator(mockAccountUrl, mockTestName); + + expect(Utils.exportYAML).toHaveBeenCalledWith( + mockAccountTemplateHandle, + expect.objectContaining({ + [mockTestName]: expect.objectContaining({ + context: expect.objectContaining({ + current_account: "5000", + period: "2024-12-31", + }), + }), + }), + "accountTemplate" + ); + }); + + it("should fetch account template custom and results", async () => { + await testGenerator(mockAccountUrl, mockTestName); + + // Verify custom fetch + expect(SF.getAccountTemplateCustom).toHaveBeenCalledWith( + "firm", + mockAccountParameters.firmId, + mockAccountParameters.companyId, + mockAccountParameters.ledgerId, + mockAccountResponse.account.id + ); + + // Verify results fetch + expect(SF.getAccountTemplateResults).toHaveBeenCalledWith( + "firm", + mockAccountParameters.firmId, + mockAccountParameters.companyId, + mockAccountParameters.ledgerId, + mockAccountResponse.account.id + ); + + // Verify account data structure in test object + expect(Utils.exportYAML).toHaveBeenCalledWith( + mockAccountTemplateHandle, + expect.objectContaining({ + [mockTestName]: expect.objectContaining({ + data: expect.objectContaining({ + periods: expect.objectContaining({ + "2024-12-31": expect.objectContaining({ + accounts: expect.objectContaining({ + 5000: expect.objectContaining({ + name: "Test Account", + value: 12345.67, + custom: expect.objectContaining({ + "account_namespace.account_key": "account_value", + }), + }), + }), + }), + }), + }), + expectation: expect.objectContaining({ + results: expect.objectContaining({ + account_result1: "value1", + account_result2: "value2", + }), + }), + }), + }), + "accountTemplate" + ); + }); + + it("should use starred status from account response", async () => { + const starredAccountResponse = { + ...mockAccountResponse, + starred: true, + }; + SF.findAccountByNumber.mockResolvedValue(starredAccountResponse); + + await testGenerator(mockAccountUrl, mockTestName); + + // The starred status should be extracted from response (not from workflow lookup) + expect(SF.findReconciliationInWorkflow).not.toHaveBeenCalled(); + }); + + it("should skip dependency resolution for account templates", async () => { + await testGenerator(mockAccountUrl, mockTestName); + + // Verify shared parts are NOT searched (dependency resolution is skipped) + expect(SharedPart.read).not.toHaveBeenCalled(); + + // Verify the test object does not contain reconciliation dependencies + // (only account data should be present) + expect(Utils.exportYAML).toHaveBeenCalledWith( + mockAccountTemplateHandle, + expect.objectContaining({ + [mockTestName]: expect.objectContaining({ + data: expect.objectContaining({ + periods: expect.objectContaining({ + "2024-12-31": expect.objectContaining({ + accounts: expect.any(Object), + // Should not have reconciliations object + }), + }), + }), + }), + }), + "accountTemplate" + ); + }); + + it("should handle missing account template association gracefully", async () => { + const accountWithoutTemplate = { + ...mockAccountResponse, + account_reconciliation_template: null, + }; + SF.findAccountByNumber.mockResolvedValue(accountWithoutTemplate); + + await expect(testGenerator(mockAccountUrl, mockTestName)).rejects.toThrow("Process.exit called with code 1"); + expect(consola.error).toHaveBeenCalledWith(expect.stringContaining("No account template associated with account")); + }); + + it("should handle missing account template file gracefully", async () => { + AccountTemplate.read.mockResolvedValue(false); + + await expect(testGenerator(mockAccountUrl, mockTestName)).rejects.toThrow("Process.exit called with code undefined"); + expect(consola.warn).toHaveBeenCalledWith(`Template "${mockAccountTemplateHandle}" wasn't found`); + }); + + it("should handle account lookup errors gracefully", async () => { + SF.findAccountByNumber.mockRejectedValue(new Error("Account not found")); + + await expect(testGenerator(mockAccountUrl, mockTestName)).rejects.toThrow("Process.exit called with code 1"); + expect(consola.error).toHaveBeenCalledWith(expect.stringContaining("Failed to get account template details")); + }); + + it("should process period custom data for account templates", async () => { + await testGenerator(mockAccountUrl, mockTestName); + + expect(Utils.exportYAML).toHaveBeenCalledWith( + mockAccountTemplateHandle, + expect.objectContaining({ + [mockTestName]: expect.objectContaining({ + data: expect.objectContaining({ + periods: expect.objectContaining({ + "2024-12-31": expect.objectContaining({ + custom: expect.objectContaining({ + "pit_integration.code_1002": "yes", + }), + }), + }), + }), + }), + }), + "accountTemplate" + ); + }); + + it("should handle empty period custom data for account templates", async () => { + SF.getAllPeriodCustom.mockResolvedValue([]); + + await testGenerator(mockAccountUrl, mockTestName); + + // Should not add custom data if empty + expect(Utils.exportYAML).toHaveBeenCalledWith( + mockAccountTemplateHandle, + expect.objectContaining({ + [mockTestName]: expect.objectContaining({ + data: expect.objectContaining({ + periods: expect.objectContaining({ + "2024-12-31": expect.not.objectContaining({ + custom: expect.anything(), + }), + }), + }), + }), + }), + "accountTemplate" + ); + }); + }); }); }); From 4e262479f74ad2f32769398110bd7dd3dea97312 Mon Sep 17 00:00:00 2001 From: Michiel Degezelle Date: Wed, 28 Jan 2026 10:13:40 +0100 Subject: [PATCH 6/7] Review comments --- CHANGELOG.md | 3 +++ lib/api/sfApi.js | 3 ++- lib/liquidTestGenerator.js | 5 +++-- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f2abfa7..75edd00 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,9 @@ All notable changes to this project will be documented in this file. +## [1.53.0] (28/01/2026) +Added `create-test` command support for account templates (fetches template data, period data, and custom data). + ## [1.52.0] (12/01/2026) This update improves test execution performance when running tests with status checks across multiple template handles. Tests are now run in parallel for multiple handles when using the `--status` flag, significantly reducing the overall execution time. Previously, tests with status checks for multiple handles would run sequentially, but now they leverage parallel processing for better efficiency. This change only affects the `silverfin run-test` command when both multiple handles and the status flag are used together. diff --git a/lib/api/sfApi.js b/lib/api/sfApi.js index 2d24f55..6e54717 100644 --- a/lib/api/sfApi.js +++ b/lib/api/sfApi.js @@ -662,7 +662,8 @@ async function findAccountByNumber(firmId, companyId, periodId, accountNumber, p // Not found in this page, try next page return findAccountByNumber(firmId, companyId, periodId, accountNumber, page + 1); } catch (error) { - apiUtils.responseErrorHandler(error); + const response = await apiUtils.responseErrorHandler(error); + return response; } } diff --git a/lib/liquidTestGenerator.js b/lib/liquidTestGenerator.js index f4f6ced..102a1df 100644 --- a/lib/liquidTestGenerator.js +++ b/lib/liquidTestGenerator.js @@ -61,7 +61,7 @@ async function testGenerator(url, testName, reconciledStatus = true) { switch (templateType) { case "reconciliationText": starredStatus = { - ...SF.findReconciliationInWorkflow(parameters.firmId, templateHandle, parameters.companyId, parameters.ledgerId, parameters.workflowId), + ...(await SF.findReconciliationInWorkflow(parameters.firmId, templateHandle, parameters.companyId, parameters.ledgerId, parameters.workflowId)), }.starred; break; case "accountTemplate": @@ -95,7 +95,7 @@ async function testGenerator(url, testName, reconciledStatus = true) { const periodCustoms = Utils.processCustom(allPeriodCustoms); liquidTestObject[testName].data.periods[currentPeriodData.fiscal_year.end_date].custom = periodCustoms; } - process.exit; + process.exit(1); // Get all the text properties (customs) and results from current template switch (templateType) { @@ -121,6 +121,7 @@ async function testGenerator(url, testName, reconciledStatus = true) { name: responseDetails.account.name, value: Number(responseDetails.value), custom: currentAccountTemplateCustom, + starred: starredStatus, }, }; From 0c4daaeb415d5647f256fc944bb2d7fbce27a25c Mon Sep 17 00:00:00 2001 From: Michiel Degezelle Date: Wed, 28 Jan 2026 10:43:12 +0100 Subject: [PATCH 7/7] Bump version --- lib/liquidTestGenerator.js | 1 - package-lock.json | 194 ++++++++++++++++++------------------- package.json | 2 +- 3 files changed, 98 insertions(+), 99 deletions(-) diff --git a/lib/liquidTestGenerator.js b/lib/liquidTestGenerator.js index 102a1df..9d6ac11 100644 --- a/lib/liquidTestGenerator.js +++ b/lib/liquidTestGenerator.js @@ -95,7 +95,6 @@ async function testGenerator(url, testName, reconciledStatus = true) { const periodCustoms = Utils.processCustom(allPeriodCustoms); liquidTestObject[testName].data.periods[currentPeriodData.fiscal_year.end_date].custom = periodCustoms; } - process.exit(1); // Get all the text properties (customs) and results from current template switch (templateType) { diff --git a/package-lock.json b/package-lock.json index 687d65f..a15f7da 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "silverfin-cli", - "version": "1.52.0", + "version": "1.53.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "silverfin-cli", - "version": "1.52.0", + "version": "1.53.0", "license": "MIT", "dependencies": { "axios": "^1.6.2", @@ -30,13 +30,13 @@ } }, "node_modules/@babel/code-frame": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", - "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.28.6.tgz", + "integrity": "sha512-JYgintcMjRiCvS8mMECzaEn+m3PfoQiyqukOMCCVQtoJGYJw8j/8LBJEiqkHLkfwCcs74E3pbAUFNg7d9VNJ+Q==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-validator-identifier": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" }, @@ -45,9 +45,9 @@ } }, "node_modules/@babel/compat-data": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.5.tgz", - "integrity": "sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.6.tgz", + "integrity": "sha512-2lfu57JtzctfIrcGMz992hyLlByuzgIk58+hhGCxjKZ3rWI82NnVLjXcaTqkI2NvlcvOskZaiZ5kjUALo3Lpxg==", "dev": true, "license": "MIT", "engines": { @@ -55,21 +55,21 @@ } }, "node_modules/@babel/core": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz", - "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.6.tgz", + "integrity": "sha512-H3mcG6ZDLTlYfaSNi0iOKkigqMFvkTKlGUYlD8GW7nNOYRrevuA46iTypPyv+06V3fEmvvazfntkBU34L0azAw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.28.5", - "@babel/helper-compilation-targets": "^7.27.2", - "@babel/helper-module-transforms": "^7.28.3", - "@babel/helpers": "^7.28.4", - "@babel/parser": "^7.28.5", - "@babel/template": "^7.27.2", - "@babel/traverse": "^7.28.5", - "@babel/types": "^7.28.5", + "@babel/code-frame": "^7.28.6", + "@babel/generator": "^7.28.6", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", @@ -86,14 +86,14 @@ } }, "node_modules/@babel/generator": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.5.tgz", - "integrity": "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.6.tgz", + "integrity": "sha512-lOoVRwADj8hjf7al89tvQ2a1lf53Z+7tiXMgpZJL3maQPDxh0DgLMN62B2MKUOFcoodBHLMbDM6WAbKgNy5Suw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/parser": "^7.28.5", - "@babel/types": "^7.28.5", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" @@ -103,13 +103,13 @@ } }, "node_modules/@babel/helper-compilation-targets": { - "version": "7.27.2", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", - "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/compat-data": "^7.27.2", + "@babel/compat-data": "^7.28.6", "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", @@ -130,29 +130,29 @@ } }, "node_modules/@babel/helper-module-imports": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", - "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/traverse": "^7.27.1", - "@babel/types": "^7.27.1" + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-module-transforms": { - "version": "7.28.3", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz", - "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-module-imports": "^7.27.1", - "@babel/helper-validator-identifier": "^7.27.1", - "@babel/traverse": "^7.28.3" + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -162,9 +162,9 @@ } }, "node_modules/@babel/helper-plugin-utils": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", - "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", "dev": true, "license": "MIT", "engines": { @@ -202,27 +202,27 @@ } }, "node_modules/@babel/helpers": { - "version": "7.28.4", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz", - "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.6.tgz", + "integrity": "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/template": "^7.27.2", - "@babel/types": "^7.28.4" + "@babel/template": "^7.28.6", + "@babel/types": "^7.28.6" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/parser": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz", - "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.6.tgz", + "integrity": "sha512-TeR9zWR18BvbfPmGbLampPMW+uW1NZnJlRuuHso8i87QZNq2JRF9i6RgxRqtEq+wQGsS19NNTWr2duhnE49mfQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.28.5" + "@babel/types": "^7.28.6" }, "bin": { "parser": "bin/babel-parser.js" @@ -287,13 +287,13 @@ } }, "node_modules/@babel/plugin-syntax-import-attributes": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.27.1.tgz", - "integrity": "sha512-oFT0FrKHgF53f4vOsZGi2Hh3I35PfSmVs4IBFLFj4dnafP+hIWDLg3VyKmUHfLoLHlyxY4C7DGtmHuJgn+IGww==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.28.6.tgz", + "integrity": "sha512-jiLC0ma9XkQT3TKJ9uYvlakm66Pamywo+qwL+oL8HJOvc6TWdZXVfhqJr8CCzbSGUAbDOzlGHJC1U+vRfLQDvw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-plugin-utils": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -329,13 +329,13 @@ } }, "node_modules/@babel/plugin-syntax-jsx": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.27.1.tgz", - "integrity": "sha512-y8YTNIeKoyhGd9O0Jiyzyyqk8gdjnumGTQPsz0xOZOQ2RmkVJeZ1vmmfIvFEKqucBG6axJGBZDE/7iI5suUI/w==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.28.6.tgz", + "integrity": "sha512-wgEmr06G6sIpqr8YDwA2dSRTE3bJ+V0IfpzfSY3Lfgd7YWOaAdlykvJi13ZKBt8cZHfgH1IXN+CL656W3uUa4w==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-plugin-utils": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -455,13 +455,13 @@ } }, "node_modules/@babel/plugin-syntax-typescript": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.27.1.tgz", - "integrity": "sha512-xfYCBMxveHrRMnAWl1ZlPXOZjzkN82THFvLhQhFXFt81Z5HnN+EtUkZhv/zcKpmT3fzmWZB0ywiBrbC3vogbwQ==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.28.6.tgz", + "integrity": "sha512-+nDNmQye7nlnuuHDboPbGm00Vqg3oO8niRRL27/4LYHUsHYh0zJ1xWOz0uRwNFmM1Avzk8wZbc6rdiYhomzv/A==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-plugin-utils": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -471,33 +471,33 @@ } }, "node_modules/@babel/template": { - "version": "7.27.2", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", - "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.27.1", - "@babel/parser": "^7.27.2", - "@babel/types": "^7.27.1" + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/traverse": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.5.tgz", - "integrity": "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.6.tgz", + "integrity": "sha512-fgWX62k02qtjqdSNTAGxmKYY/7FSL9WAS1o2Hu5+I5m9T0yxZzr4cnrfXQ/MX0rIifthCSs6FKTlzYbJcPtMNg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.28.5", + "@babel/code-frame": "^7.28.6", + "@babel/generator": "^7.28.6", "@babel/helper-globals": "^7.28.0", - "@babel/parser": "^7.28.5", - "@babel/template": "^7.27.2", - "@babel/types": "^7.28.5", + "@babel/parser": "^7.28.6", + "@babel/template": "^7.28.6", + "@babel/types": "^7.28.6", "debug": "^4.3.1" }, "engines": { @@ -505,9 +505,9 @@ } }, "node_modules/@babel/types": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz", - "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.6.tgz", + "integrity": "sha512-0ZrskXVEHSWIqZM/sQZ4EV3jZJXRkio/WCxaqKZP1g//CEWEPSfeZFcms4XeKBCHU0ZKnIkdJeU/kF+eRp5lBg==", "dev": true, "license": "MIT", "dependencies": { @@ -1265,9 +1265,9 @@ } }, "node_modules/@types/node": { - "version": "25.0.6", - "resolved": "https://registry.npmjs.org/@types/node/-/node-25.0.6.tgz", - "integrity": "sha512-NNu0sjyNxpoiW3YuVFfNz7mxSQ+S4X2G28uqg2s+CzoqoQjLPsWSbsFFyztIAqt2vb8kfEAsJNepMGPTxFDx3Q==", + "version": "25.0.10", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.0.10.tgz", + "integrity": "sha512-zWW5KPngR/yvakJgGOmZ5vTBemDoSqF3AcV/LrO5u5wTWyEAVVh+IT39G4gtyAkh3CtTZs8aX/yRM82OfzHJRg==", "dev": true, "license": "MIT", "dependencies": { @@ -1413,9 +1413,9 @@ "license": "MIT" }, "node_modules/axios": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz", - "integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==", + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.4.tgz", + "integrity": "sha512-1wVkUaAO6WyaYtCkcYCOx12ZgpGf9Zif+qXa4n+oYzK558YryKqiL6UWwd5DqiH3VRW0GYhTZQ/vlgJrCoNQlg==", "license": "MIT", "dependencies": { "follow-redirects": "^1.15.6", @@ -1561,9 +1561,9 @@ "license": "MIT" }, "node_modules/baseline-browser-mapping": { - "version": "2.9.14", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.14.tgz", - "integrity": "sha512-B0xUquLkiGLgHhpPBqvl7GWegWBUNuujQ6kXd/r1U38ElPT6Ok8KZ8e+FpUGEc2ZoRQUzq/aUnaKFc/svWUGSg==", + "version": "2.9.18", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.18.tgz", + "integrity": "sha512-e23vBV1ZLfjb9apvfPk4rHVu2ry6RIr2Wfs+O324okSidrX7pTAnEJPCh/O5BtRlr7QtZI7ktOP3vsqr7Z5XoA==", "dev": true, "license": "Apache-2.0", "bin": { @@ -1690,9 +1690,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001764", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001764.tgz", - "integrity": "sha512-9JGuzl2M+vPL+pz70gtMF9sHdMFbY9FJaQBi186cHKH3pSzDvzoUJUPV6fqiKIMyXbud9ZLg4F3Yza1vJ1+93g==", + "version": "1.0.30001766", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001766.tgz", + "integrity": "sha512-4C0lfJ0/YPjJQHagaE9x2Elb69CIqEPZeG0anQt9SIvIoOH4a4uaRl73IavyO+0qZh6MDLH//DrXThEYKHkmYA==", "dev": true, "funding": [ { @@ -2031,9 +2031,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.5.267", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.267.tgz", - "integrity": "sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==", + "version": "1.5.279", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.279.tgz", + "integrity": "sha512-0bblUU5UNdOt5G7XqGiJtpZMONma6WAfq9vsFmtn9x1+joAObr6x1chfqyxFSDCAFwFhCQDrqeAr6MYdpwJ9Hg==", "dev": true, "license": "ISC" }, diff --git a/package.json b/package.json index 2f115c6..6f4d3c0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "silverfin-cli", - "version": "1.52.0", + "version": "1.53.0", "description": "Command line tool for Silverfin template development", "main": "index.js", "license": "MIT",