@model-ts/dynamodb 3.0.4 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/dist/cjs/__test__/client-with-cursor-encryption.test.js +1241 -1343
- package/dist/cjs/__test__/client-with-cursor-encryption.test.js.map +1 -1
- package/dist/cjs/__test__/client.test.js +1488 -1458
- package/dist/cjs/__test__/client.test.js.map +1 -1
- package/dist/cjs/__test__/diff.test.d.ts +1 -0
- package/dist/cjs/__test__/diff.test.js +160 -0
- package/dist/cjs/__test__/diff.test.js.map +1 -0
- package/dist/cjs/diff.d.ts +3 -0
- package/dist/cjs/diff.js +339 -0
- package/dist/cjs/diff.js.map +1 -0
- package/dist/cjs/provider.d.ts +149 -19
- package/dist/cjs/provider.js +131 -1
- package/dist/cjs/provider.js.map +1 -1
- package/dist/cjs/sandbox.js +2 -2
- package/dist/cjs/sandbox.js.map +1 -1
- package/dist/cjs/test-utils/setup.d.ts +1 -1
- package/dist/cjs/test-utils/setup.js +11 -5
- package/dist/cjs/test-utils/setup.js.map +1 -1
- package/dist/esm/__test__/client-with-cursor-encryption.test.js +1241 -1343
- package/dist/esm/__test__/client-with-cursor-encryption.test.js.map +1 -1
- package/dist/esm/__test__/client.test.js +1488 -1458
- package/dist/esm/__test__/client.test.js.map +1 -1
- package/dist/esm/__test__/diff.test.d.ts +1 -0
- package/dist/esm/__test__/diff.test.js +158 -0
- package/dist/esm/__test__/diff.test.js.map +1 -0
- package/dist/esm/diff.d.ts +3 -0
- package/dist/esm/diff.js +335 -0
- package/dist/esm/diff.js.map +1 -0
- package/dist/esm/provider.d.ts +149 -19
- package/dist/esm/provider.js +131 -1
- package/dist/esm/provider.js.map +1 -1
- package/dist/esm/sandbox.js +2 -2
- package/dist/esm/sandbox.js.map +1 -1
- package/dist/esm/test-utils/setup.d.ts +1 -1
- package/dist/esm/test-utils/setup.js +11 -5
- package/dist/esm/test-utils/setup.js.map +1 -1
- package/package.json +1 -1
- package/src/__test__/client-with-cursor-encryption.test.ts +1245 -1347
- package/src/__test__/client.test.ts +1584 -1482
- package/src/__test__/diff.test.ts +165 -0
- package/src/diff.ts +443 -0
- package/src/provider.ts +147 -3
- package/src/sandbox.ts +2 -2
- package/src/test-utils/setup.ts +9 -5
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { formatSnapshotDiff } from "../diff"
|
|
2
|
+
|
|
3
|
+
describe("formatSnapshotDiff", () => {
|
|
4
|
+
test("renders added items with nested objects and arrays", () => {
|
|
5
|
+
const before = {}
|
|
6
|
+
const after = {
|
|
7
|
+
"PK#user#1__SK#profile": {
|
|
8
|
+
PK: "PK#user#1",
|
|
9
|
+
SK: "SK#profile",
|
|
10
|
+
name: "Ada",
|
|
11
|
+
stats: {
|
|
12
|
+
flags: ["a", "b"],
|
|
13
|
+
logins: 3
|
|
14
|
+
},
|
|
15
|
+
tags: ["alpha", "beta"],
|
|
16
|
+
connections: [
|
|
17
|
+
{
|
|
18
|
+
user: "1"
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
user: "2",
|
|
22
|
+
type: "friend"
|
|
23
|
+
}
|
|
24
|
+
]
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
expect(formatSnapshotDiff(before, after)).toMatchInlineSnapshot(`
|
|
29
|
+
+ [PK#user#1 / SK#profile]
|
|
30
|
+
+ PK: "PK#user#1"
|
|
31
|
+
+ SK: "SK#profile"
|
|
32
|
+
+ connections:
|
|
33
|
+
+ - user: "1"
|
|
34
|
+
+ - type: "friend"
|
|
35
|
+
+ user: "2"
|
|
36
|
+
+ name: "Ada"
|
|
37
|
+
+ stats:
|
|
38
|
+
+ flags:
|
|
39
|
+
+ - "a"
|
|
40
|
+
+ - "b"
|
|
41
|
+
+ logins: 3
|
|
42
|
+
+ tags:
|
|
43
|
+
+ - "alpha"
|
|
44
|
+
+ - "beta"
|
|
45
|
+
`)
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
test("renders removed items", () => {
|
|
49
|
+
const before = {
|
|
50
|
+
"PK#post#9__SK#meta": {
|
|
51
|
+
PK: "PK#post#9",
|
|
52
|
+
SK: "SK#meta",
|
|
53
|
+
title: "Removed",
|
|
54
|
+
views: 10
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
const after = {}
|
|
58
|
+
|
|
59
|
+
expect(formatSnapshotDiff(before, after)).toMatchInlineSnapshot(`
|
|
60
|
+
- [PK#post#9 / SK#meta]
|
|
61
|
+
- PK: "PK#post#9"
|
|
62
|
+
- SK: "SK#meta"
|
|
63
|
+
- title: "Removed"
|
|
64
|
+
- views: 10
|
|
65
|
+
`)
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
test("renders updated fields with nested diffs", () => {
|
|
69
|
+
const before = {
|
|
70
|
+
"PK#order#1__SK#summary": {
|
|
71
|
+
PK: "PK#order#1",
|
|
72
|
+
SK: "SK#summary",
|
|
73
|
+
status: "pending",
|
|
74
|
+
count: 1,
|
|
75
|
+
meta: {
|
|
76
|
+
flags: ["a", "b", "c"],
|
|
77
|
+
stats: ["a", "b", "c"],
|
|
78
|
+
config: { enabled: true }
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
const after = {
|
|
83
|
+
"PK#order#1__SK#summary": {
|
|
84
|
+
PK: "PK#order#1",
|
|
85
|
+
SK: "SK#summary",
|
|
86
|
+
status: "paid",
|
|
87
|
+
count: 2,
|
|
88
|
+
meta: {
|
|
89
|
+
flags: ["a", "c", "d"],
|
|
90
|
+
stats: ["a", "c"],
|
|
91
|
+
config: { enabled: false, mode: "fast" }
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
expect(formatSnapshotDiff(before, after)).toMatchInlineSnapshot(`
|
|
97
|
+
[PK#order#1 / SK#summary]
|
|
98
|
+
PK: "PK#order#1"
|
|
99
|
+
SK: "SK#summary"
|
|
100
|
+
- count: 1
|
|
101
|
+
+ count: 2
|
|
102
|
+
meta:
|
|
103
|
+
config:
|
|
104
|
+
- enabled: true
|
|
105
|
+
+ enabled: false
|
|
106
|
+
+ mode: "fast"
|
|
107
|
+
flags:
|
|
108
|
+
- "a"
|
|
109
|
+
- - "b"
|
|
110
|
+
- "c"
|
|
111
|
+
+ - "d"
|
|
112
|
+
stats:
|
|
113
|
+
- "a"
|
|
114
|
+
- - "b"
|
|
115
|
+
- "c"
|
|
116
|
+
- status: "pending"
|
|
117
|
+
+ status: "paid"
|
|
118
|
+
`)
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
test("skips unchanged items and keeps output compact", () => {
|
|
122
|
+
const before = {
|
|
123
|
+
"PK#steady__SK#1": {
|
|
124
|
+
PK: "PK#steady",
|
|
125
|
+
SK: "SK#1",
|
|
126
|
+
value: "same"
|
|
127
|
+
},
|
|
128
|
+
"PK#beta__SK#1": {
|
|
129
|
+
PK: "PK#beta",
|
|
130
|
+
SK: "SK#1",
|
|
131
|
+
count: 1
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
const after = {
|
|
135
|
+
"PK#steady__SK#1": {
|
|
136
|
+
PK: "PK#steady",
|
|
137
|
+
SK: "SK#1",
|
|
138
|
+
value: "same"
|
|
139
|
+
},
|
|
140
|
+
"PK#beta__SK#1": {
|
|
141
|
+
PK: "PK#beta",
|
|
142
|
+
SK: "SK#1",
|
|
143
|
+
count: 2
|
|
144
|
+
},
|
|
145
|
+
"PK#alpha__SK#1": {
|
|
146
|
+
PK: "PK#alpha",
|
|
147
|
+
SK: "SK#1",
|
|
148
|
+
flag: true
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
expect(formatSnapshotDiff(before, after)).toMatchInlineSnapshot(`
|
|
153
|
+
+ [PK#alpha / SK#1]
|
|
154
|
+
+ PK: "PK#alpha"
|
|
155
|
+
+ SK: "SK#1"
|
|
156
|
+
+ flag: true
|
|
157
|
+
|
|
158
|
+
[PK#beta / SK#1]
|
|
159
|
+
PK: "PK#beta"
|
|
160
|
+
SK: "SK#1"
|
|
161
|
+
- count: 1
|
|
162
|
+
+ count: 2
|
|
163
|
+
`)
|
|
164
|
+
})
|
|
165
|
+
})
|
package/src/diff.ts
ADDED
|
@@ -0,0 +1,443 @@
|
|
|
1
|
+
const INDENT_SIZE = 2
|
|
2
|
+
const ITEM_FIELD_INDENT = 4
|
|
3
|
+
const PRIORITY_FIELDS = ["PK", "SK"]
|
|
4
|
+
const PRIORITY_FIELD_SET = new Set(PRIORITY_FIELDS)
|
|
5
|
+
const ARRAY_ITEM_KEY = "[]"
|
|
6
|
+
|
|
7
|
+
type Snapshot = Record<string, any>
|
|
8
|
+
type RenderResult = { lines: string[]; changed: boolean }
|
|
9
|
+
|
|
10
|
+
const indent = (level: number) => " ".repeat(level)
|
|
11
|
+
|
|
12
|
+
const isPlainObject = (value: any): value is Record<string, any> =>
|
|
13
|
+
value !== null && typeof value === "object" && !Array.isArray(value)
|
|
14
|
+
|
|
15
|
+
const orderKeys = (keys: string[]) => {
|
|
16
|
+
const uniqueKeys = Array.from(new Set(keys))
|
|
17
|
+
const prioritized = PRIORITY_FIELDS.filter((key) => uniqueKeys.includes(key))
|
|
18
|
+
const rest = uniqueKeys
|
|
19
|
+
.filter((key) => !PRIORITY_FIELD_SET.has(key))
|
|
20
|
+
.sort()
|
|
21
|
+
return [...prioritized, ...rest]
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const isEqual = (left: any, right: any): boolean => {
|
|
25
|
+
if (left === right) return true
|
|
26
|
+
if (left === null || right === null) return left === right
|
|
27
|
+
if (left === undefined || right === undefined) return left === right
|
|
28
|
+
if (Array.isArray(left) && Array.isArray(right)) {
|
|
29
|
+
if (left.length !== right.length) return false
|
|
30
|
+
for (let i = 0; i < left.length; i += 1) {
|
|
31
|
+
if (!isEqual(left[i], right[i])) return false
|
|
32
|
+
}
|
|
33
|
+
return true
|
|
34
|
+
}
|
|
35
|
+
if (isPlainObject(left) && isPlainObject(right)) {
|
|
36
|
+
const leftKeys = Object.keys(left).sort()
|
|
37
|
+
const rightKeys = Object.keys(right).sort()
|
|
38
|
+
if (leftKeys.length !== rightKeys.length) return false
|
|
39
|
+
for (let i = 0; i < leftKeys.length; i += 1) {
|
|
40
|
+
const key = leftKeys[i]
|
|
41
|
+
if (key !== rightKeys[i]) return false
|
|
42
|
+
if (!isEqual(left[key], right[key])) return false
|
|
43
|
+
}
|
|
44
|
+
return true
|
|
45
|
+
}
|
|
46
|
+
if (left instanceof Date && right instanceof Date) {
|
|
47
|
+
return left.toISOString() === right.toISOString()
|
|
48
|
+
}
|
|
49
|
+
return false
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const formatScalar = (value: any): string => {
|
|
53
|
+
if (value === null) return "null"
|
|
54
|
+
if (value === undefined) return "undefined"
|
|
55
|
+
if (typeof value === "string") return JSON.stringify(value)
|
|
56
|
+
if (typeof value === "number" || typeof value === "boolean")
|
|
57
|
+
return String(value)
|
|
58
|
+
if (typeof value === "bigint") return `${value}n`
|
|
59
|
+
if (value instanceof Date) return JSON.stringify(value.toISOString())
|
|
60
|
+
|
|
61
|
+
return JSON.stringify(value)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const parseItemKey = (key: string, item?: Record<string, any>) => {
|
|
65
|
+
const pk = typeof item?.PK === "string" ? item.PK : undefined
|
|
66
|
+
const sk = typeof item?.SK === "string" ? item.SK : undefined
|
|
67
|
+
|
|
68
|
+
if (pk && sk) return { pk, sk }
|
|
69
|
+
|
|
70
|
+
const [rawPk, rawSk] = key.split("__")
|
|
71
|
+
return {
|
|
72
|
+
pk: pk ?? rawPk ?? "?",
|
|
73
|
+
sk: sk ?? rawSk ?? "?",
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const formatItemHeader = (key: string, item?: Record<string, any>) => {
|
|
78
|
+
const { pk, sk } = parseItemKey(key, item)
|
|
79
|
+
return `[${pk} / ${sk}]`
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const renderValueLines = (
|
|
83
|
+
key: string | null,
|
|
84
|
+
value: any,
|
|
85
|
+
indentLevel: number
|
|
86
|
+
): string[] => {
|
|
87
|
+
const prefix = indent(indentLevel)
|
|
88
|
+
|
|
89
|
+
if (key === ARRAY_ITEM_KEY) {
|
|
90
|
+
return renderArrayItemLines(value, indentLevel)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (isPlainObject(value)) {
|
|
94
|
+
const keys = orderKeys(Object.keys(value))
|
|
95
|
+
if (keys.length === 0) {
|
|
96
|
+
const label = key ? `${key}: {}` : "{}"
|
|
97
|
+
return [`${prefix}${label}`]
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const lines: string[] = []
|
|
101
|
+
if (key) lines.push(`${prefix}${key}:`)
|
|
102
|
+
keys.forEach((childKey) => {
|
|
103
|
+
lines.push(
|
|
104
|
+
...renderValueLines(childKey, value[childKey], indentLevel + INDENT_SIZE)
|
|
105
|
+
)
|
|
106
|
+
})
|
|
107
|
+
return lines
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (Array.isArray(value)) {
|
|
111
|
+
if (value.length === 0) {
|
|
112
|
+
const label = key ? `${key}: []` : "[]"
|
|
113
|
+
return [`${prefix}${label}`]
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const lines: string[] = []
|
|
117
|
+
if (key) lines.push(`${prefix}${key}:`)
|
|
118
|
+
value.forEach((item) => {
|
|
119
|
+
lines.push(
|
|
120
|
+
...renderValueLines(ARRAY_ITEM_KEY, item, indentLevel + INDENT_SIZE)
|
|
121
|
+
)
|
|
122
|
+
})
|
|
123
|
+
return lines
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const label = key ? `${key}: ${formatScalar(value)}` : formatScalar(value)
|
|
127
|
+
return [`${prefix}${label}`]
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const applyPrefix = (line: string, prefix: "+" | "-") => {
|
|
131
|
+
if (line.startsWith(" ")) return `${prefix}${line.slice(1)}`
|
|
132
|
+
return `${prefix} ${line}`
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const renderValueLinesWithPrefix = (
|
|
136
|
+
key: string | null,
|
|
137
|
+
value: any,
|
|
138
|
+
indentLevel: number,
|
|
139
|
+
prefix: "+" | "-"
|
|
140
|
+
) =>
|
|
141
|
+
renderValueLines(key, value, indentLevel).map((line) =>
|
|
142
|
+
applyPrefix(line, prefix)
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
const renderArrayItemLines = (value: any, indentLevel: number): string[] => {
|
|
146
|
+
const prefix = indent(indentLevel)
|
|
147
|
+
|
|
148
|
+
if (isPlainObject(value)) {
|
|
149
|
+
const keys = orderKeys(Object.keys(value))
|
|
150
|
+
if (keys.length === 0) return [`${prefix}- {}`]
|
|
151
|
+
|
|
152
|
+
const lines: string[] = []
|
|
153
|
+
const [firstKey, ...restKeys] = keys
|
|
154
|
+
const firstLines = renderValueLines(
|
|
155
|
+
firstKey,
|
|
156
|
+
value[firstKey],
|
|
157
|
+
indentLevel + INDENT_SIZE
|
|
158
|
+
)
|
|
159
|
+
const trimmedFirstLine = firstLines[0].slice(indentLevel + INDENT_SIZE)
|
|
160
|
+
lines.push(`${prefix}- ${trimmedFirstLine}`)
|
|
161
|
+
lines.push(...firstLines.slice(1))
|
|
162
|
+
|
|
163
|
+
restKeys.forEach((key) => {
|
|
164
|
+
lines.push(...renderValueLines(key, value[key], indentLevel + INDENT_SIZE))
|
|
165
|
+
})
|
|
166
|
+
return lines
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (Array.isArray(value)) {
|
|
170
|
+
if (value.length === 0) return [`${prefix}- []`]
|
|
171
|
+
|
|
172
|
+
const lines: string[] = [`${prefix}-`]
|
|
173
|
+
value.forEach((item) => {
|
|
174
|
+
lines.push(
|
|
175
|
+
...renderValueLines(ARRAY_ITEM_KEY, item, indentLevel + INDENT_SIZE)
|
|
176
|
+
)
|
|
177
|
+
})
|
|
178
|
+
return lines
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return [`${prefix}- ${formatScalar(value)}`]
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const findArrayMatches = (before: any[], after: any[]) => {
|
|
185
|
+
const rows = before.length + 1
|
|
186
|
+
const cols = after.length + 1
|
|
187
|
+
const dp: number[][] = Array.from({ length: rows }, () =>
|
|
188
|
+
Array(cols).fill(0)
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
for (let i = before.length - 1; i >= 0; i -= 1) {
|
|
192
|
+
for (let j = after.length - 1; j >= 0; j -= 1) {
|
|
193
|
+
if (isEqual(before[i], after[j])) {
|
|
194
|
+
dp[i][j] = dp[i + 1][j + 1] + 1
|
|
195
|
+
} else {
|
|
196
|
+
dp[i][j] = Math.max(dp[i + 1][j], dp[i][j + 1])
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const matches: Array<{ beforeMatch: number; afterMatch: number }> = []
|
|
202
|
+
let i = 0
|
|
203
|
+
let j = 0
|
|
204
|
+
while (i < before.length && j < after.length) {
|
|
205
|
+
if (isEqual(before[i], after[j])) {
|
|
206
|
+
matches.push({ beforeMatch: i, afterMatch: j })
|
|
207
|
+
i += 1
|
|
208
|
+
j += 1
|
|
209
|
+
} else if (dp[i + 1][j] >= dp[i][j + 1]) {
|
|
210
|
+
i += 1
|
|
211
|
+
} else {
|
|
212
|
+
j += 1
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return matches
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const renderArraySegment = (
|
|
220
|
+
beforeSegment: any[],
|
|
221
|
+
afterSegment: any[],
|
|
222
|
+
indentLevel: number
|
|
223
|
+
): RenderResult => {
|
|
224
|
+
const lines: string[] = []
|
|
225
|
+
let changed = false
|
|
226
|
+
|
|
227
|
+
if (beforeSegment.length === 0 && afterSegment.length === 0) {
|
|
228
|
+
return { lines, changed: false }
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (beforeSegment.length === afterSegment.length) {
|
|
232
|
+
for (let i = 0; i < beforeSegment.length; i += 1) {
|
|
233
|
+
const result = renderFieldDiff(
|
|
234
|
+
ARRAY_ITEM_KEY,
|
|
235
|
+
beforeSegment[i],
|
|
236
|
+
afterSegment[i],
|
|
237
|
+
indentLevel
|
|
238
|
+
)
|
|
239
|
+
if (result.lines.length > 0) lines.push(...result.lines)
|
|
240
|
+
if (result.changed) changed = true
|
|
241
|
+
}
|
|
242
|
+
return { lines, changed }
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
beforeSegment.forEach((value) => {
|
|
246
|
+
lines.push(
|
|
247
|
+
...renderValueLinesWithPrefix(ARRAY_ITEM_KEY, value, indentLevel, "-")
|
|
248
|
+
)
|
|
249
|
+
changed = true
|
|
250
|
+
})
|
|
251
|
+
afterSegment.forEach((value) => {
|
|
252
|
+
lines.push(
|
|
253
|
+
...renderValueLinesWithPrefix(ARRAY_ITEM_KEY, value, indentLevel, "+")
|
|
254
|
+
)
|
|
255
|
+
changed = true
|
|
256
|
+
})
|
|
257
|
+
return { lines, changed }
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const renderArrayDiff = (
|
|
261
|
+
key: string,
|
|
262
|
+
before: any[],
|
|
263
|
+
after: any[],
|
|
264
|
+
indentLevel: number
|
|
265
|
+
): RenderResult => {
|
|
266
|
+
const lines: string[] = [`${indent(indentLevel)}${key}:`]
|
|
267
|
+
let changed = false
|
|
268
|
+
|
|
269
|
+
const matches = findArrayMatches(before, after)
|
|
270
|
+
let beforeIndex = 0
|
|
271
|
+
let afterIndex = 0
|
|
272
|
+
|
|
273
|
+
matches.forEach(({ beforeMatch, afterMatch }) => {
|
|
274
|
+
const segment = renderArraySegment(
|
|
275
|
+
before.slice(beforeIndex, beforeMatch),
|
|
276
|
+
after.slice(afterIndex, afterMatch),
|
|
277
|
+
indentLevel + INDENT_SIZE
|
|
278
|
+
)
|
|
279
|
+
if (segment.lines.length > 0) lines.push(...segment.lines)
|
|
280
|
+
if (segment.changed) changed = true
|
|
281
|
+
|
|
282
|
+
lines.push(
|
|
283
|
+
...renderValueLines(
|
|
284
|
+
ARRAY_ITEM_KEY,
|
|
285
|
+
before[beforeMatch],
|
|
286
|
+
indentLevel + INDENT_SIZE
|
|
287
|
+
)
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
beforeIndex = beforeMatch + 1
|
|
291
|
+
afterIndex = afterMatch + 1
|
|
292
|
+
})
|
|
293
|
+
|
|
294
|
+
const tailSegment = renderArraySegment(
|
|
295
|
+
before.slice(beforeIndex),
|
|
296
|
+
after.slice(afterIndex),
|
|
297
|
+
indentLevel + INDENT_SIZE
|
|
298
|
+
)
|
|
299
|
+
if (tailSegment.lines.length > 0) lines.push(...tailSegment.lines)
|
|
300
|
+
if (tailSegment.changed) changed = true
|
|
301
|
+
|
|
302
|
+
return { lines, changed }
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
const renderObjectDiff = (
|
|
306
|
+
key: string,
|
|
307
|
+
before: Record<string, any>,
|
|
308
|
+
after: Record<string, any>,
|
|
309
|
+
indentLevel: number
|
|
310
|
+
): RenderResult => {
|
|
311
|
+
const lines: string[] = [`${indent(indentLevel)}${key}:`]
|
|
312
|
+
let changed = false
|
|
313
|
+
const keys = orderKeys([...Object.keys(before), ...Object.keys(after)])
|
|
314
|
+
|
|
315
|
+
keys.forEach((childKey) => {
|
|
316
|
+
const result = renderFieldDiff(
|
|
317
|
+
childKey,
|
|
318
|
+
before[childKey],
|
|
319
|
+
after[childKey],
|
|
320
|
+
indentLevel + INDENT_SIZE
|
|
321
|
+
)
|
|
322
|
+
if (result.lines.length > 0) lines.push(...result.lines)
|
|
323
|
+
if (result.changed) changed = true
|
|
324
|
+
})
|
|
325
|
+
|
|
326
|
+
return { lines, changed }
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
const renderFieldDiff = (
|
|
330
|
+
key: string,
|
|
331
|
+
beforeValue: any,
|
|
332
|
+
afterValue: any,
|
|
333
|
+
indentLevel: number
|
|
334
|
+
): RenderResult => {
|
|
335
|
+
if (beforeValue === undefined && afterValue === undefined) {
|
|
336
|
+
return { lines: [], changed: false }
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
if (isEqual(beforeValue, afterValue)) {
|
|
340
|
+
return {
|
|
341
|
+
lines: renderValueLines(key, beforeValue, indentLevel),
|
|
342
|
+
changed: false,
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
if (beforeValue === undefined) {
|
|
347
|
+
return {
|
|
348
|
+
lines: renderValueLinesWithPrefix(key, afterValue, indentLevel, "+"),
|
|
349
|
+
changed: true,
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
if (afterValue === undefined) {
|
|
354
|
+
return {
|
|
355
|
+
lines: renderValueLinesWithPrefix(key, beforeValue, indentLevel, "-"),
|
|
356
|
+
changed: true,
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
if (Array.isArray(beforeValue) && Array.isArray(afterValue)) {
|
|
361
|
+
return renderArrayDiff(key, beforeValue, afterValue, indentLevel)
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
if (isPlainObject(beforeValue) && isPlainObject(afterValue)) {
|
|
365
|
+
return renderObjectDiff(key, beforeValue, afterValue, indentLevel)
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
return {
|
|
369
|
+
lines: [
|
|
370
|
+
...renderValueLinesWithPrefix(key, beforeValue, indentLevel, "-"),
|
|
371
|
+
...renderValueLinesWithPrefix(key, afterValue, indentLevel, "+"),
|
|
372
|
+
],
|
|
373
|
+
changed: true,
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
const renderItemFields = (item: Record<string, any>, prefix: "+" | "-") => {
|
|
378
|
+
const keys = orderKeys(Object.keys(item))
|
|
379
|
+
|
|
380
|
+
const lines: string[] = []
|
|
381
|
+
keys.forEach((key) => {
|
|
382
|
+
lines.push(
|
|
383
|
+
...renderValueLinesWithPrefix(key, item[key], ITEM_FIELD_INDENT, prefix)
|
|
384
|
+
)
|
|
385
|
+
})
|
|
386
|
+
return lines
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
const renderItemFieldsDiff = (
|
|
390
|
+
before: Record<string, any>,
|
|
391
|
+
after: Record<string, any>
|
|
392
|
+
): RenderResult => {
|
|
393
|
+
const keys = orderKeys([...Object.keys(before), ...Object.keys(after)])
|
|
394
|
+
const lines: string[] = []
|
|
395
|
+
let changed = false
|
|
396
|
+
|
|
397
|
+
keys.forEach((key) => {
|
|
398
|
+
const result = renderFieldDiff(
|
|
399
|
+
key,
|
|
400
|
+
before[key],
|
|
401
|
+
after[key],
|
|
402
|
+
ITEM_FIELD_INDENT
|
|
403
|
+
)
|
|
404
|
+
if (result.lines.length > 0) lines.push(...result.lines)
|
|
405
|
+
if (result.changed) changed = true
|
|
406
|
+
})
|
|
407
|
+
|
|
408
|
+
return { lines, changed }
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
export const formatSnapshotDiff = (before: Snapshot, after: Snapshot) => {
|
|
412
|
+
const keys = new Set([...Object.keys(before), ...Object.keys(after)])
|
|
413
|
+
const sortedKeys = Array.from(keys).sort((a, b) => {
|
|
414
|
+
const itemA = after[a] ?? before[a]
|
|
415
|
+
const itemB = after[b] ?? before[b]
|
|
416
|
+
return formatItemHeader(a, itemA).localeCompare(formatItemHeader(b, itemB))
|
|
417
|
+
})
|
|
418
|
+
|
|
419
|
+
const lines: string[] = []
|
|
420
|
+
|
|
421
|
+
sortedKeys.forEach((key) => {
|
|
422
|
+
const beforeItem = before[key]
|
|
423
|
+
const afterItem = after[key]
|
|
424
|
+
const header = formatItemHeader(key, afterItem ?? beforeItem)
|
|
425
|
+
|
|
426
|
+
let itemLines: string[] = []
|
|
427
|
+
if (!beforeItem && afterItem) {
|
|
428
|
+
itemLines = [`+ ${header}`, ...renderItemFields(afterItem, "+")]
|
|
429
|
+
} else if (beforeItem && !afterItem) {
|
|
430
|
+
itemLines = [`- ${header}`, ...renderItemFields(beforeItem, "-")]
|
|
431
|
+
} else if (beforeItem && afterItem) {
|
|
432
|
+
const diffResult = renderItemFieldsDiff(beforeItem, afterItem)
|
|
433
|
+
if (diffResult.changed) itemLines = [header, ...diffResult.lines]
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
if (itemLines.length > 0) {
|
|
437
|
+
if (lines.length > 0) lines.push("")
|
|
438
|
+
lines.push(...itemLines)
|
|
439
|
+
}
|
|
440
|
+
})
|
|
441
|
+
|
|
442
|
+
return lines.join("\n")
|
|
443
|
+
}
|