From 84d5b714eff27a10463df49f9f6c120facab8761 Mon Sep 17 00:00:00 2001 From: Geoff McElhanon Date: Tue, 27 Aug 2024 16:59:16 -0500 Subject: [PATCH] =?UTF-8?q?[ODS-6480]=20Schr=C3=B6dinger=E2=80=99s=20stude?= =?UTF-8?q?ntContactAssociation=20(v7.2)=20(#1125)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Database/Querying/QueryBuilder.cs | 45 +- .../Database/Querying/SqlBuilderHelpers.cs | 14 + .../Models/Resource/ResourceProperty.cs | 19 +- .../ChangeQueriesDatabaseConstants.cs | 5 + .../DeletedItemsQueryBuilderFactory.cs | 73 +- ...kedChangesIdentifierProjectionsProvider.cs | 4 +- ...Deletes Test Suite.postman_collection.json | 977 +++++++++++++++++- 7 files changed, 1063 insertions(+), 74 deletions(-) create mode 100644 Application/EdFi.Ods.Common/Database/Querying/SqlBuilderHelpers.cs diff --git a/Application/EdFi.Ods.Common/Database/Querying/QueryBuilder.cs b/Application/EdFi.Ods.Common/Database/Querying/QueryBuilder.cs index f2749e780e..7be2121206 100644 --- a/Application/EdFi.Ods.Common/Database/Querying/QueryBuilder.cs +++ b/Application/EdFi.Ods.Common/Database/Querying/QueryBuilder.cs @@ -32,7 +32,7 @@ public QueryBuilder(Dialect dialect) /// /// /// - private QueryBuilder(Dialect dialect, ParameterIndexer parameterIndexer) + public QueryBuilder(Dialect dialect, ParameterIndexer parameterIndexer) { _dialect = dialect; _parameterIndexer = parameterIndexer; @@ -57,6 +57,11 @@ private QueryBuilder(Dialect dialect, SqlBuilder sqlBuilder, string tableName, P private IDictionary Parameters { get; } = new Dictionary(); + public ParameterIndexer ParameterIndexer + { + get => _parameterIndexer; + } + /// /// Clones the current instance with all its state so it can be used as a starting point for future queries. /// @@ -385,33 +390,33 @@ public Cte(string name, QueryBuilder queryBuilder, object parameters = null) public object Parameters { get; } } + } - protected class ParameterIndexer - { - private int _parameterIndex = -1; + public class ParameterIndexer + { + private int _parameterIndex = -1; - public ParameterIndexer() { } + public ParameterIndexer() { } - private ParameterIndexer(int parameterIndex) - { - _parameterIndex = parameterIndex; - } + private ParameterIndexer(int parameterIndex) + { + _parameterIndex = parameterIndex; + } - public int Increment() => Interlocked.Increment(ref _parameterIndex); + public int Increment() => Interlocked.Increment(ref _parameterIndex); - /// - /// Gets the next parameter name, incrementing the index with a call to . - /// - /// - public string NextParameterName() => $"@p{Increment()}"; + /// + /// Gets the next parameter name, incrementing the index with a call to . + /// + /// + public string NextParameterName() => $"@p{Increment()}"; - public ParameterIndexer Clone() - { - return new ParameterIndexer(_parameterIndex); - } + public ParameterIndexer Clone() + { + return new ParameterIndexer(_parameterIndex); } } - + public class Join { private readonly List<(string first, string second, string @operator)> _segments = diff --git a/Application/EdFi.Ods.Common/Database/Querying/SqlBuilderHelpers.cs b/Application/EdFi.Ods.Common/Database/Querying/SqlBuilderHelpers.cs new file mode 100644 index 0000000000..cc44ebbab6 --- /dev/null +++ b/Application/EdFi.Ods.Common/Database/Querying/SqlBuilderHelpers.cs @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +namespace EdFi.Ods.Common.Database.Querying; + +public static class SqlBuilderHelpers +{ + public static string Alias(this string tableName, string alias) + { + return $"{tableName} AS {alias}"; + } +} diff --git a/Application/EdFi.Ods.Common/Models/Resource/ResourceProperty.cs b/Application/EdFi.Ods.Common/Models/Resource/ResourceProperty.cs index 17f8d94362..edbeecad48 100644 --- a/Application/EdFi.Ods.Common/Models/Resource/ResourceProperty.cs +++ b/Application/EdFi.Ods.Common/Models/Resource/ResourceProperty.cs @@ -52,7 +52,8 @@ public class ResourceProperty : ResourceMemberBase { private readonly ResourceMemberBase _containingMember; private Lazy _descriptorResource; - + private Lazy _isUniqueIdUsage; + public ResourceProperty(ResourceClassBase resourceClass, EntityProperty entityProperty) : this(null, resourceClass, entityProperty) { } @@ -66,7 +67,7 @@ public ResourceProperty(ResourceMemberBase containingMember, ResourceClassBase r // Assign property characteristics IsDescriptorUsage = entityProperty.IsDescriptorUsage; IsDirectDescriptorUsage = entityProperty.IsDirectDescriptorUsage; - + IsIdentifying = entityProperty.IsIdentifying || (UniqueIdConventions.IsUniqueId(entityProperty.PropertyName) && entityProperty.Entity.IsPersonEntity() @@ -128,6 +129,10 @@ private void InitializeLazyMembers() () => EntityProperty.DescriptorEntity == null ? null : ResourceClass?.ResourceModel?.GetResourceByFullName(EntityProperty.DescriptorEntity.FullName)); + + _isUniqueIdUsage = new Lazy( + () => EntityProperty.DefiningProperty.Entity.IsPersonEntity() + && EntityProperty.DefiningProperty.Entity != ResourceClass.Entity); } public string DescriptorName { get; } @@ -178,7 +183,15 @@ public bool IsLookup /// Indicates whether the property represents the usage of a descriptor. /// public bool IsDescriptorUsage { get; } - + + /// + /// Indicates whether the property is a usage of a person-type unique identifier. + /// + public bool IsUniqueIdUsage + { + get => _isUniqueIdUsage.Value; + } + public bool IsServerAssigned { get; } /// diff --git a/Application/EdFi.Ods.Features/ChangeQueries/ChangeQueriesDatabaseConstants.cs b/Application/EdFi.Ods.Features/ChangeQueries/ChangeQueriesDatabaseConstants.cs index fd0564f750..9273b4ef69 100644 --- a/Application/EdFi.Ods.Features/ChangeQueries/ChangeQueriesDatabaseConstants.cs +++ b/Application/EdFi.Ods.Features/ChangeQueries/ChangeQueriesDatabaseConstants.cs @@ -27,6 +27,11 @@ public class ChangeQueriesDatabaseConstants /// public const string OldKeyValueColumnPrefix = "Old"; + /// + /// Prefix applied to an USI column name containing the current value (for the tracked UniqueId value). + /// + public const string CurrentKeyValueColumnPrefix = "Current"; + /// /// Prefix applied to the identifier column name containing the new value. /// diff --git a/Application/EdFi.Ods.Features/ChangeQueries/Repositories/DeletedItems/DeletedItemsQueryBuilderFactory.cs b/Application/EdFi.Ods.Features/ChangeQueries/Repositories/DeletedItems/DeletedItemsQueryBuilderFactory.cs index 5959261a1b..9b6a34bc93 100644 --- a/Application/EdFi.Ods.Features/ChangeQueries/Repositories/DeletedItems/DeletedItemsQueryBuilderFactory.cs +++ b/Application/EdFi.Ods.Features/ChangeQueries/Repositories/DeletedItems/DeletedItemsQueryBuilderFactory.cs @@ -6,11 +6,14 @@ using System; using System.Collections.Concurrent; using System.Linq; +using EdFi.Common.Extensions; using EdFi.Ods.Common.Database.NamingConventions; using EdFi.Ods.Common.Database.Querying; using EdFi.Ods.Common.Models.Domain; using EdFi.Ods.Common.Models.Resource; +using static EdFi.Ods.Features.ChangeQueries.ChangeQueriesDatabaseConstants; + namespace EdFi.Ods.Features.ChangeQueries.Repositories.DeletedItems { public class DeletedItemsQueryBuilderFactory : IDeletedItemsQueryBuilderFactory @@ -19,19 +22,23 @@ public class DeletedItemsQueryBuilderFactory : IDeletedItemsQueryBuilderFactory private readonly ITrackedChangesIdentifierProjectionsProvider _trackedChangesIdentifierProjectionsProvider; private const string SourceTableAlias = "src"; + private const string CurrentTableAliasPrefix = "curr"; private readonly ConcurrentDictionary _queryBuilderByResourceName = new(); private readonly Func _createQueryBuilder; + private readonly Func _createQueryBuilderWithIndexer; public DeletedItemsQueryBuilderFactory( IDatabaseNamingConvention namingConvention, ITrackedChangesIdentifierProjectionsProvider trackedChangesIdentifierProjectionsProvider, - Func createQueryBuilder) + Func createQueryBuilder, + Func createQueryBuilderWithIndexer) { _namingConvention = namingConvention; _trackedChangesIdentifierProjectionsProvider = trackedChangesIdentifierProjectionsProvider; _createQueryBuilder = createQueryBuilder; + _createQueryBuilderWithIndexer = createQueryBuilderWithIndexer; } public QueryBuilder CreateQueryBuilder(Resource resource) @@ -51,16 +58,62 @@ private QueryBuilder CreateBaselineQueryBuilder(Resource resource) var identifierProjections = _trackedChangesIdentifierProjectionsProvider.GetIdentifierProjections(resource); - var baselineDeletedItemsQuery = QueryFactoryHelper.CreateBaseTrackedChangesQuery(_createQueryBuilder, _namingConvention, entity) + // USIs needing translation to "current" values + var subjectUsiProperties = entity.Identifier.Properties + .Where(p => p.DefiningProperty.Entity.IsPersonEntity()) + .ToArray(); + + QueryBuilder baselineDeletedItemsQuery; + + if (subjectUsiProperties.Any()) + { + // Build the CTE query + var cteQuery = QueryFactoryHelper.CreateBaseTrackedChangesQuery(_createQueryBuilder, _namingConvention, entity); + cteQuery.Select($"{TrackedChangesAlias}.*"); + + int i = 0; + + foreach (var usiProperty in subjectUsiProperties) + { + string uniqueIdName = usiProperty.PropertyName.ReplaceSuffix("USI", "UniqueId"); + + string tableName = _namingConvention.TableName(usiProperty.DefiningProperty.Entity); + string schemaName = _namingConvention.Schema(usiProperty.DefiningProperty.Entity); + + cteQuery.LeftJoin( + $"{schemaName}.{tableName}".Alias($"{CurrentTableAliasPrefix}{i}"), + $"{TrackedChangesAlias}.{_namingConvention.ColumnName($"Old{uniqueIdName}")}", + $"{CurrentTableAliasPrefix}{i}.{_namingConvention.ColumnName(uniqueIdName)}"); + + // Current USI + cteQuery.Select( + $"{CurrentTableAliasPrefix}{i}.{_namingConvention.ColumnName(usiProperty.DefiningProperty)} AS {_namingConvention.ColumnName($"Current{usiProperty.PropertyName}")}"); + + i++; + } + + string cteName = "TranslatedTrackedChanges"; + + QueryFactoryHelper.ApplyDiscriminatorCriteriaForDerivedEntities(cteQuery, entity, _namingConvention); + + baselineDeletedItemsQuery = _createQueryBuilderWithIndexer(cteQuery.ParameterIndexer) + .From(cteName.Alias(TrackedChangesAlias)) + .With(cteName, cteQuery); + } + else + { + baselineDeletedItemsQuery = QueryFactoryHelper.CreateBaseTrackedChangesQuery(_createQueryBuilder, _namingConvention, entity); + QueryFactoryHelper.ApplyDiscriminatorCriteriaForDerivedEntities(baselineDeletedItemsQuery, entity, _namingConvention); + } + + baselineDeletedItemsQuery .Select( - $"{ChangeQueriesDatabaseConstants.TrackedChangesAlias}.{_namingConvention.ColumnName("Id")}", - $"{ChangeQueriesDatabaseConstants.TrackedChangesAlias}.{_namingConvention.ColumnName(ChangeQueriesDatabaseConstants.ChangeVersionColumnName)}") + $"{TrackedChangesAlias}.{_namingConvention.ColumnName("Id")}", + $"{TrackedChangesAlias}.{_namingConvention.ColumnName(ChangeVersionColumnName)}") .Select(QueryFactoryHelper.IdentifyingColumns(identifierProjections, columnGroups: ColumnGroups.OldValue)) .Distinct() .OrderBy( - $"{ChangeQueriesDatabaseConstants.TrackedChangesAlias}.{_namingConvention.ColumnName(ChangeQueriesDatabaseConstants.ChangeVersionColumnName)}"); - - QueryFactoryHelper.ApplyDiscriminatorCriteriaForDerivedEntities(baselineDeletedItemsQuery, entity, _namingConvention); + $"{TrackedChangesAlias}.{_namingConvention.ColumnName(ChangeVersionColumnName)}"); // Deletes-specific query filters ApplySourceTableExclusionForUndeletedItems(); @@ -78,7 +131,7 @@ void ApplySourceTableExclusionForUndeletedItems() foreach (var projection in identifierProjections) { @join.On( - $"{ChangeQueriesDatabaseConstants.TrackedChangesAlias}.{projection.ChangeTableJoinColumnName}", + $"{TrackedChangesAlias}.{projection.ChangeTableJoinColumnName}", $"{SourceTableAlias}.{projection.SourceTableJoinColumnName}"); } @@ -97,9 +150,9 @@ void ApplyDeletesOnlyCriteria() : entity).Identifier.Properties.First(); string columnName = _namingConvention.ColumnName( - $"{ChangeQueriesDatabaseConstants.NewKeyValueColumnPrefix}{firstIdentifierProperty.PropertyName}"); + $"{NewKeyValueColumnPrefix}{firstIdentifierProperty.PropertyName}"); - baselineDeletedItemsQuery.WhereNull($"{ChangeQueriesDatabaseConstants.TrackedChangesAlias}.{columnName}"); + baselineDeletedItemsQuery.WhereNull($"{TrackedChangesAlias}.{columnName}"); } } } diff --git a/Application/EdFi.Ods.Features/ChangeQueries/Repositories/TrackedChangesIdentifierProjectionsProvider.cs b/Application/EdFi.Ods.Features/ChangeQueries/Repositories/TrackedChangesIdentifierProjectionsProvider.cs index 68b5a0434c..2f00909a92 100644 --- a/Application/EdFi.Ods.Features/ChangeQueries/Repositories/TrackedChangesIdentifierProjectionsProvider.cs +++ b/Application/EdFi.Ods.Features/ChangeQueries/Repositories/TrackedChangesIdentifierProjectionsProvider.cs @@ -12,6 +12,8 @@ using EdFi.Ods.Common.Models.Domain; using EdFi.Ods.Common.Models.Resource; +using static EdFi.Ods.Features.ChangeQueries.ChangeQueriesDatabaseConstants; + namespace EdFi.Ods.Features.ChangeQueries.Repositories { public class TrackedChangesIdentifierProjectionsProvider : ITrackedChangesIdentifierProjectionsProvider @@ -50,7 +52,7 @@ private QueryProjection[] CreateIdentifierProjections(ResourceClassBase resource IsDescriptorUsage = rp.IsDescriptorUsage, // Columns for performing join to source table (if necessary) - ChangeTableJoinColumnName = _namingConvention.ColumnName($"{ChangeQueriesDatabaseConstants.OldKeyValueColumnPrefix}{changeTableJoinProperty.PropertyName}"), + ChangeTableJoinColumnName = _namingConvention.ColumnName($"{(rp.IsUniqueIdUsage ? CurrentKeyValueColumnPrefix : OldKeyValueColumnPrefix)}{changeTableJoinProperty.PropertyName}"), SourceTableJoinColumnName = _namingConvention.ColumnName(rp.EntityProperty), }; }) diff --git a/Postman Test Suite/Ed-Fi ODS-API ChangeQueries Key Changes and Deletes Test Suite.postman_collection.json b/Postman Test Suite/Ed-Fi ODS-API ChangeQueries Key Changes and Deletes Test Suite.postman_collection.json index 55a44fc23c..220db6ac92 100644 --- a/Postman Test Suite/Ed-Fi ODS-API ChangeQueries Key Changes and Deletes Test Suite.postman_collection.json +++ b/Postman Test Suite/Ed-Fi ODS-API ChangeQueries Key Changes and Deletes Test Suite.postman_collection.json @@ -131,8 +131,8 @@ "header": [ { "key": "Content-Type", - "type": "text", - "value": "application/json" + "value": "application/json", + "type": "text" } ], "body": { @@ -475,8 +475,8 @@ "header": [ { "key": "Content-Type", - "type": "text", - "value": "application/json" + "value": "application/json", + "type": "text" } ], "body": { @@ -894,8 +894,8 @@ "header": [ { "key": "Content-Type", - "type": "text", - "value": "application/json" + "value": "application/json", + "type": "text" } ], "body": { @@ -1625,8 +1625,8 @@ "header": [ { "key": "Content-Type", - "type": "text", - "value": "application/json" + "value": "application/json", + "type": "text" } ], "body": { @@ -1965,8 +1965,8 @@ "header": [ { "key": "Content-Type", - "type": "text", - "value": "application/json" + "value": "application/json", + "type": "text" } ], "body": { @@ -2442,8 +2442,8 @@ "header": [ { "key": "Content-Type", - "type": "text", - "value": "application/json" + "value": "application/json", + "type": "text" } ], "body": { @@ -2918,8 +2918,8 @@ "header": [ { "key": "Content-Type", - "type": "text", - "value": "application/json" + "value": "application/json", + "type": "text" } ], "body": { @@ -3679,8 +3679,8 @@ "header": [ { "key": "Content-Type", - "type": "text", - "value": "application/json" + "value": "application/json", + "type": "text" } ], "body": { @@ -4082,8 +4082,8 @@ "header": [ { "key": "Content-Type", - "type": "text", - "value": "application/json" + "value": "application/json", + "type": "text" } ], "body": { @@ -4507,8 +4507,8 @@ "header": [ { "key": "Content-Type", - "type": "text", - "value": "application/json" + "value": "application/json", + "type": "text" } ], "body": { @@ -4848,8 +4848,8 @@ "header": [ { "key": "Content-Type", - "type": "text", - "value": "application/json" + "value": "application/json", + "type": "text" } ], "body": { @@ -5206,6 +5206,903 @@ "response": [] } ] + }, + { + "name": "Student Contact Association (Full Delete/Recreate in Change Window)", + "item": [ + { + "name": "Setup", + "item": [ + { + "name": "Create Student", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is 201\", () => {", + " pm.expect(pm.response.code).to.equal(201);", + "});", + "", + "pm.environment.set('known:student:id', pm.response.headers.one('Location').value.split(\"/\").pop());" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"studentUniqueId\": \"ABC123\",\r\n \"birthDate\": \"2022-10-29\",\r\n \"firstName\": \"Johnny\",\r\n \"lastSurname\": \"Doe\"\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{ApiBaseUrl}}/data/v3/ed-fi/students", + "host": [ + "{{ApiBaseUrl}}" + ], + "path": [ + "data", + "v3", + "ed-fi", + "students" + ] + }, + "description": "Scenario: The caller is assigned a profile and attempts to update a covered resource using a different profile's content type\r\n Given the caller is assigned the \"Test-Profile-StudentOnly-Resource-IncludeAll\" profile\r\n And the caller is using the \"Test-Profile-StudentOnly2-Resource-IncludeAll\" profile\r\n When a POST request with a resource is submitted to students with a request body content type of the appropriate value for the profile in use\r\n Then the response should contain a 403 Forbidden failure indicating that \"based on the assigned profiles, one of the following profile-specific content types is required when updating this resource\"\r\n" + }, + "response": [] + }, + { + "name": "Create School", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is 201\", () => {", + " pm.expect(pm.response.code).to.equal(201);", + "});", + "", + "pm.environment.set('known:school:id', pm.response.headers.one('Location').value.split(\"/\").pop());" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"localEducationAgencyReference\": {\r\n \"localEducationAgencyId\": 255901\r\n },\r\n \"schoolId\": 255901555,\r\n \"nameOfInstitution\": \"{{$randomCompanyName}}\",\r\n \"educationOrganizationCategories\": [\r\n {\r\n \"educationOrganizationCategoryDescriptor\": \"uri://ed-fi.org/EducationOrganizationCategoryDescriptor#School\"\r\n }\r\n ],\r\n \"gradeLevels\": [\r\n {\r\n \"gradeLevelDescriptor\": \"uri://ed-fi.org/GradeLevelDescriptor#Second grade\"\r\n }\r\n ]\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{ApiBaseUrl}}/data/v3/ed-fi/schools", + "host": [ + "{{ApiBaseUrl}}" + ], + "path": [ + "data", + "v3", + "ed-fi", + "schools" + ] + } + }, + "response": [] + }, + { + "name": "Create Student School Association", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is 201\", () => {", + " pm.expect(pm.response.code).to.equal(201);", + "});", + "", + "pm.environment.set('known:studentSchoolAssociation:id', pm.response.headers.one('Location').value.split(\"/\").pop());" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"entryDate\": \"2022-10-29\",\r\n \"schoolReference\": {\r\n \"schoolId\": 255901555\r\n },\r\n \"studentReference\": {\r\n \"studentUniqueId\": \"ABC123\"\r\n },\r\n \"entryGradeLevelDescriptor\": \"uri://ed-fi.org/GradeLevelDescriptor#Second grade\"\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{ApiBaseUrl}}/data/v3/ed-fi/studentSchoolAssociations", + "host": [ + "{{ApiBaseUrl}}" + ], + "path": [ + "data", + "v3", + "ed-fi", + "studentSchoolAssociations" + ] + }, + "description": "Scenario: The caller is assigned a profile and attempts to update a covered resource using a different profile's content type\r\n Given the caller is assigned the \"Test-Profile-StudentOnly-Resource-IncludeAll\" profile\r\n And the caller is using the \"Test-Profile-StudentOnly2-Resource-IncludeAll\" profile\r\n When a POST request with a resource is submitted to students with a request body content type of the appropriate value for the profile in use\r\n Then the response should contain a 403 Forbidden failure indicating that \"based on the assigned profiles, one of the following profile-specific content types is required when updating this resource\"\r\n" + }, + "response": [] + }, + { + "name": "Create Contact", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is 201\", () => {\r", + " pm.expect(pm.response.code).to.equal(201);\r", + "});\r", + "\r", + "pm.environment.set('known:contact:id', pm.response.headers.one('Location').value.split(\"/\").pop());" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"{{ParentOrContactName}}UniqueId\": \"CDE123\",\r\n \"firstName\": \"John\",\r\n \"lastSurname\": \"Doe\"\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{ApiBaseUrl}}/data/v3/ed-fi/{{ParentOrContactCollectionName}}", + "host": [ + "{{ApiBaseUrl}}" + ], + "path": [ + "data", + "v3", + "ed-fi", + "{{ParentOrContactCollectionName}}" + ] + } + }, + "response": [] + }, + { + "name": "Create Student Contact Association", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is 201\", () => {\r", + " pm.expect(pm.response.code).to.equal(201);\r", + "});\r", + "\r", + "pm.environment.set('known:studentContactAssociation:id', pm.response.headers.one('Location').value.split(\"/\").pop());\r", + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"{{ParentOrContactName}}Reference\": {\r\n \"{{ParentOrContactName}}UniqueId\": \"CDE123\"\r\n },\r\n \"studentReference\": {\r\n \"studentUniqueId\": \"ABC123\"\r\n },\r\n \"emergencyContactStatus\": true\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{ApiBaseUrl}}/data/v3/ed-fi/student{{ParentOrContactProperName}}Associations", + "host": [ + "{{ApiBaseUrl}}" + ], + "path": [ + "data", + "v3", + "ed-fi", + "student{{ParentOrContactProperName}}Associations" + ] + } + }, + "response": [] + }, + { + "name": "Get Change Version", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is 200\", () => {\r", + " pm.expect(pm.response.code).to.equal(200);\r", + "});\r", + "\r", + "const responseItem = pm.response.json();\r", + "pm.environment.set('known:minChangeVersion', responseItem.newestChangeVersion + 1);" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{ApiBaseUrl}}/changeQueries/v1/availableChangeVersions", + "host": [ + "{{ApiBaseUrl}}" + ], + "path": [ + "changeQueries", + "v1", + "availableChangeVersions" + ] + } + }, + "response": [] + } + ] + }, + { + "name": "Delete and Recreate", + "item": [ + { + "name": "Delete Student Contact Association", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is 204\", () => {\r", + " pm.expect(pm.response.code).to.equal(204);\r", + "});\r", + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "DELETE", + "header": [], + "url": { + "raw": "{{ApiBaseUrl}}/data/v3/ed-fi/student{{ParentOrContactProperName}}Associations/{{known:studentContactAssociation:id}}", + "host": [ + "{{ApiBaseUrl}}" + ], + "path": [ + "data", + "v3", + "ed-fi", + "student{{ParentOrContactProperName}}Associations", + "{{known:studentContactAssociation:id}}" + ] + } + }, + "response": [] + }, + { + "name": "Delete Contact", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is 204\", () => {\r", + " pm.expect(pm.response.code).to.equal(204);\r", + "});\r", + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "DELETE", + "header": [], + "url": { + "raw": "{{ApiBaseUrl}}/data/v3/ed-fi/{{ParentOrContactCollectionName}}/{{known:contact:id}}", + "host": [ + "{{ApiBaseUrl}}" + ], + "path": [ + "data", + "v3", + "ed-fi", + "{{ParentOrContactCollectionName}}", + "{{known:contact:id}}" + ] + } + }, + "response": [] + }, + { + "name": "Get Deleted Contacts", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is 200\", () => {\r", + " pm.expect(pm.response.code).to.equal(200);\r", + "});\r", + "\r", + "const items = pm.response.json()\r", + "\r", + "pm.test(\"Should return a single item.\", () => {\r", + " pm.expect(items.length).to.equal(1);\r", + "});\r", + "\r", + "const uniqueIdPropertyName = pm.environment.get(\"ParentOrContactName\") + \"UniqueId\";\r", + "\r", + "pm.test(\"Should return the deleted contact.\", () => {\r", + " const item = items[0];\r", + "\r", + " pm.expect(item.keyValues).to.deep.contain({\r", + " [uniqueIdPropertyName]: \"CDE123\"\r", + " });\r", + "});" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{ApiBaseUrl}}/data/v3/ed-fi/{{ParentOrContactCollectionName}}/deletes?MinChangeVersion={{known:minChangeVersion}}", + "host": [ + "{{ApiBaseUrl}}" + ], + "path": [ + "data", + "v3", + "ed-fi", + "{{ParentOrContactCollectionName}}", + "deletes" + ], + "query": [ + { + "key": "MinChangeVersion", + "value": "{{known:minChangeVersion}}" + } + ] + } + }, + "response": [] + }, + { + "name": "Get Deleted Student Contact Associations", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is 200\", () => {\r", + " pm.expect(pm.response.code).to.equal(200);\r", + "});\r", + "\r", + "const items = pm.response.json()\r", + "\r", + "pm.test(\"Should return a single item.\", () => {\r", + " pm.expect(items.length).to.equal(1);\r", + "});\r", + "\r", + "const uniqueIdPropertyName = pm.environment.get(\"ParentOrContactName\") + \"UniqueId\";\r", + "\r", + "pm.test(\"Should return the deleted contact.\", () => {\r", + " const item = items[0];\r", + "\r", + " pm.expect(item.keyValues).to.deep.contain({\r", + " [uniqueIdPropertyName]: \"CDE123\",\r", + " \"studentUniqueId\": \"ABC123\",\r", + " });\r", + "});" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{ApiBaseUrl}}/data/v3/ed-fi/student{{ParentOrContactProperName}}Associations/deletes?MinChangeVersion={{known:minChangeVersion}}", + "host": [ + "{{ApiBaseUrl}}" + ], + "path": [ + "data", + "v3", + "ed-fi", + "student{{ParentOrContactProperName}}Associations", + "deletes" + ], + "query": [ + { + "key": "MinChangeVersion", + "value": "{{known:minChangeVersion}}" + } + ] + } + }, + "response": [] + }, + { + "name": "Recreate Contact", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is 201\", () => {\r", + " pm.expect(pm.response.code).to.equal(201);\r", + "});\r", + "\r", + "pm.environment.set('known:contact:id', pm.response.headers.one('Location').value.split(\"/\").pop());" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"{{ParentOrContactName}}UniqueId\": \"CDE123\",\r\n \"firstName\": \"John\",\r\n \"lastSurname\": \"Doe\",\r\n \"personalTitlePrefix\": \"Sir\"\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{ApiBaseUrl}}/data/v3/ed-fi/{{ParentOrContactCollectionName}}", + "host": [ + "{{ApiBaseUrl}}" + ], + "path": [ + "data", + "v3", + "ed-fi", + "{{ParentOrContactCollectionName}}" + ] + } + }, + "response": [] + }, + { + "name": "Recreate Student Contact Association", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is 201\", () => {\r", + " pm.expect(pm.response.code).to.equal(201);\r", + "});\r", + "\r", + "pm.environment.set('known:studentContactAssociation:id', pm.response.headers.one('Location').value.split(\"/\").pop());\r", + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"{{ParentOrContactName}}Reference\": {\r\n \"{{ParentOrContactName}}UniqueId\": \"CDE123\"\r\n },\r\n \"studentReference\": {\r\n \"studentUniqueId\": \"ABC123\"\r\n },\r\n \"emergencyContactStatus\": true\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{ApiBaseUrl}}/data/v3/ed-fi/student{{ParentOrContactProperName}}Associations", + "host": [ + "{{ApiBaseUrl}}" + ], + "path": [ + "data", + "v3", + "ed-fi", + "student{{ParentOrContactProperName}}Associations" + ] + } + }, + "response": [] + }, + { + "name": "Get Deleted Contacts", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is 200\", () => {\r", + " pm.expect(pm.response.code).to.equal(200);\r", + "});\r", + "\r", + "const items = pm.response.json()\r", + "\r", + "pm.test(\"Should not return the deleted but recreated item.\", () => {\r", + " pm.expect(items.length).to.equal(0);\r", + "});\r", + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{ApiBaseUrl}}/data/v3/ed-fi/{{ParentOrContactCollectionName}}/deletes?MinChangeVersion={{known:minChangeVersion}}", + "host": [ + "{{ApiBaseUrl}}" + ], + "path": [ + "data", + "v3", + "ed-fi", + "{{ParentOrContactCollectionName}}", + "deletes" + ], + "query": [ + { + "key": "MinChangeVersion", + "value": "{{known:minChangeVersion}}" + } + ] + } + }, + "response": [] + }, + { + "name": "Get Deleted Student Contact Associations", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is 200\", () => {\r", + " pm.expect(pm.response.code).to.equal(200);\r", + "});\r", + "\r", + "const items = pm.response.json()\r", + "\r", + "pm.test(\"Should not return the deleted but recreated item.\", () => {\r", + " pm.expect(items.length).to.equal(0);\r", + "});\r", + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{ApiBaseUrl}}/data/v3/ed-fi/student{{ParentOrContactProperName}}Associations/deletes?MinChangeVersion={{known:minChangeVersion}}", + "host": [ + "{{ApiBaseUrl}}" + ], + "path": [ + "data", + "v3", + "ed-fi", + "student{{ParentOrContactProperName}}Associations", + "deletes" + ], + "query": [ + { + "key": "MinChangeVersion", + "value": "{{known:minChangeVersion}}" + } + ] + } + }, + "response": [] + } + ] + }, + { + "name": "Teardown", + "item": [ + { + "name": "Delete Student Contact Association", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is 204\", () => {\r", + " pm.expect(pm.response.code).to.equal(204);\r", + "});\r", + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "DELETE", + "header": [], + "url": { + "raw": "{{ApiBaseUrl}}/data/v3/ed-fi/student{{ParentOrContactProperName}}Associations/{{known:studentContactAssociation:id}}", + "host": [ + "{{ApiBaseUrl}}" + ], + "path": [ + "data", + "v3", + "ed-fi", + "student{{ParentOrContactProperName}}Associations", + "{{known:studentContactAssociation:id}}" + ] + } + }, + "response": [] + }, + { + "name": "Delete Contact", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is 204\", () => {\r", + " pm.expect(pm.response.code).to.equal(204);\r", + "});\r", + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "DELETE", + "header": [], + "url": { + "raw": "{{ApiBaseUrl}}/data/v3/ed-fi/{{ParentOrContactCollectionName}}/{{known:contact:id}}", + "host": [ + "{{ApiBaseUrl}}" + ], + "path": [ + "data", + "v3", + "ed-fi", + "{{ParentOrContactCollectionName}}", + "{{known:contact:id}}" + ] + } + }, + "response": [] + }, + { + "name": "Delete Student School Association", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is 204\", () => {\r", + " pm.expect(pm.response.code).to.equal(204);\r", + "});\r", + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "DELETE", + "header": [], + "url": { + "raw": "{{ApiBaseUrl}}/data/v3/ed-fi/studentSchoolAssociations/{{known:studentSchoolAssociation:id}}", + "host": [ + "{{ApiBaseUrl}}" + ], + "path": [ + "data", + "v3", + "ed-fi", + "studentSchoolAssociations", + "{{known:studentSchoolAssociation:id}}" + ] + } + }, + "response": [] + }, + { + "name": "Delete Student", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is 204\", () => {\r", + " pm.expect(pm.response.code).to.equal(204);\r", + "});\r", + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "DELETE", + "header": [], + "url": { + "raw": "{{ApiBaseUrl}}/data/v3/ed-fi/students/{{known:student:id}}", + "host": [ + "{{ApiBaseUrl}}" + ], + "path": [ + "data", + "v3", + "ed-fi", + "students", + "{{known:student:id}}" + ] + } + }, + "response": [] + }, + { + "name": "Delete School", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is 204\", () => {\r", + " pm.expect(pm.response.code).to.equal(204);\r", + "});\r", + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "DELETE", + "header": [], + "url": { + "raw": "{{ApiBaseUrl}}/data/v3/ed-fi/schools/{{known:school:id}}", + "host": [ + "{{ApiBaseUrl}}" + ], + "path": [ + "data", + "v3", + "ed-fi", + "schools", + "{{known:school:id}}" + ] + } + }, + "response": [] + }, + { + "name": "Clean up Environment Variables", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "// Remove all environment variables that start with \"known:\" or \"supplied:\"\r", + "_.chain(_.keys(pm.environment.toObject()))\r", + " .filter(x => _.startsWith(x, 'known:') || _.startsWith(x, 'supplied:'))\r", + " .each(k => pm.environment.unset(k)).value();" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{ApiBaseUrl}}", + "host": [ + "{{ApiBaseUrl}}" + ] + } + }, + "response": [] + } + ] + } + ], + "description": "ODS-6480" } ] }, @@ -5255,8 +6152,8 @@ "header": [ { "key": "Content-Type", - "type": "text", - "value": "application/json" + "value": "application/json", + "type": "text" } ], "body": { @@ -5798,8 +6695,8 @@ "header": [ { "key": "Content-Type", - "type": "text", - "value": "application/json" + "value": "application/json", + "type": "text" } ], "body": { @@ -6121,8 +7018,8 @@ "header": [ { "key": "Content-Type", - "type": "text", - "value": "application/json" + "value": "application/json", + "type": "text" } ], "body": { @@ -6454,8 +7351,8 @@ "header": [ { "key": "Content-Type", - "type": "text", - "value": "application/json" + "value": "application/json", + "type": "text" } ], "body": { @@ -6892,8 +7789,8 @@ "header": [ { "key": "Content-Type", - "type": "text", - "value": "application/json" + "value": "application/json", + "type": "text" } ], "body": { @@ -9024,8 +9921,8 @@ "header": [ { "key": "Content-Type", - "type": "text", - "value": "application/json" + "value": "application/json", + "type": "text" } ], "body": { @@ -9347,8 +10244,8 @@ "header": [ { "key": "Content-Type", - "type": "text", - "value": "application/json" + "value": "application/json", + "type": "text" } ], "body": { @@ -10039,8 +10936,8 @@ "header": [ { "key": "Content-Type", - "type": "text", - "value": "application/json" + "value": "application/json", + "type": "text" } ], "body": { @@ -10764,8 +11661,8 @@ "header": [ { "key": "Content-Type", - "type": "text", - "value": "application/json" + "value": "application/json", + "type": "text" } ], "body": {