-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathmerge-patch.js
More file actions
124 lines (113 loc) · 4.3 KB
/
Copy pathmerge-patch.js
File metadata and controls
124 lines (113 loc) · 4.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
'use strict'
// Structural pre-processor: expand $merge (RFC 7386) and $patch (RFC 6902 subset)
// keywords before handing the schema to ata-validator. Fastify passes schemas
// containing these keywords when ajv-merge-patch is listed in ajv.plugins; we
// handle them here so ata never sees them.
//
// Rules:
// $merge: { source, with } -> JSON Merge Patch (RFC 7386): W applied to S
// $patch: { source, with } -> RFC 6902 JSON Patch (ops: add, replace, remove only)
//
// Expansion is recursive: keywords may be nested anywhere; results may contain
// further keywords. Never mutates the input.
function hasMergePatch(schema) {
if (!schema || typeof schema !== 'object') return false
if (Array.isArray(schema)) return schema.some(hasMergePatch)
if ('$merge' in schema || '$patch' in schema) return true
return Object.values(schema).some(hasMergePatch)
}
// RFC 7386 JSON Merge Patch: apply patch W onto target S.
// Objects merge recursively; null in W deletes the key; arrays/scalars replace.
function applyMergePatch(source, patch) {
if (patch === null || typeof patch !== 'object' || Array.isArray(patch)) {
return patch
}
if (source === null || typeof source !== 'object' || Array.isArray(source)) {
source = {}
}
const result = Object.assign({}, source)
for (const key of Object.keys(patch)) {
if (patch[key] === null) {
delete result[key]
} else {
result[key] = applyMergePatch(result[key], patch[key])
}
}
return result
}
// JSON Pointer resolution with ~0/~1 unescaping (RFC 6901).
function resolvePointer(obj, pointer) {
if (pointer === '') return { obj, key: null, parent: null }
const parts = pointer.slice(1).split('/').map(p => p.replace(/~1/g, '/').replace(/~0/g, '~'))
let parent = null
let current = obj
let lastKey = null
for (const part of parts) {
parent = current
lastKey = part
if (current == null || typeof current !== 'object') {
throw new Error(`$patch: cannot traverse into non-object at /${part}`)
}
current = current[part]
}
return { parent, key: lastKey, value: current }
}
// RFC 6902 JSON Patch: apply array of ops to document. Supported: add, replace, remove.
function applyJsonPatch(source, ops) {
// Deep-clone so ops can mutate freely without touching input.
let doc = JSON.parse(JSON.stringify(source == null ? {} : source))
for (const op of ops) {
const { op: opName, path, value } = op
if (opName === 'add' || opName === 'replace') {
if (path === '') {
doc = value
continue
}
const { parent, key } = resolvePointer(doc, path)
if (parent == null) throw new Error(`$patch: invalid path "${path}"`)
if (opName === 'replace' && !(key in parent)) {
throw new Error(`$patch: cannot replace non-existent path "${path}"`)
}
parent[key] = value
} else if (opName === 'remove') {
if (path === '') throw new Error('$patch: cannot remove root')
const { parent, key } = resolvePointer(doc, path)
if (parent == null) throw new Error(`$patch: invalid path "${path}"`)
if (!(key in parent)) {
throw new Error(`$patch: cannot remove non-existent path "${path}"`)
}
delete parent[key]
} else {
throw new Error(`$patch: unsupported op "${opName}" (only add/replace/remove supported)`)
}
}
return doc
}
// Recursively expand $merge/$patch in schema. Returns a new object; never mutates.
function expand(schema, depth = 0) {
if (depth > 100) {
throw new Error('$merge/$patch: expansion exceeded depth limit (circular?)')
}
if (!schema || typeof schema !== 'object') return schema
if (Array.isArray(schema)) return schema.map(s => expand(s, depth))
if ('$merge' in schema) {
const source = expand(schema.$merge.source, depth + 1)
const patch = expand(schema.$merge.with, depth + 1)
return expand(applyMergePatch(source, patch), depth + 1)
}
if ('$patch' in schema) {
const source = expand(schema.$patch.source, depth + 1)
const ops = schema.$patch.with
return expand(applyJsonPatch(source, ops), depth + 1)
}
const result = {}
for (const key of Object.keys(schema)) {
result[key] = expand(schema[key], depth)
}
return result
}
function expandMergePatch(schema) {
if (!hasMergePatch(schema)) return schema
return expand(schema)
}
module.exports = { expandMergePatch }