From 03befa4792b706adb377a51a4471864210337439 Mon Sep 17 00:00:00 2001 From: Dan Lynch Date: Thu, 22 Jan 2026 07:18:45 +0000 Subject: [PATCH] refactor(env): change array merge strategy from unique to replace Changes the deep merge array strategy from mergeArraysUnique (which concatenated and deduplicated arrays) to replaceArrays (which allows later configuration sources to completely override earlier ones). This fixes the issue where arrays like api.metaSchemas with non-empty defaults could not be fully replaced by config/env/override values. - Rename mergeArraysUnique to replaceArrays in pgpm/env/src/utils.ts - Update imports in pgpm/env and graphql/env merge.ts files - Update tests to reflect new replace behavior - Update snapshots --- .../__snapshots__/merge.test.ts.snap | 7 ----- graphql/env/__tests__/merge.test.ts | 28 ++++-------------- graphql/env/src/merge.ts | 4 +-- pgpm/env/__tests__/merge.test.ts | 29 +++++-------------- pgpm/env/src/index.ts | 2 +- pgpm/env/src/merge.ts | 4 +-- pgpm/env/src/utils.ts | 11 ++++--- 7 files changed, 25 insertions(+), 60 deletions(-) diff --git a/graphql/env/__tests__/__snapshots__/merge.test.ts.snap b/graphql/env/__tests__/__snapshots__/merge.test.ts.snap index 02c79c085..636127402 100644 --- a/graphql/env/__tests__/__snapshots__/merge.test.ts.snap +++ b/graphql/env/__tests__/__snapshots__/merge.test.ts.snap @@ -12,10 +12,6 @@ exports[`getEnvOptions merges pgpm defaults, graphql defaults, config, env, and ], "isPublic": true, "metaSchemas": [ - "config_meta", - "services_public", - "metaschema_public", - "metaschema_modules_public", "env_meta1", "env_meta2", ], @@ -75,9 +71,6 @@ exports[`getEnvOptions merges pgpm defaults, graphql defaults, config, env, and "graphileBuildOptions": {}, "overrideSettings": {}, "schema": [ - "config_schema", - "env_schema_a", - "env_schema_b", "override_schema", ], }, diff --git a/graphql/env/__tests__/merge.test.ts b/graphql/env/__tests__/merge.test.ts index 4cc6e240f..edbc3f6df 100644 --- a/graphql/env/__tests__/merge.test.ts +++ b/graphql/env/__tests__/merge.test.ts @@ -84,8 +84,8 @@ describe('getEnvOptions', () => { expect(result).toMatchSnapshot(); }); - it('dedupes graphql array fields across config, env, and overrides', () => { - tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'graphql-env-dedupe-')); + it('replaces graphql array fields with later values (overrides win)', () => { + tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'graphql-env-replace-')); writeConfig(tempDir, { graphile: { schema: ['config_schema', 'shared_schema'] @@ -116,25 +116,9 @@ describe('getEnvOptions', () => { testEnv ); - expect(result.graphile?.schema).toEqual([ - 'config_schema', - 'shared_schema', - 'env_schema', - 'override_schema' - ]); - expect(result.api?.exposedSchemas).toEqual([ - 'public', - 'shared', - 'env_schema', - 'override_schema' - ]); - expect(result.api?.metaSchemas).toEqual([ - 'metaschema_public', - 'services_public', - 'config_meta', - 'metaschema_modules_public', - 'env_meta', - 'override_meta' - ]); + // Arrays are replaced, not merged - overrides win completely + expect(result.graphile?.schema).toEqual(['override_schema', 'shared_schema']); + expect(result.api?.exposedSchemas).toEqual(['public', 'override_schema']); + expect(result.api?.metaSchemas).toEqual(['env_meta', 'override_meta']); }); }); diff --git a/graphql/env/src/merge.ts b/graphql/env/src/merge.ts index c9e1e235a..669c75269 100644 --- a/graphql/env/src/merge.ts +++ b/graphql/env/src/merge.ts @@ -1,6 +1,6 @@ import deepmerge from 'deepmerge'; import { ConstructiveOptions, constructiveGraphqlDefaults } from '@constructive-io/graphql-types'; -import { getEnvOptions as getPgpmEnvOptions, loadConfigSync, mergeArraysUnique } from '@pgpmjs/env'; +import { getEnvOptions as getPgpmEnvOptions, loadConfigSync, replaceArrays } from '@pgpmjs/env'; import { getGraphQLEnvVars } from './env'; /** @@ -47,7 +47,7 @@ export const getEnvOptions = ( graphqlEnvOptions, overrides ], { - arrayMerge: mergeArraysUnique + arrayMerge: replaceArrays }) as ConstructiveOptions; }; diff --git a/pgpm/env/__tests__/merge.test.ts b/pgpm/env/__tests__/merge.test.ts index f6db3d26e..c880841c5 100644 --- a/pgpm/env/__tests__/merge.test.ts +++ b/pgpm/env/__tests__/merge.test.ts @@ -180,8 +180,8 @@ describe('getEnvOptions', () => { expect(result).toMatchSnapshot(); }); - it('dedupes array fields across config, env, and overrides', () => { - tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'pgpm-env-dedupe-')); + it('replaces array fields with later values (overrides win)', () => { + tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'pgpm-env-replace-')); writeConfig(tempDir, { db: { extensions: ['uuid', 'postgis'] @@ -219,25 +219,10 @@ describe('getEnvOptions', () => { const result = getEnvOptions(overrides, tempDir, testEnv) as PgpmOptionsWithPackages; - expect(result.db?.extensions).toEqual([ - 'uuid', - 'postgis', - 'pgcrypto', - 'hstore' - ]); - expect(result.jobs?.worker?.supported).toEqual([ - 'alpha', - 'beta', - 'gamma', - 'delta', - 'epsilon' - ]); - expect(result.jobs?.scheduler?.supported).toEqual([ - 'beta', - 'gamma', - 'delta', - 'zeta' - ]); - expect(result.packages).toEqual(['testing/*', 'packages/*', 'extensions/*']); + // Arrays are replaced, not merged - overrides win completely + expect(result.db?.extensions).toEqual(['uuid', 'hstore']); + expect(result.jobs?.worker?.supported).toEqual(['delta', 'epsilon']); + expect(result.jobs?.scheduler?.supported).toEqual(['gamma', 'zeta']); + expect(result.packages).toEqual(['testing/*', 'extensions/*']); }); }); diff --git a/pgpm/env/src/index.ts b/pgpm/env/src/index.ts index 66fb144f2..9ec47ea63 100644 --- a/pgpm/env/src/index.ts +++ b/pgpm/env/src/index.ts @@ -11,6 +11,6 @@ export { } from './config'; export type { WorkspaceType } from './config'; export { getEnvVars, getNodeEnv, parseEnvBoolean, parseEnvNumber } from './env'; -export { walkUp, mergeArraysUnique } from './utils'; +export { walkUp, replaceArrays } from './utils'; export type { PgpmOptions, PgTestConnectionOptions, DeploymentOptions } from '@pgpmjs/types'; diff --git a/pgpm/env/src/merge.ts b/pgpm/env/src/merge.ts index 1e0a08074..d0e920db3 100644 --- a/pgpm/env/src/merge.ts +++ b/pgpm/env/src/merge.ts @@ -2,7 +2,7 @@ import deepmerge from 'deepmerge'; import { pgpmDefaults, PgpmOptions, PgTestConnectionOptions, DeploymentOptions } from '@pgpmjs/types'; import { loadConfigSync } from './config'; import { getEnvVars } from './env'; -import { mergeArraysUnique } from './utils'; +import { replaceArrays } from './utils'; /** * Get core PGPM environment options by merging: @@ -26,7 +26,7 @@ export const getEnvOptions = ( const envOptions = getEnvVars(env); return deepmerge.all([pgpmDefaults, configOptions, envOptions, overrides], { - arrayMerge: mergeArraysUnique + arrayMerge: replaceArrays }); }; diff --git a/pgpm/env/src/utils.ts b/pgpm/env/src/utils.ts index 083be7831..2b8cba957 100644 --- a/pgpm/env/src/utils.ts +++ b/pgpm/env/src/utils.ts @@ -26,11 +26,14 @@ export const walkUp = (startDir: string, filename: string): string => { throw new Error(`File "${filename}" not found in any parent directories.`); }; -export const mergeArraysUnique = ( - target: T[], +/** + * Array merge strategy that replaces target array with source array. + * This allows later configuration sources to completely override earlier ones. + */ +export const replaceArrays = ( + _target: T[], source: T[], _options?: unknown ): T[] => { - const merged = [...target, ...source]; - return [...new Set(merged)]; + return source; };