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', diff --git a/src/services/config/config.services.ts b/src/services/config/config.services.ts index b81e4536..eb8c133b 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}); @@ -113,7 +118,7 @@ const saveLastAppliedConfigHashes = ({ .reduce>( (acc, rule) => ({ ...acc, - [rule.collection]: objHash(rule) + [rule.collection]: ruleHash(rule) }), {} ) @@ -212,7 +217,7 @@ const getCurrentConfig = async ({ items.reduce( (acc, rule) => ({ ...acc, - [rule.collection]: [rule, objHash(rule)] + [rule.collection]: [rule, ruleHash(rule)] }), {} ); @@ -464,12 +469,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,8 +615,14 @@ const prepareConfig = async ({ }; if (isLastAppliedConfigCurrent()) { - return extendWithVersions(); + return filterIdenticalConfig(extendWithVersions()); } 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); 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');