From 9057f95f3c13acc2fbbd516f72c5ee0d5263b6da Mon Sep 17 00:00:00 2001 From: Matthew Bee Date: Thu, 13 Jul 2023 17:21:33 +0100 Subject: [PATCH 1/2] Initial implementation of PATCH functionality Implementing PATCH operation from the additional Partial document update in Azure Cosmos DB API extension overlaying the 2018-12-31 REST API version 3. Implementing a set of new getPatchOp[VERB] functions to QueryBuilder to handle all available PATCH operations; Similarities and differences between PATCH operations ADD SET REPLACE REMOVE INCREMENT MOVE Ensures conforming patch operation elements are generated to be passed into the new patch method $operations array rather than relying on a top level calling function having to populate the array manually. Implementing PATCH verb via a new patch method in the QueryBuilder methods, attempting to follow the existing methods and conventions, including adding the dependent functionality up chain in CosmosDbCollection and CosmosDB classes. Best efforts to align whitespace scheme with original project, (apologies for any differences due to differing indent configuration in my IDE, I tried to align with limited success) --- src/CosmosDb.php | 91 +++++++++++------ src/CosmosDbCollection.php | 59 ++++++----- src/QueryBuilder.php | 194 +++++++++++++++++++++++++++++++------ 3 files changed, 261 insertions(+), 83 deletions(-) diff --git a/src/CosmosDb.php b/src/CosmosDb.php index c76ad82..bdb16b6 100644 --- a/src/CosmosDb.php +++ b/src/CosmosDb.php @@ -39,7 +39,7 @@ public function setHttpClientOptions(array $options = []) * * @link http://msdn.microsoft.com/en-us/library/azure/dn783368.aspx * @access private - * @param string $verb Request Method (GET, POST, PUT, DELETE) + * @param string $verb Request Method (GET, POST, PUT, DELETE, PATCH) * @param string $resource_type Resource Type * @param string $resource_id Resource ID * @return array of Request Headers @@ -151,20 +151,20 @@ public function getInfo(): string * @return array JSON response * @throws GuzzleException */ - public function query(string $rid_id, string $rid_col, string $query, bool $isCrossPartition = false, $partitionValue = null): array - { + public function query(string $rid_id, string $rid_col, string $query, bool $isCrossPartition = false, $partitionValue = null): array + { $headers = $this->getAuthHeaders('POST', 'docs', $rid_col); $headers['Content-Length'] = strlen($query); $headers['Content-Type'] = 'application/query+json'; $headers['x-ms-max-item-count'] = -1; $headers['x-ms-documentdb-isquery'] = 'True'; - + if ($isCrossPartition) { $headers['x-ms-documentdb-query-enablecrosspartition'] = 'True'; } - + if ($partitionValue) { - $headers['x-ms-documentdb-partitionkey'] = '["'.$partitionValue.'"]'; + $headers['x-ms-documentdb-partitionkey'] = '["' . $partitionValue . '"]'; } /* * Fix for https://github.com/jupitern/cosmosdb/issues/21 (credits to https://github.com/ElvenSpellmaker). @@ -186,8 +186,8 @@ public function query(string $rid_id, string $rid_col, string $query, bool $isCr $result = $this->request("/dbs/{$rid_id}/colls/{$rid_col}/docs", "POST", $headers, $query); $results[] = $result->getBody()->getContents(); } - } - catch (\GuzzleHttp\Exception\ClientException $e) { + } + catch (\GuzzleHttp\Exception\ClientException $e) { $responseError = \json_decode($e->getResponse()->getBody()->getContents()); // -- Retry the request with PK Ranges -- @@ -220,14 +220,14 @@ public function query(string $rid_id, string $rid_col, string $query, bool $isCr * @return mixed * @throws GuzzleException */ - public function getPkRanges(string $rid_id, string $rid_col): mixed + public function getPkRanges(string $rid_id, string $rid_col): mixed { - $headers = $this->getAuthHeaders('GET', 'pkranges', $rid_col); - $headers['Accept'] = 'application/json'; - $headers['x-ms-max-item-count'] = -1; - $result = $this->request("/dbs/{$rid_id}/colls/{$rid_col}/pkranges", "GET", $headers); - return json_decode($result->getBody()->getContents()); - } + $headers = $this->getAuthHeaders('GET', 'pkranges', $rid_col); + $headers['Accept'] = 'application/json'; + $headers['x-ms-max-item-count'] = -1; + $result = $this->request("/dbs/{$rid_id}/colls/{$rid_col}/pkranges", "GET", $headers); + return json_decode($result->getBody()->getContents()); + } /** * getPkFullRange @@ -238,12 +238,12 @@ public function getPkRanges(string $rid_id, string $rid_col): mixed * @return string * @throws GuzzleException */ - public function getPkFullRange($rid_id, $rid_col): string + public function getPkFullRange($rid_id, $rid_col): string { - $result = $this->getPkRanges($rid_id, $rid_col); - $ids = \array_column($result->PartitionKeyRanges, "id"); - return $result->_rid . "," . \implode(",", $ids); - } + $result = $this->getPkRanges($rid_id, $rid_col); + $ids = \array_column($result->PartitionKeyRanges, "id"); + return $result->_rid . "," . \implode(",", $ids); + } /** * listDatabases @@ -534,7 +534,7 @@ public function createDocument(string $rid_id, string $rid_col, string $json, st $headers = \array_merge($headers, $authHeaders); $headers['Content-Length'] = strlen($json); if ($partitionKey !== null) { - $headers['x-ms-documentdb-partitionkey'] = '["'.$partitionKey.'"]'; + $headers['x-ms-documentdb-partitionkey'] = '["' . $partitionKey . '"]'; } return $this->request("/dbs/{$rid_id}/colls/{$rid_col}/docs", "POST", $headers, $json)->getBody()->getContents(); @@ -560,12 +560,39 @@ public function replaceDocument(string $rid_id, string $rid_col, string $rid_doc $headers = \array_merge($headers, $authHeaders); $headers['Content-Length'] = strlen($json); if ($partitionKey !== null) { - $headers['x-ms-documentdb-partitionkey'] = '["'.$partitionKey.'"]'; + $headers['x-ms-documentdb-partitionkey'] = '["' . $partitionKey . '"]'; } return $this->request("/dbs/{$rid_id}/colls/{$rid_col}/docs/{$rid_doc}", "PUT", $headers, $json)->getBody()->getContents(); } + /** + * patchDocument + * + * @link https://learn.microsoft.com/en-us/azure/cosmos-db/partial-document-update#rest-api-reference-for-partial-document-update + * @access public + * @param string $rid_id Resource ID + * @param string $rid_col Resource Collection ID + * @param string $rid_doc Resource Doc ID + * @param string $json JSON request + * @param string|null $partitionKey + * @param array $headers Optional headers to send along with the request + * @return string JSON response + * @throws GuzzleException + */ + public function patchDocument(string $rid_id, string $rid_col, string $rid_doc, string $json, string $partitionKey = null, array $headers = []): string + { + $authHeaders = $this->getAuthHeaders('PATCH', 'docs', $rid_doc); + $headers = \array_merge($headers, $authHeaders); + $headers['Content-Length'] = strlen($json); + $headers['Content-Type'] = 'application/json_patch+json'; + if ($partitionKey !== null) { + $headers['x-ms-documentdb-partitionkey'] = '["' . $partitionKey . '"]'; + } + + return $this->request("/dbs/{$rid_id}/colls/{$rid_col}/docs/{$rid_doc}", "PATCH", $headers, $json)->getBody()->getContents(); + } + /** * deleteDocument * @@ -585,7 +612,7 @@ public function deleteDocument(string $rid_id, string $rid_col, string $rid_doc, $headers = \array_merge($headers, $authHeaders); $headers['Content-Length'] = '0'; if ($partitionKey !== null) { - $headers['x-ms-documentdb-partitionkey'] = '["'.$partitionKey.'"]'; + $headers['x-ms-documentdb-partitionkey'] = '["' . $partitionKey . '"]'; } return $this->request("/dbs/{$rid_id}/colls/{$rid_col}/docs/{$rid_doc}", "DELETE", $headers)->getBody()->getContents(); @@ -935,7 +962,7 @@ public function replaceStoredProcedure(string $rid_id, string $rid_col, string $ * @throws GuzzleException */ public function deleteStoredProcedure(string $rid_id, string $rid_col, string $rid_sproc): string - { + { $headers = $this->getAuthHeaders('DELETE', 'sprocs', $rid_sproc); $headers['Content-Length'] = '0'; return $this->request("/dbs/{$rid_id}/colls/{$rid_col}/sprocs/{$rid_sproc}", "DELETE", $headers)->getBody()->getContents(); @@ -952,7 +979,7 @@ public function deleteStoredProcedure(string $rid_id, string $rid_col, string $r * @throws GuzzleException */ public function listUserDefinedFunctions(string $rid_id, string $rid_col): string - { + { $headers = $this->getAuthHeaders('GET', 'udfs', $rid_col); $headers['Content-Length'] = '0'; return $this->request("/dbs/{$rid_id}/colls/{$rid_col}/udfs", "GET", $headers)->getBody()->getContents(); @@ -970,7 +997,7 @@ public function listUserDefinedFunctions(string $rid_id, string $rid_col): strin * @throws GuzzleException */ public function createUserDefinedFunction(string $rid_id, string $rid_col, string $json): string - { + { $headers = $this->getAuthHeaders('POST', 'udfs', $rid_col); $headers['Content-Length'] = strlen($json); return $this->request("/dbs/{$rid_id}/colls/{$rid_col}/udfs", "POST", $headers, $json)->getBody()->getContents(); @@ -989,7 +1016,7 @@ public function createUserDefinedFunction(string $rid_id, string $rid_col, strin * @throws GuzzleException */ public function replaceUserDefinedFunction(string $rid_id, string $rid_col, string $rid_udf, string $json): string - { + { $headers = $this->getAuthHeaders('PUT', 'udfs', $rid_udf); $headers['Content-Length'] = strlen($json); return $this->request("/dbs/{$rid_id}/colls/{$rid_col}/udfs/{$rid_udf}", "PUT", $headers, $json)->getBody()->getContents(); @@ -1007,7 +1034,7 @@ public function replaceUserDefinedFunction(string $rid_id, string $rid_col, stri * @throws GuzzleException */ public function deleteUserDefinedFunction(string $rid_id, string $rid_col, string $rid_udf): string - { + { $headers = $this->getAuthHeaders('DELETE', 'udfs', $rid_udf); $headers['Content-Length'] = '0'; return $this->request("/dbs/{$rid_id}/colls/{$rid_col}/udfs/{$rid_udf}", "DELETE", $headers)->getBody()->getContents(); @@ -1024,7 +1051,7 @@ public function deleteUserDefinedFunction(string $rid_id, string $rid_col, strin * @throws GuzzleException */ public function listTriggers(string $rid_id, string $rid_col): string - { + { $headers = $this->getAuthHeaders('GET', 'triggers', $rid_col); $headers['Content-Length'] = '0'; return $this->request("/dbs/{$rid_id}/colls/{$rid_col}/triggers", "GET", $headers)->getBody()->getContents(); @@ -1042,7 +1069,7 @@ public function listTriggers(string $rid_id, string $rid_col): string * @throws GuzzleException */ public function createTrigger(string $rid_id, string $rid_col, string $json): string - { + { $headers = $this->getAuthHeaders('POST', 'triggers', $rid_col); $headers['Content-Length'] = strlen($json); return $this->request("/dbs/{$rid_id}/colls/{$rid_col}/triggers", "POST", $headers, $json)->getBody()->getContents(); @@ -1061,7 +1088,7 @@ public function createTrigger(string $rid_id, string $rid_col, string $json): st * @throws GuzzleException */ public function replaceTrigger(string $rid_id, string $rid_col, string $rid_trigger, string $json): string - { + { $headers = $this->getAuthHeaders('PUT', 'triggers', $rid_trigger); $headers['Content-Length'] = strlen($json); return $this->request("/dbs/{$rid_id}/colls/{$rid_col}/triggers/{$rid_trigger}", "PUT", $headers, $json)->getBody()->getContents(); @@ -1079,7 +1106,7 @@ public function replaceTrigger(string $rid_id, string $rid_col, string $rid_trig * @throws GuzzleException */ public function deleteTrigger(string $rid_id, string $rid_col, string $rid_trigger): string - { + { $headers = $this->getAuthHeaders('DELETE', 'triggers', $rid_trigger); $headers['Content-Length'] = '0'; return $this->request("/dbs/{$rid_id}/colls/{$rid_col}/triggers/{$rid_trigger}", "DELETE", $headers)->getBody()->getContents(); diff --git a/src/CosmosDbCollection.php b/src/CosmosDbCollection.php index 5c5c983..421bc91 100644 --- a/src/CosmosDbCollection.php +++ b/src/CosmosDbCollection.php @@ -35,9 +35,9 @@ public function query($query, $params = [], $isCrossPartition = false, $partitio { $paramsJson = []; foreach ($params as $key => $val) { - $val = is_int($val) || is_float($val) ? $val : '"'. str_replace('"', '\\"', $val) .'"'; + $val = is_int($val) || is_float($val) ? $val : '"' . str_replace('"', '\\"', $val) . '"'; - $paramsJson[] = '{"name": "' . str_replace('"', '\\"', $key) . '", "value": '.$val.'}'; + $paramsJson[] = '{"name": "' . str_replace('"', '\\"', $key) . '", "value": ' . $val . '}'; } $query = '{"query": "' . str_replace('"', '\\"', $query) . '", "parameters": [' . implode(',', $paramsJson) . ']}'; @@ -45,25 +45,25 @@ public function query($query, $params = [], $isCrossPartition = false, $partitio return $this->document_db->query($this->rid_db, $this->rid_col, $query, $isCrossPartition, $partitionValue); } - /** - * getPkRanges - * - * @return mixed - */ - public function getPkRanges() - { - return $this->document_db->getPkRanges($this->rid_db, $this->rid_col); - } - - /** - * getPkFullRange - * - * @return mixed - */ - public function getPkFullRange() - { - return $this->document_db->getPkFullRange($this->rid_db, $this->rid_col); - } + /** + * getPkRanges + * + * @return mixed + */ + public function getPkRanges() + { + return $this->document_db->getPkRanges($this->rid_db, $this->rid_col); + } + + /** + * getPkFullRange + * + * @return mixed + */ + public function getPkFullRange() + { + return $this->document_db->getPkFullRange($this->rid_db, $this->rid_col); + } /** * createDocument @@ -94,6 +94,21 @@ public function replaceDocument($rid, $json, $partitionKey = null, array $header return $this->document_db->replaceDocument($this->rid_db, $this->rid_col, $rid, $json, $partitionKey, $headers); } + /** + * patchDocument + * + * @access public + * @param string $rid document ResourceID (_rid) + * @param string $json JSON formatted operations array + * @param string $partitionKey + * @param array $headers Optional headers to send along with the request + * @return string JSON strings + */ + public function patchDocument($rid_doc, $operations, $partitionKey = null, array $headers = []) + { + return $this->document_db->patchDocument($this->rid_db, $this->rid_col, $rid_doc, $operations, $partitionKey, $headers); + } + /** * deleteDocument * @@ -134,7 +149,7 @@ public function getPermission($uid, $pid) return $this->document_db->getPermission($this->rid_db, $uid, $pid); } */ - + public function listStoredProcedures() { return $this->document_db->listStoredProcedures($this->rid_db, $this->rid_col); diff --git a/src/QueryBuilder.php b/src/QueryBuilder.php index 10904c8..a42671b 100644 --- a/src/QueryBuilder.php +++ b/src/QueryBuilder.php @@ -89,9 +89,9 @@ public function where(string $where): static * @return QueryBuilder */ public function whereStartsWith(string $field, mixed $value): static - { - return $this->where("STARTSWITH($field, '{$value}')"); - } + { + return $this->where("STARTSWITH($field, '{$value}')"); + } /** * @param string $field @@ -100,8 +100,8 @@ public function whereStartsWith(string $field, mixed $value): static */ public function whereEndsWith(string $field, mixed $value): static { - return $this->where("ENDSWITH($field, '{$value}')"); - } + return $this->where("ENDSWITH($field, '{$value}')"); + } /** * @param string $field @@ -110,8 +110,8 @@ public function whereEndsWith(string $field, mixed $value): static */ public function whereContains(string $field, mixed $value): static { - return $this->where("CONTAINS($field, '{$value}'"); - } + return $this->where("CONTAINS($field, '{$value}'"); + } /** * @param string $field @@ -119,11 +119,11 @@ public function whereContains(string $field, mixed $value): static * @return $this|QueryBuilder */ public function whereIn(string $field, array $values): QueryBuilder|static - { - if (empty($values)) return $this; + { + if (empty($values)) return $this; - return $this->where("$field IN('".implode("', '", $values)."')"); - } + return $this->where("$field IN('" . implode("', '", $values) . "')"); + } /** * @param string $field @@ -134,7 +134,7 @@ public function whereNotIn(string $field, array $values): QueryBuilder|static { if (empty($values)) return $this; - return $this->where("$field NOT IN('".implode("', '", $values)."')"); + return $this->where("$field NOT IN('" . implode("', '", $values) . "')"); } /** @@ -171,6 +171,7 @@ public function params(array $params): static * @param boolean $isCrossPartition * @return $this */ + public function findAll(bool $isCrossPartition = false): static { $this->response = null; @@ -229,10 +230,10 @@ public function setPartitionKey($fieldName): static * @return null */ public function getPartitionKey() - { - return $this->partitionKey; + { + return $this->partitionKey; } - + /** * @param $fieldName * @return $this @@ -248,9 +249,9 @@ public function setPartitionValue($fieldName): static * @return null */ public function getPartitionValue() - { - return $this->partitionValue; - } + { + return $this->partitionValue; + } /** * @param string $string @@ -267,8 +268,8 @@ public function setQueryString(string $string): static */ public function getQueryString(): ?string { - return $this->queryString; - } + return $this->queryString; + } /** * @param string $partitionKey @@ -285,6 +286,106 @@ public function isNested(string $partitionKey): bool|static return str_contains($partitionKey, '/'); } + /** + * @param string $path Instances of / in property names within $path must be escaped as /~1 + * @param mixed $value + * @return array Fully formed ADD patch operation element + */ + + public function getPatchOpAdd(string $path, mixed $value): array + { + + $op = [ + 'op' => 'add', + 'path' => str_replace('~', '~0', $path), + 'value' => $value + ]; + return $op; + } + + /** + * @param string $path Instances of / in property names within $path must be escaped as /~1 + * @param mixed $value + * @return array Fully formed SET patch operation element + */ + + public function getPatchOpSet(string $path, mixed $value): array + { + + $op = [ + 'op' => 'set', + 'path' => str_replace('~', '~0', $path), + 'value' => $value + ]; + return $op; + } + + /** + * @param string $path Instances of / in property names within $path must be escaped as /~1 + * @param mixed $value + * @return array Fully formed REPLACE patch operation element + */ + + public function getPatchOpReplace(string $path, mixed $value): array + { + + $op = [ + 'op' => 'replace', + 'path' => str_replace('~', '~0', $path), + 'value' => $value + ]; + return $op; + } + + /** + * @param string $path Instances of / in property names within $path must be escaped as /~1 + * @return array Fully formed REMOVE patch operation element + */ + + public function getPatchOpRemove(string $path): array + { + + $op = [ + 'op' => 'remove', + 'path' => str_replace('~', '~0', $path), + ]; + return $op; + } + + /** + * @param string $path Instances of / in property names within $path must be escaped as /~1 + * @param int $value + * @return array Fully formed INCR patch operation element + */ + + public function getPatchOpIncrement(string $path, int $value): array + { + + $op = [ + 'op' => 'replace', + 'path' => str_replace('~', '~0', $path), + 'value' => $value + ]; + return $op; + } + + /** + * @param string $fromPath Source property - Instances of / in property names within $path must be escaped as /~1 + * @param string $toPath Destination property - Instances of / in property names within $path must be escaped as /~1 + * @return array Fully formed MOVE patch operation element + */ + + public function patchOpMove(string $fromPath, string $toPath): array + { + + $op = [ + 'op' => 'replace', + 'from' => str_replace('~', '~0', $fromPath), + 'path' => str_replace('~', '~0', $toPath), + ]; + return $op; + } + /** * Find and set the partition value * @@ -305,7 +406,7 @@ public function findPartitionValue(object $document, bool $toString = false) # formatted as a cosmos query string if ($toString) { - foreach( $properties as $p ) { + foreach ($properties as $p) { $this->setQueryString($p); } @@ -315,7 +416,7 @@ public function findPartitionValue(object $document, bool $toString = false) # and find the value of the property key else { - foreach( $properties as $p ) { + foreach ($properties as $p) { $document = (object)$document->{$p}; } @@ -354,13 +455,49 @@ public function save($document) return $resultObj->_rid ?? null; } + /** + * @param $rid_doc + * @param $patchOps Array Array of operations, max 10 per request + * @return string|null + * @throws \Exception + */ + public function patch(string $rid_doc, array $patchOps) + { + if (count($patchOps) > 10) { + //Throw an equivalent HTTP error rather than waste a request to have the API return the same + throw new \Exception("400 : PATCH supports maximum of 10 operations per request"); + }; + + $partitionValue = $this->partitionKey != null ? $this->partitionValue : null; + //Conditional patch is possible - check if QueryBuilder was set up with a condition and append + $condition = ($this->where) ? 'from ' . $this->from . ' where ' . $this->where : ''; + + $updates = []; + if ($condition) { + $updates['condition'] = $condition; + }; + $updates['operations'] = $patchOps; + $json = json_encode($updates); + + $result = $this->collection->patchDocument($rid_doc, $json, $partitionValue, $this->triggersAsHeaders("patch")); + $resultObj = json_decode($result); + + if (isset($resultObj->code) && isset($resultObj->message)) { + throw new \Exception("$resultObj->code : $resultObj->message"); + } + + return $resultObj->_rid ?? null; + } + + + /* delete */ /** * @param boolean $isCrossPartition * @return boolean */ - public function delete(bool $isCrossPartition = false) :bool + public function delete(bool $isCrossPartition = false): bool { $this->response = null; @@ -382,7 +519,7 @@ public function delete(bool $isCrossPartition = false) :bool * @param boolean $isCrossPartition * @return boolean */ - public function deleteAll(bool $isCrossPartition = false) :bool + public function deleteAll(bool $isCrossPartition = false): bool { $this->response = null; @@ -410,7 +547,7 @@ public function deleteAll(bool $isCrossPartition = false) :bool public function addTrigger(string $operation, string $type, string $id): self { $operation = \strtolower($operation); - if (!\in_array($operation, ["all", "create", "delete", "replace"])) + if (!\in_array($operation, ["all", "create", "delete", "replace", "patch"])) throw new \Exception("Trigger: Invalid operation \"{$operation}\""); $type = \strtolower($type); @@ -432,7 +569,7 @@ protected function triggersAsHeaders(string $operation): array { $headers = []; - // Add headers for the current operation type at $operation (create|delete!replace) + // Add headers for the current operation type at $operation (create|delete!replace|patch) if (isset($this->triggers[$operation])) { foreach ($this->triggers[$operation] as $name => $ids) { $ids = \is_array($ids) ? $ids : [$ids]; @@ -520,7 +657,7 @@ public function toArray($arrayKey = null): array|null $results = (array)$this->toObject($arrayKey); if ($this->multipleResults && is_array($results)) { - array_walk($results, function(&$value) { + array_walk($results, function (&$value) { $value = (array)$value; }); } @@ -533,9 +670,8 @@ public function toArray($arrayKey = null): array|null * @param null $default * @return mixed */ - public function getValue($fieldName, $default = null) + public function getValue($fieldName, $default = null) { return ($this->toObject())->{$fieldName} ?? $default; } - } From 1c4c53fb4467b4895dc3dbaccfd594c953c1d11f Mon Sep 17 00:00:00 2001 From: Matthew Bee Date: Mon, 17 Jul 2023 13:12:50 +0100 Subject: [PATCH 2/2] Correcting function naming to align with convention and adding readme example Function name correction to follow convention Adding example of basic PATCH usage Adding limitation of multi-document patch using transactions --- README.md | 52 +++++++++++++++++++++++++++++++++++++++++++- src/QueryBuilder.php | 2 +- 2 files changed, 52 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 164c00a..64e8b7a 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,9 @@ Include jupitern/cosmosdb in your project, by adding it to your composer.json fi ## Changelog +### v2.7.0 +- adding support for PATCH verb (CosmosDb add-on PATCH API) with Add, Set, Replace, Remove, Increment and Move operations, including "getPatchOp[OP]" helper functions + ### v2.6.0 - code refactor. min PHP verion supported is now 8.0 - selectCollection no longer creates a colletion if not exist. use createCollection for that @@ -43,7 +46,8 @@ Include jupitern/cosmosdb in your project, by adding it to your composer.json fi This package adds additional functionalities to the [AzureDocumentDB-PHP](https://github.com/cocteau666/AzureDocumentDB-PHP) package. All other functionality exists in this package as well. ## Limitations -Use of `limit()` or `order()` in cross-partition queries is currently not supported. +- Use of `limit()` or `order()` in cross-partition queries is currently not supported. +- Multi-document patch using transaction based requests is currently not supported. ## Usage @@ -90,6 +94,52 @@ $rid = \Jupitern\CosmosDb\QueryBuilder::instance() ]); ``` +### Patching Records + +```php + +# Patch operations: ADD | SET | REPLACE | REMOVE | INCR | MOVE +# https://learn.microsoft.com/en-us/azure/cosmos-db/partial-document-update#similarities-and-differences + +# Where a PartitionKey is in use, the PartitionValue should be set on the QueryBuilder instance +# prior to making any PATCH operations, as by the nature of PATCH, there is no document body to find the value in, +# and the value is taken from the class property when the request is made. $rid_doc is also required because PATCH is an item-level operation. + +# Example starting document (as array) + +# [ +# "_rid" => $rid, +# 'id' => '2', +# 'name' => 'Jane Doe Something', +# 'age' => 36, +# 'country' => 'Portugal' +# ] + +$res = \Jupitern\CosmosDb\QueryBuilder::instance() + ->setCollection($collection) + ->setPartitionKey('country') + ->setPartitionValue('Portugal'); + +# Patch operations can be batched, so the $operations argument is an array of arrays +# Batch patch operations are limited to 10 operations per request +$operations[] = $res->getPatchOpSet('/age', 38); +$operations[] = $res->getPatchOpAdd('/region' 'Algarve'); + +$rid_doc = $res->patch($rid_doc, $operations); + +# Example patched document (as array) + +# [ +# "_rid" => $rid, +# 'id' => '2', +# 'name' => 'Jane Doe Something', +# 'age' => 38, +# 'country' => 'Portugal' +# 'region' => 'Algarve' +# ] + +``` + ### Querying Records ```php diff --git a/src/QueryBuilder.php b/src/QueryBuilder.php index a42671b..f5d4ffd 100644 --- a/src/QueryBuilder.php +++ b/src/QueryBuilder.php @@ -375,7 +375,7 @@ public function getPatchOpIncrement(string $path, int $value): array * @return array Fully formed MOVE patch operation element */ - public function patchOpMove(string $fromPath, string $toPath): array + public function getPatchOpMove(string $fromPath, string $toPath): array { $op = [