Skip to content

Commit 9bddb94

Browse files
Merge branch 'main' into CAT-1380
2 parents 8af026d + d69f97f commit 9bddb94

File tree

7 files changed

+170
-118
lines changed

7 files changed

+170
-118
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
1111
- Added `id` field as secondary sort to sort config to ensure unique pagination tokens. [#421](https://github.com/stac-utils/stac-fastapi-elasticsearch-opensearch/pull/421)
1212
- Added default environment variable `STAC_ITEM_LIMIT` to SFEOS for result limiting of returned items and STAC collections [#419](https://github.com/stac-utils/stac-fastapi-elasticsearch-opensearch/pull/419)
1313

14+
### Changed
15+
16+
- Simplified Patch class and updated patch script creation including adding nest creation for merge patch [#420](https://github.com/stac-utils/stac-fastapi-elasticsearch-opensearch/pull/420)
17+
1418
## [v6.2.0] - 2025-08-27
1519

1620
### Added

stac_fastapi/core/stac_fastapi/core/base_database_logic.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ async def json_patch_item(
4848
item_id: str,
4949
operations: List,
5050
base_url: str,
51+
create_nest: bool = False,
5152
refresh: bool = True,
5253
) -> Dict:
5354
"""Patch a item in the database follows RF6902."""
@@ -94,6 +95,7 @@ async def json_patch_collection(
9495
collection_id: str,
9596
operations: List,
9697
base_url: str,
98+
create_nest: bool = False,
9799
refresh: bool = True,
98100
) -> Dict:
99101
"""Patch a collection in the database follows RF6902."""

stac_fastapi/elasticsearch/stac_fastapi/elasticsearch/database_logic.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -886,6 +886,7 @@ async def merge_patch_item(
886886
item_id=item_id,
887887
operations=operations,
888888
base_url=base_url,
889+
create_nest=True,
889890
refresh=refresh,
890891
)
891892

@@ -895,6 +896,7 @@ async def json_patch_item(
895896
item_id: str,
896897
operations: List[PatchOperation],
897898
base_url: str,
899+
create_nest: bool = False,
898900
refresh: bool = True,
899901
) -> Item:
900902
"""Database logic for json patching an item following RF6902.
@@ -929,7 +931,7 @@ async def json_patch_item(
929931
else:
930932
script_operations.append(operation)
931933

932-
script = operations_to_script(script_operations)
934+
script = operations_to_script(script_operations, create_nest=create_nest)
933935

934936
try:
935937
search_response = await self.client.search(
@@ -1265,6 +1267,7 @@ async def merge_patch_collection(
12651267
collection_id=collection_id,
12661268
operations=operations,
12671269
base_url=base_url,
1270+
create_nest=True,
12681271
refresh=refresh,
12691272
)
12701273

@@ -1273,6 +1276,7 @@ async def json_patch_collection(
12731276
collection_id: str,
12741277
operations: List[PatchOperation],
12751278
base_url: str,
1279+
create_nest: bool = False,
12761280
refresh: bool = True,
12771281
) -> Collection:
12781282
"""Database logic for json patching a collection following RF6902.
@@ -1300,7 +1304,7 @@ async def json_patch_collection(
13001304
else:
13011305
script_operations.append(operation)
13021306

1303-
script = operations_to_script(script_operations)
1307+
script = operations_to_script(script_operations, create_nest=create_nest)
13041308

13051309
try:
13061310
await self.client.update(

stac_fastapi/opensearch/stac_fastapi/opensearch/database_logic.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -869,6 +869,7 @@ async def merge_patch_item(
869869
item_id=item_id,
870870
operations=operations,
871871
base_url=base_url,
872+
create_nest=True,
872873
refresh=refresh,
873874
)
874875

@@ -878,6 +879,7 @@ async def json_patch_item(
878879
item_id: str,
879880
operations: List[PatchOperation],
880881
base_url: str,
882+
create_nest: bool = False,
881883
refresh: bool = True,
882884
) -> Item:
883885
"""Database logic for json patching an item following RF6902.
@@ -912,7 +914,7 @@ async def json_patch_item(
912914
else:
913915
script_operations.append(operation)
914916

915-
script = operations_to_script(script_operations)
917+
script = operations_to_script(script_operations, create_nest=create_nest)
916918

917919
try:
918920
search_response = await self.client.search(
@@ -1220,6 +1222,7 @@ async def merge_patch_collection(
12201222
collection_id=collection_id,
12211223
operations=operations,
12221224
base_url=base_url,
1225+
create_nest=True,
12231226
refresh=refresh,
12241227
)
12251228

@@ -1228,6 +1231,7 @@ async def json_patch_collection(
12281231
collection_id: str,
12291232
operations: List[PatchOperation],
12301233
base_url: str,
1234+
create_nest: bool = False,
12311235
refresh: bool = True,
12321236
) -> Collection:
12331237
"""Database logic for json patching a collection following RF6902.
@@ -1255,7 +1259,7 @@ async def json_patch_collection(
12551259
else:
12561260
script_operations.append(operation)
12571261

1258-
script = operations_to_script(script_operations)
1262+
script = operations_to_script(script_operations, create_nest=create_nest)
12591263

12601264
try:
12611265
await self.client.update(

stac_fastapi/sfeos_helpers/stac_fastapi/sfeos_helpers/database/utils.py

Lines changed: 75 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ def merge_to_operations(data: Dict) -> List:
7676
nested_operations = merge_to_operations(value)
7777

7878
for nested_operation in nested_operations:
79-
nested_operation.path = f"{key}.{nested_operation.path}"
79+
nested_operation.path = f"{key}/{nested_operation.path}"
8080
operations.append(nested_operation)
8181

8282
else:
@@ -90,6 +90,7 @@ def check_commands(
9090
op: str,
9191
path: ElasticPath,
9292
from_path: bool = False,
93+
create_nest: bool = False,
9394
) -> None:
9495
"""Add Elasticsearch checks to operation.
9596
@@ -101,25 +102,44 @@ def check_commands(
101102
102103
"""
103104
if path.nest:
104-
commands.add(
105-
f"if (!ctx._source.containsKey('{path.nest}'))"
106-
f"{{Debug.explain('{path.nest} does not exist');}}"
107-
)
108-
109-
if path.index or op in ["remove", "replace", "test"] or from_path:
110-
commands.add(
111-
f"if (!ctx._source{path.es_nest}.containsKey('{path.key}'))"
112-
f"{{Debug.explain('{path.key} does not exist in {path.nest}');}}"
113-
)
114-
115-
if from_path and path.index is not None:
116-
commands.add(
117-
f"if ((ctx._source{path.es_location} instanceof ArrayList"
118-
f" && ctx._source{path.es_location}.size() < {path.index})"
119-
f" || (!(ctx._source{path.es_location} instanceof ArrayList)"
120-
f" && !ctx._source{path.es_location}.containsKey('{path.index}')))"
121-
f"{{Debug.explain('{path.path} does not exist');}}"
122-
)
105+
part_nest = ""
106+
for index, path_part in enumerate(path.parts):
107+
108+
# Create nested dictionaries if not present for merge operations
109+
if create_nest and not from_path:
110+
value = "[:]"
111+
for sub_part in reversed(path.parts[index + 1 :]):
112+
value = f"['{sub_part}': {value}]"
113+
114+
commands.add(
115+
f"if (!ctx._source{part_nest}.containsKey('{path_part}'))"
116+
f"{{ctx._source{part_nest}['{path_part}'] = {value};}}"
117+
f"{'' if index == len(path.parts) - 1 else' else '}"
118+
)
119+
120+
else:
121+
commands.add(
122+
f"if (!ctx._source{part_nest}.containsKey('{path_part}'))"
123+
f"{{Debug.explain('{path_part} in {path.path} does not exist');}}"
124+
)
125+
126+
part_nest += f"['{path_part}']"
127+
128+
if from_path or op in ["remove", "replace", "test"]:
129+
130+
if isinstance(path.key, int):
131+
commands.add(
132+
f"if ((ctx._source{path.es_nest} instanceof ArrayList"
133+
f" && ctx._source{path.es_nest}.size() < {abs(path.key)})"
134+
f" || (!(ctx._source{path.es_nest} instanceof ArrayList)"
135+
f" && !ctx._source{path.es_nest}.containsKey('{path.key}')))"
136+
f"{{Debug.explain('{path.key} does not exist in {path.nest}');}}"
137+
)
138+
else:
139+
commands.add(
140+
f"if (!ctx._source{path.es_nest}.containsKey('{path.key}'))"
141+
f"{{Debug.explain('{path.key} does not exist in {path.nest}');}}"
142+
)
123143

124144

125145
def remove_commands(commands: ESCommandSet, path: ElasticPath) -> None:
@@ -130,15 +150,16 @@ def remove_commands(commands: ESCommandSet, path: ElasticPath) -> None:
130150
path (ElasticPath): Path to value to be removed
131151
132152
"""
133-
if path.index is not None:
153+
commands.add(f"def {path.variable_name};")
154+
if isinstance(path.key, int):
134155
commands.add(
135-
f"def {path.variable_name} = ctx._source{path.es_location}.remove({path.index});"
156+
f"if (ctx._source{path.es_nest} instanceof ArrayList)"
157+
f"{{{path.variable_name} = ctx._source{path.es_nest}.remove({path.es_key});}} else "
136158
)
137159

138-
else:
139-
commands.add(
140-
f"def {path.variable_name} = ctx._source{path.es_nest}.remove('{path.key}');"
141-
)
160+
commands.add(
161+
f"{path.variable_name} = ctx._source{path.es_nest}.remove('{path.key}');"
162+
)
142163

143164

144165
def add_commands(
@@ -160,21 +181,22 @@ def add_commands(
160181
value = (
161182
from_path.variable_name
162183
if operation.op == "move"
163-
else f"ctx._source.{from_path.es_path}"
184+
else f"ctx._source{from_path.es_path}"
164185
)
186+
165187
else:
166188
value = f"params.{path.param_key}"
167189
params[path.param_key] = operation.value
168190

169-
if path.index is not None:
191+
if isinstance(path.key, int):
170192
commands.add(
171-
f"if (ctx._source{path.es_location} instanceof ArrayList)"
172-
f"{{ctx._source{path.es_location}.{'add' if operation.op in ['add', 'move'] else 'set'}({path.index}, {value})}}"
173-
f"else{{ctx._source.{path.es_path} = {value}}}"
193+
f"if (ctx._source{path.es_nest} instanceof ArrayList)"
194+
f"{{ctx._source{path.es_nest}.{'add' if operation.op in ['add', 'move'] else 'set'}({path.es_key}, {value});}}"
195+
f" else ctx._source{path.es_nest}['{path.es_key}'] = {value};"
174196
)
175197

176198
else:
177-
commands.add(f"ctx._source.{path.es_path} = {value};")
199+
commands.add(f"ctx._source{path.es_path} = {value};")
178200

179201

180202
def test_commands(
@@ -190,14 +212,23 @@ def test_commands(
190212
value = f"params.{path.param_key}"
191213
params[path.param_key] = operation.value
192214

215+
if isinstance(path.key, int):
216+
commands.add(
217+
f"if (ctx._source{path.es_nest} instanceof ArrayList)"
218+
f"{{if (ctx._source{path.es_nest}[{path.es_key}] != {value})"
219+
f"{{Debug.explain('Test failed `{path.path}`"
220+
f" != ' + ctx._source{path.es_path});}}"
221+
f"}} else "
222+
)
223+
193224
commands.add(
194-
f"if (ctx._source.{path.es_path} != {value})"
195-
f"{{Debug.explain('Test failed `{path.path}` | "
196-
f"{operation.json_value} != ' + ctx._source.{path.es_path});}}"
225+
f"if (ctx._source{path.es_path} != {value})"
226+
f"{{Debug.explain('Test failed `{path.path}`"
227+
f" != ' + ctx._source{path.es_path});}}"
197228
)
198229

199230

200-
def operations_to_script(operations: List) -> Dict:
231+
def operations_to_script(operations: List, create_nest: bool = False) -> Dict:
201232
"""Convert list of operation to painless script.
202233
203234
Args:
@@ -215,10 +246,16 @@ def operations_to_script(operations: List) -> Dict:
215246
ElasticPath(path=operation.from_) if hasattr(operation, "from_") else None
216247
)
217248

218-
check_commands(commands=commands, op=operation.op, path=path)
249+
check_commands(
250+
commands=commands, op=operation.op, path=path, create_nest=create_nest
251+
)
219252
if from_path is not None:
220253
check_commands(
221-
commands=commands, op=operation.op, path=from_path, from_path=True
254+
commands=commands,
255+
op=operation.op,
256+
path=from_path,
257+
from_path=True,
258+
create_nest=create_nest,
222259
)
223260

224261
if operation.op in ["remove", "move"]:

0 commit comments

Comments
 (0)