Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -225,7 +225,7 @@ public async Task<BundleResponse> Handle(BundleRequest request, CancellationToke
else if (_bundleType == BundleType.Transaction)
{
// For resources within a transaction, we need to validate if they are referring to each other and throw an exception in such case.
await _transactionBundleValidator.ValidateBundle(bundleResource, _referenceIdDictionary, cancellationToken);
await _transactionBundleValidator.ValidateBundle(bundleResource, bundleProcessingLogic, _referenceIdDictionary, cancellationToken);

await FillRequestLists(bundleResource.Entry, cancellationToken);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,35 +45,36 @@
/// It also validates if the request operations within a entry is a valid operation.
/// If a conditional create or update is executed and a resource exists, the value is populated in the idDictionary.
/// </summary>
/// <param name="bundle"> The input bundle</param>
/// <param name="bundle">The input bundle</param>
/// <param name="processingLogic">Bundle processing logic</param>
/// <param name="idDictionary">The id dictionary that stores fullUrl to actual ids.</param>
/// <param name="cancellationToken"> The cancellation token</param>
public async Task ValidateBundle(Hl7.Fhir.Model.Bundle bundle, IDictionary<string, (string resourceId, string resourceType)> idDictionary, CancellationToken cancellationToken)
public async Task ValidateBundle(Hl7.Fhir.Model.Bundle bundle, BundleProcessingLogic processingLogic, IDictionary<string, (string resourceId, string resourceType)> idDictionary, CancellationToken cancellationToken)
{
EnsureArg.IsNotNull(bundle, nameof(bundle));

var resourceIdList = new HashSet<string>(StringComparer.OrdinalIgnoreCase);

foreach (var entry in bundle.Entry)
{
if (ShouldValidateBundleEntry(entry))
if (ShouldValidateBundleEntry(entry, processingLogic))
{
string resourceId = await GetResourceId(entry, idDictionary, cancellationToken);
string conditionalCreateQuery = entry.Request.IfNoneExist;

if (!string.IsNullOrEmpty(resourceId))
{
// Throw exception if resourceId is already present in the hashset.
if (resourceIdList.Contains(resourceId))
{
string requestUrl = BuildRequestUrlForConditionalQueries(entry, conditionalCreateQuery);
throw new RequestNotValidException(string.Format(Api.Resources.ResourcesMustBeUnique, requestUrl));
}

resourceIdList.Add(resourceId);
}
}
}

Check notice

Code scanning / CodeQL

Missed opportunity to use Where Note

This foreach loop
implicitly filters its target sequence
- consider filtering the sequence explicitly using '.Where(...)'.
}

private static string BuildRequestUrlForConditionalQueries(EntryComponent entry, string conditionalCreateQuery)
Expand Down Expand Up @@ -138,7 +139,7 @@
return string.Empty;
}

private static bool ShouldValidateBundleEntry(EntryComponent entry)
private static bool ShouldValidateBundleEntry(EntryComponent entry, BundleProcessingLogic processingLogic)
{
string requestUrl = entry.Request?.Url;
if (string.IsNullOrWhiteSpace(requestUrl))
Expand All @@ -160,8 +161,9 @@
throw new RequestNotValidException(string.Format(Api.Resources.InvalidBundleEntry, entry.Request.Url, requestMethod));
}

// Conditional Delete operation is not currently supported.
// However, hard deletes (DELETE {type}/{id}?_hardDelete=true) are allowed.
// Conditional Delete operation is NOT supported.
// However, hard deletes (DELETE {type}/{id}?_hardDelete=true) are allowed in SEQUENTIAL BUNDLES, as they use C# Transactions.
// Hard deletes and historical purge in PARALLEL BUNDLES are not allowed, as they are not covered by a unique SQL Transaction (Work item #182638).
// Conditional deletes have the pattern: DELETE {type}?{query} (no resource ID).
if (requestMethod == HTTPVerb.DELETE && requestUrl.Contains('?', StringComparison.Ordinal))
{
Expand All @@ -173,6 +175,13 @@
{
throw new RequestNotValidException(string.Format(Api.Resources.InvalidBundleEntry, entry.Request.Url, requestMethod));
}

// TODO: 182638 - Add support to hard deletes in parallel processing mode.
if (processingLogic == BundleProcessingLogic.Parallel &&
(requestUrl.Contains("_hardDelete=true", StringComparison.OrdinalIgnoreCase) || requestUrl.Contains("_purge=true", StringComparison.OrdinalIgnoreCase)))
{
throw new RequestNotValidException(string.Format(Api.Resources.InvalidBundleEntry, entry.Request.Url, requestMethod));
}
}

// Resource type bundle is not supported within a bundle.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -850,7 +850,23 @@ public async Task<ResourceWrapper> GetAsync(ResourceKey key, CancellationToken c

public async Task HardDeleteAsync(ResourceKey key, bool keepCurrentVersion, bool allowPartialSuccess, CancellationToken cancellationToken)
{
await _sqlStoreClient.HardDeleteAsync(_model.GetResourceTypeId(key.ResourceType), key.Id, keepCurrentVersion, _coreFeatures.SupportsResourceChangeCapture, cancellationToken);
short resourceTypeId = _model.GetResourceTypeId(key.ResourceType);

// Handles hard delete within a transaction scope.
if (_sqlTransactionHandler.SqlTransactionScope != null)
{
using (SqlCommand cmd = SqlStoreClient.CreateHardDeleteSqlCommand(resourceTypeId, key.Id, keepCurrentVersion, _coreFeatures.SupportsResourceChangeCapture))
using (SqlConnectionWrapper conn = await _sqlConnectionWrapperFactory.ObtainSqlConnectionWrapperAsync(cancellationToken, enlistInTransaction: true))
{
cmd.Connection = conn.SqlConnection;
cmd.Transaction = conn.SqlTransaction;
await cmd.ExecuteNonQueryAsync(cancellationToken);
}
}
else
{
await _sqlStoreClient.HardDeleteAsync(resourceTypeId, key.Id, keepCurrentVersion, _coreFeatures.SupportsResourceChangeCapture, cancellationToken);
}
}

public async Task BulkUpdateSearchParameterIndicesAsync(IReadOnlyCollection<ResourceWrapper> resources, CancellationToken cancellationToken)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,12 +45,20 @@ public SqlStoreClient(ISqlRetryService sqlRetryService, ILogger<SqlStoreClient>

public async Task HardDeleteAsync(short resourceTypeId, string resourceId, bool keepCurrentVersion, bool isResourceChangeCaptureEnabled, CancellationToken cancellationToken)
{
using var cmd = new SqlCommand() { CommandText = "dbo.HardDeleteResource", CommandType = CommandType.StoredProcedure };
using var cmd = CreateHardDeleteSqlCommand(resourceTypeId, resourceId, keepCurrentVersion, isResourceChangeCaptureEnabled);
await cmd.ExecuteNonQueryAsync(_sqlRetryService, _logger, cancellationToken);
}

internal static SqlCommand CreateHardDeleteSqlCommand(short resourceTypeId, string resourceId, bool keepCurrentVersion, bool isResourceChangeCaptureEnabled)
{
SqlCommand cmd = new SqlCommand() { CommandText = "dbo.HardDeleteResource", CommandType = CommandType.StoredProcedure };

cmd.Parameters.AddWithValue("@ResourceTypeId", resourceTypeId);
cmd.Parameters.AddWithValue("@ResourceId", resourceId);
cmd.Parameters.AddWithValue("@KeepCurrentVersion", keepCurrentVersion);
cmd.Parameters.AddWithValue("@IsResourceChangeCaptureEnabled", isResourceChangeCaptureEnabled);
await cmd.ExecuteNonQueryAsync(_sqlRetryService, _logger, cancellationToken);

return cmd;
}

internal async Task TryLogEvent(string process, string status, string text, DateTime? startDate, CancellationToken cancellationToken)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,7 @@
<None Remove="TestFiles\Normative\Bundle-TransactionWithMetaHistory.json" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="TestFiles\Normative\Bundle-TransactionWithDelete.json" />
<EmbeddedResource Include="TestFiles\Normative\Bundle-TransactionWithMetaHistory.json" />
<EmbeddedResource Include="TestFiles\Normative\Bundle-BatchWithDuplicatedItems.json" />
<EmbeddedResource Include="TestFiles\Normative\Bundle-BatchWithConditionalUpdateByIdentifier.json" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,16 @@
"url": "Patient/patient-rollback-0"
}
},
{
"request": {
"method": "DELETE",
"url": "Patient/patient-rollback-1"
}
},
{
"resource": {
"resourceType": "Patient",
"id": "patient-rollback-1",
"id": "patient-rollback-2",
"identifier": [
{
"use": "official",
Expand Down Expand Up @@ -46,7 +52,7 @@
},
"request": {
"method": "PUT",
"url": "Patient/patient-rollback-1",
"url": "Patient/patient-rollback-2",
"ifMatch": "W/\"2112\""
}
},
Expand Down Expand Up @@ -75,7 +81,7 @@
},
"request": {
"method": "PUT",
"url": "Observation/observation-rollback-1",
"url": "Observation/observation-rollback-0",
"ifMatch": "W/\"2112\""
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
{
"resourceType": "Bundle",
"type": "transaction",
"entry": [
{
"request": {
"method": "DELETE",
"url": "Patient/patient-validresource-0"
}
},
{
"request": {
"method": "DELETE",
"url": "Patient/patient-validresource-1"
}
},
{
"resource": {
"resourceType": "Patient",
"id": "patient-validresource-2",
"identifier": [
{
"use": "official",
"system": "https://github.com/synthetichealth/synthea",
"value": "patient-rollback-1"
}
],
"name": [
{
"use": "official",
"family": "Foo",
"given": [ "Bar patient-rollback-1" ]
}
],
"telecom": [
{
"system": "phone",
"value": "555-106-9045",
"use": "work"
}
],
"gender": "male",
"birthDate": "2005-02-05",
"address": [
{
"line": [ "426 Fadel Approach" ],
"city": "Redmond",
"state": "WA",
"country": "USA"
}
]
},
"request": {
"method": "PUT",
"url": "Patient/patient-validresource-2"
}
},
{
"resource": {
"resourceType": "Observation",
"id": "observation-validresource-0",
"status": "final",
"code": {
"coding": [
{
"system": "http://loinc.org",
"code": "55284-4"
}
],
"text": "Blood pressure"
},
"subject": { "reference": "Patient/patient-rollback-1" },
"effectiveDateTime": "2026-01-29T16:30:23.8621387-05:00",
"valueQuantity": {
"value": 120,
"unit": "mmHg",
"system": "http://unitsofmeasure.org",
"code": "mmHg"
}
},
"request": {
"method": "PUT",
"url": "Observation/observation-validresource-0"
}
}
]
}
Loading
Loading