From 172095540bf429311abdcf7a48b236c4d5a6602d Mon Sep 17 00:00:00 2001 From: David Dal Busco Date: Sat, 2 Aug 2025 17:08:19 +0200 Subject: [PATCH 1/3] feat: apply config when needed (or try to at least) --- src/services/config/config.services.ts | 80 +++++++++++++++++++++++++- src/utils/obj.utils.ts | 18 +++++- 2 files changed, 94 insertions(+), 4 deletions(-) diff --git a/src/services/config/config.services.ts b/src/services/config/config.services.ts index b81e4536..7ce42b89 100644 --- a/src/services/config/config.services.ts +++ b/src/services/config/config.services.ts @@ -75,6 +75,11 @@ export const config = async () => { satelliteConfig }); + if (Object.values(editConfig).filter(nonNullish).length === 0) { + console.log('🤷‍♂️ No configuration changes detected.'); + return; + } + // Effectively update the configurations and collections of the Satellite const results = await applyConfig({satellite, editConfig}); @@ -210,7 +215,10 @@ const getCurrentConfig = async ({ const mapRules = ({items}: ListRulesResults): CurrentCollectionsConfig => items.reduce( - (acc, rule) => ({ + // We trim createdAt and updatedAt because those information are not used for applying and handling the configuration + // but also to generate hashes without those values. This way we can compare if a collection must really but created + // or updated or if already similar to the value defined in the configuration file. + (acc, {createdAt: _, updatedAt: __, ...rule}) => ({ ...acc, [rule.collection]: [rule, objHash(rule)] }), @@ -464,12 +472,78 @@ const prepareConfig = async ({ }; }; + // We want to spare updates if there is no changes to apply + const filterIdenticalConfig = (editConfig: EditConfig): EditConfig => { + const {storage, datastore, authentication, settings, collections} = editConfig; + + const storageHash = currentStorage?.[1]; + const datastoreHash = currentDatastore?.[1]; + const authHash = currentAuth?.[1]; + const settingsHash = currentSettings?.[1]; + + const filterCollections = ({ + collections, + currentCollections + }: { + collections?: Array; + currentCollections?: CurrentCollectionsConfig; + }): Array | undefined => + collections?.filter((rule) => { + const currentHash = currentCollections?.[rule.collection]?.[1]; + + const extendRuleWithDefault = { + ...rule, + ...(!('mutablePermissions' in rule) && {mutablePermissions: true}) + }; + + return nonNullish(currentHash) && currentHash !== objHash(extendRuleWithDefault); + }); + + const storageCollections = filterCollections({ + collections: collections?.storage, + currentCollections: currentStorageCollections + }); + + const datastoreCollections = filterCollections({ + collections: collections?.datastore, + currentCollections: currentDatastoreCollections + }); + + return { + storage: + nonNullish(storageHash) && nonNullish(storage) && storageHash === objHash(storage) + ? undefined + : storage, + datastore: + nonNullish(datastoreHash) && nonNullish(datastore) && datastoreHash === objHash(datastore) + ? undefined + : datastore, + authentication: + nonNullish(authHash) && nonNullish(authentication) && authHash === objHash(authentication) + ? undefined + : authentication, + settings: + nonNullish(settingsHash) && nonNullish(settings) && settingsHash === objHash(settings) + ? undefined + : settings, + ...(((nonNullish(storageCollections) && storageCollections.length > 0) || + (nonNullish(datastoreCollections) && datastoreCollections.length > 0)) && { + collections: { + ...(nonNullish(storageCollections) && + storageCollections.length > 0 && {storage: storageCollections}), + ...(nonNullish(datastoreCollections) && + datastoreCollections.length > 0 && {datastore: datastoreCollections}) + } + }) + }; + }; + const confirmAndExtendWithVersions = async (): Promise => { await confirmAndExit( 'This action will overwrite the current configuration of the Satellite. Are you sure you want to continue?' ); - return extendWithVersions(); + return filterIdenticalConfig(extendWithVersions()); }; if (firstTime) { @@ -544,7 +618,7 @@ const prepareConfig = async ({ }; if (isLastAppliedConfigCurrent()) { - return extendWithVersions(); + return filterIdenticalConfig(extendWithVersions()); } return await confirmAndExtendWithVersions(); diff --git a/src/utils/obj.utils.ts b/src/utils/obj.utils.ts index 65ca2349..f20d7459 100644 --- a/src/utils/obj.utils.ts +++ b/src/utils/obj.utils.ts @@ -1,5 +1,21 @@ import {jsonReplacer} from '@dfinity/utils'; import {createHash} from 'node:crypto'; +const sortReplacer = (_key: string, value: unknown): unknown => + value instanceof Object && !(value instanceof Array) + ? Object.keys(value) + .sort() + .reduce( + (sorted, key) => ({ + ...sorted, + [key]: value[key] + }), + {} + ) + : value; + +const replacers = (key: string, value: unknown): unknown => + [sortReplacer, jsonReplacer].reduce((val, replacer) => replacer(key, val), value); + export const objHash = (obj: unknown): string => - createHash('sha256').update(JSON.stringify(obj, jsonReplacer)).digest('hex'); + createHash('sha256').update(JSON.stringify(obj, replacers)).digest('hex'); From 75a90599a62cee33ac820606d1ad000b9df06169 Mon Sep 17 00:00:00 2001 From: David Dal Busco Date: Sat, 2 Aug 2025 17:11:50 +0200 Subject: [PATCH 2/3] feat: clean rule --- src/services/config/config.services.ts | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/services/config/config.services.ts b/src/services/config/config.services.ts index 7ce42b89..eb8c133b 100644 --- a/src/services/config/config.services.ts +++ b/src/services/config/config.services.ts @@ -118,7 +118,7 @@ const saveLastAppliedConfigHashes = ({ .reduce>( (acc, rule) => ({ ...acc, - [rule.collection]: objHash(rule) + [rule.collection]: ruleHash(rule) }), {} ) @@ -215,12 +215,9 @@ const getCurrentConfig = async ({ const mapRules = ({items}: ListRulesResults): CurrentCollectionsConfig => items.reduce( - // We trim createdAt and updatedAt because those information are not used for applying and handling the configuration - // but also to generate hashes without those values. This way we can compare if a collection must really but created - // or updated or if already similar to the value defined in the configuration file. - (acc, {createdAt: _, updatedAt: __, ...rule}) => ({ + (acc, rule) => ({ ...acc, - [rule.collection]: [rule, objHash(rule)] + [rule.collection]: [rule, ruleHash(rule)] }), {} ); @@ -623,3 +620,9 @@ const prepareConfig = async ({ return await confirmAndExtendWithVersions(); }; + +// We trim `createdAt` and `updatedAt` because they are not used when applying or handling the configuration. +// They are also excluded when generating hashes to ensure comparisons are based only on meaningful changes. +// This allows us to determine whether a collection truly needs to be created or updated, or if it already matches +// the configuration definition. +const ruleHash = ({createdAt: _, updatedAt: __, ...rule}: Rule): string => objHash(rule); From 4c6f104b4d69902b815245fc938037e909f05877 Mon Sep 17 00:00:00 2001 From: David Dal Busco Date: Sat, 2 Aug 2025 17:14:29 +0200 Subject: [PATCH 3/3] chore: max lines --- eslint.config.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eslint.config.mjs b/eslint.config.mjs index 71d91181..2d12b63f 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -32,7 +32,7 @@ export default [ 'no-console': 'off', 'arrow-body-style': 'off', complexity: 'off', - 'max-lines': ['error', 600], + 'max-lines': ['error', 1000], '@typescript-eslint/explicit-function-return-type': 'off', '@typescript-eslint/ban-ts-comment': 'off', '@typescript-eslint/no-misused-promises': 'off',