# keyValueUpsertMode 與陣列合併策略
# keyValueUpsertMode with Array Merge Strategies

## 概述

本文件說明 `keyValueUpsertMode` 選項與不同陣列合併策略的組合使用方式，並提供實用場景範例。

This document explains how to combine `keyValueUpsertMode` option with different array merge strategies, with practical examples.

---

## 陣列合併策略

deepmerge 支援多種陣列合併策略，透過 `arrayMerge` 選項自訂。

deepmerge supports multiple array merge strategies through the `arrayMerge` option.

### 策略比較表

| 策略 | 函式 | 行為 | 適用情境 |
|------|------|------|---------|
| **預設串接** | （不指定） | `[...target, ...source]` | 兩者合併，保留所有元素 |
| **左邊為主** | `arrayMergeTargetWins` | `target` | 保留預設值，忽略新資料 |
| **右邊為主** | `arrayMergeSourceWins` | `source` | 完全覆蓋，使用新資料 |

### 策略程式碼

```typescript
/**
 * 預設串接：合併兩邊陣列
 * Default concat: Merge both arrays
 */
function defaultArrayMerge(target: any[], source: any[]): any[]
{
	return target.concat(source);
}

/**
 * 左邊為主：完全保留目標陣列
 * Left primary: Keep target array completely
 */
function arrayMergeTargetWins(target: any[], source: any[]): any[]
{
	return target;
}

/**
 * 右邊為主：完全使用來源陣列
 * Right primary: Use source array completely
 */
function arrayMergeSourceWins(target: any[], source: any[]): any[]
{
	return source;
}
```

---

## 與 keyValueUpsertMode 搭配

### keyValueUpsertMode: true 的作用

當 `keyValueUpsertMode: true` 時：
- 如果目標值不是 `undefined`，保留目標值
- 如果目標值是 `undefined`，使用來源值
- 對於陣列，這會影響整個陣列是否被採用

When `keyValueUpsertMode: true`:
- If target value is not `undefined`, keep target value
- If target value is `undefined`, use source value
- For arrays, this affects whether the entire array is used

### 三種策略對比

```typescript
const target = {
	tags: ['a', 'b', 'c'],
	name: 'Alice',
};

const source = {
	tags: ['x', 'y', 'z'],
	name: 'Bob',
};

// 三種策略結果
const resultTargetWins = merge(target, source, {
	keyValueUpsertMode: true,
	arrayMerge: arrayMergeTargetWins,
});
// tags = ['a', 'b', 'c']  (左邊為主-目標)
// name = 'Alice'          (keyValueUpsertMode 保留)

const resultSourceWins = merge(target, source, {
	keyValueUpsertMode: true,
	arrayMerge: arrayMergeSourceWins,
});
// tags = ['x', 'y', 'z']  (右邊為主-來源)
// name = 'Alice'          (keyValueUpsertMode 保留)

const resultConcat = merge(target, source, {
	keyValueUpsertMode: true,
});
// tags = ['a', 'b', 'c', 'x', 'y', 'z']  (預設串接)
// name = 'Alice'                           (keyValueUpsertMode 保留)
```

### 結果對照表

| 策略 | `tags` 結果 | `name` 結果 |
|------|-------------|-------------|
| `arrayMergeTargetWins` | `['a', 'b', 'c']` | `'Alice'` |
| `arrayMergeSourceWins` | `['x', 'y', 'z']` | `'Alice'` |
| 預設串接 | `['a', 'b', 'c', 'x', 'y', 'z']` | `'Alice'` |

---

## 實用場景

### 場景 1：使用者配置與預設配置合併

```typescript
/**
 * 情境：合併使用者配置與預設配置
 * - 使用者已設定的值應該被保留
 * - 新的設定應該被添加
 * - 陣列（如外掛清單）應該完全替換而非合併
 */
const defaultConfig = {
	plugins: ['analytics', 'seo', 'security'],
	theme: 'light',
	language: 'en',
};

const userConfig = {
	plugins: ['custom-plugin'],
	theme: 'dark',
};

// 使用 arrayMergeSourceWins：使用者的外掛清單完全替換預設清單
const result = merge(defaultConfig, userConfig, {
	keyValueUpsertMode: true,
	arrayMerge: arrayMergeSourceWins,
});

// 結果：
// {
//   plugins: ['custom-plugin'],  // 完全替換
//   theme: 'light',             // 保留預設（keyValueUpsertMode）
//   language: 'en'             // 新增鍵
// }
```

### 場景 2：API 回應與快取合併

```typescript
/**
 * 情境：將 API 回應與本地快取合併
 * - 已經存在的資料應該被保留（以快取為準）
 * - 新資料應該被添加
 * - 歷史記錄陣列應該完全替換（只看最新）
 */
const cache = {
	history: ['action-1', 'action-2', 'action-3'],
	profile: {
		name: 'Cached User',
		updatedAt: '2024-01-01',
	},
};

const apiResponse = {
	history: ['action-4'],
	profile: {
		name: 'Cached User',
		updatedAt: '2024-01-15',
	},
};

const result = merge(cache, apiResponse, {
	keyValueUpsertMode: true,
	arrayMerge: arrayMergeSourceWins,
});

// 結果：
// {
//   history: ['action-4'],         // 完全替換（只看最新）
//   profile: {
//     name: 'Cached User',          // 保留快取值（keyValueUpsertMode）
//     updatedAt: '2024-01-01',      // 保留舊時間（keyValueUpsertMode）
//   },
// }
```

### 場景 3：表單預設值填充

```typescript
/**
 * 情境：用預設值填充表單
 * - 使用者已填寫的值應該被保留
 * - 未填寫的欄位使用預設值
 * - 選項陣列應該完全替換（多選題）
 */
const defaultValues = {
	interests: ['reading', 'music', 'sports'],
	notifications: ['email', 'sms'],
	name: undefined,
};

const userInput = {
	interests: ['coding'],
	notifications: ['push'],
	name: 'John',
};

const result = merge(defaultValues, userInput, {
	keyValueUpsertMode: true,
	arrayMerge: arrayMergeSourceWins,
});

// 結果：
// {
//   interests: ['coding'],    // 完全替換
//   notifications: ['push'],   // 完全替換
//   name: 'John'             // 使用使用者輸入（因為預設是 undefined）
// }
```

### 場景 4：預設值優先

```typescript
/**
 * 情境：系統預設值應該優先於使用者輸入
 * - 某些關鍵設定不允許使用者修改
 * - 使用 arrayMergeTargetWins 確保預設值不被覆蓋
 */
const systemDefaults = {
	allowedPlugins: ['official-plugin'],
	maxUploadSize: 100,
	features: ['feature-a', 'feature-b'],
};

const userSettings = {
	allowedPlugins: ['hacked-plugin'], // 嘗試添加惡意插件
	maxUploadSize: 10000,              // 嘗試增加上傳限制
	features: ['feature-c'],           // 嘗試添加功能
};

// 使用 arrayMergeTargetWins：系統預設值優先
const result = merge(systemDefaults, userSettings, {
	keyValueUpsertMode: true,
	arrayMerge: arrayMergeTargetWins,
});

// 結果：
// {
//   allowedPlugins: ['official-plugin'],  // 保持預設
//   maxUploadSize: 100,                  // 保持預設
//   features: ['feature-a', 'feature-b'], // 保持預設
// }
```

---

## 巢狀物件中的陣列行為

所有策略都會遞迴應用於巢狀物件中的陣列。

All strategies are recursively applied to arrays in nested objects.

```typescript
const target = {
	config: {
		plugins: ['plugin-a', 'plugin-b'],
		modules: ['mod-1', 'mod-2'],
	},
};

const source = {
	config: {
		plugins: ['plugin-c'],
		modules: ['mod-3'],
	},
};

const result = merge(target, source, {
	keyValueUpsertMode: true,
	arrayMerge: arrayMergeSourceWins,
});

// 巢狀陣列也會被替換：
// {
//   config: {
//     plugins: ['plugin-c'],         // 完全替換
//     modules: ['mod-3'],           // 完全替換
//   },
// }
```

---

## 注意事項

### 1. keyValueUpsertMode 與陣列的互動

當目標陣列為 `undefined` 時：
- `keyValueUpsertMode: true` 會使用來源陣列
- 之後再由 `arrayMerge` 策略決定合併方式

When target array is `undefined`:
- `keyValueUpsertMode: true` will use source array
- Then `arrayMerge` strategy decides how to merge

### 2. 空陣列的處理

| 情境 | `arrayMergeNoConcat` | `arrayMergeTargetWins` | 預設串接 |
|------|---------------------|----------------------|---------|
| target=`[]`, source=`['a']` | `['a']` | `[]` | `['a']` |
| target=`['a']`, source=`[]` | `[]` | `['a']` | `['a']` |
| target=`[]`, source=`[]` | `[]` | `[]` | `[]` |

### 3. 效能考量

- `arrayMergeTargetWins` 和 `arrayMergeSourceWins` 最快（直接回傳）
- `arrayMergeNoConcat` 次之（需複製來源陣列）
- 預設串接最慢（需合併兩個陣列）

---

## 相關檔案

- `test/options/key-value-upsert-mode-no-array-merge.test.ts` - 測試檔案
- `src/index.ts` - 核心實作
- `docs/key-value-upsert-mode-analysis.md` - keyValueUpsertMode 分析文件

---

## 更新日誌

| 日期 | 更新內容 |
|------|---------|
| 2026-03-26 | 新增 keyValueUpsertMode 與陣列合併策略文件 |
