diff --git a/src/DbTools/Program.cs b/src/DbTools/Program.cs
index 7879184d..9a5c7f74 100644
--- a/src/DbTools/Program.cs
+++ b/src/DbTools/Program.cs
@@ -24,7 +24,7 @@ static void Main(string[] args)
string scriptFile = GetScriptFile(versionDirectory);
string toolName = Assembly.GetExecutingAssembly().FullName ?? "DbTools";
File.WriteAllText(scriptFile, $"-- This script is autogenerated from the tool {toolName.Split(',')[0]}. Do not edit manually.{cr}{cr}");
- foreach (string filename in (new DirectoryInfo(funcAndProcDirectory).GetFiles(("*.sql")).Select(f => f.FullName)))
+ foreach (string filename in (new DirectoryInfo(funcAndProcDirectory).GetFiles(("*.sql")).Select(f => f.FullName).OrderBy(f => f)))
{
File.AppendAllText(scriptFile, $"-- {filename.Split(Path.DirectorySeparatorChar)[^1]}:{cr}{File.ReadAllText(filename)}{cr}{cr}");
}
diff --git a/src/Storage/Altinn.Platform.Storage.csproj b/src/Storage/Altinn.Platform.Storage.csproj
index be98a35a..b5feb09f 100644
--- a/src/Storage/Altinn.Platform.Storage.csproj
+++ b/src/Storage/Altinn.Platform.Storage.csproj
@@ -13,7 +13,7 @@
-
+
diff --git a/src/Storage/Controllers/ProcessController.cs b/src/Storage/Controllers/ProcessController.cs
index bf9a07cf..7a15fafe 100644
--- a/src/Storage/Controllers/ProcessController.cs
+++ b/src/Storage/Controllers/ProcessController.cs
@@ -29,6 +29,7 @@ public class ProcessController : ControllerBase
{
private readonly IInstanceRepository _instanceRepository;
private readonly IInstanceEventRepository _instanceEventRepository;
+ private readonly IInstanceAndEventsRepository _instanceAndEventsRepository;
private readonly string _storageBaseAndHost;
private readonly IAuthorization _authorizationService;
private readonly IInstanceEventService _instanceEventService;
@@ -38,18 +39,21 @@ public class ProcessController : ControllerBase
///
/// the instance repository handler
/// the instance event repository service
+ /// the instance and events repository
/// the general settings
/// the authorization service
/// the instance event service
public ProcessController(
IInstanceRepository instanceRepository,
IInstanceEventRepository instanceEventRepository,
+ IInstanceAndEventsRepository instanceAndEventsRepository,
IOptions generalsettings,
IAuthorization authorizationService,
IInstanceEventService instanceEventService)
{
_instanceRepository = instanceRepository;
_instanceEventRepository = instanceEventRepository;
+ _instanceAndEventsRepository = instanceAndEventsRepository;
_storageBaseAndHost = $"{generalsettings.Value.Hostname}/storage/api/v1/";
_authorizationService = authorizationService;
_instanceEventService = instanceEventService;
@@ -80,28 +84,57 @@ public async Task> PutProcess(
return NotFound();
}
- string taskId = null;
- string altinnTaskType = existingInstance.Process?.CurrentTask?.AltinnTaskType;
+ var (action, taskId) = ActionMapping(processState, existingInstance);
- if (processState?.CurrentTask?.FlowType == "AbandonCurrentMoveToNext")
+ bool authorized = await _authorizationService.AuthorizeInstanceAction(existingInstance, action, taskId);
+
+ if (!authorized)
{
- altinnTaskType = "reject";
+ return Forbid();
}
- else if (processState?.CurrentTask?.FlowType is not null
- && processState.CurrentTask.FlowType != "CompleteCurrentMoveToNext")
+
+ UpdateInstance(existingInstance, processState, out var updateProperties);
+
+ Instance updatedInstance = await _instanceRepository.Update(existingInstance, updateProperties);
+
+ if (processState?.CurrentTask?.AltinnTaskType == "signing")
{
- altinnTaskType = processState.CurrentTask.AltinnTaskType;
- taskId = processState.CurrentTask.ElementId;
+ await _instanceEventService.DispatchEvent(InstanceEventType.SentToSign, updatedInstance);
}
- string action = altinnTaskType switch
+ updatedInstance.SetPlatformSelfLinks(_storageBaseAndHost);
+ return Ok(updatedInstance);
+ }
+
+ ///
+ /// Updates the process state of an instance.
+ ///
+ /// The party id of the instance owner.
+ /// The id of the instance that should have its process updated.
+ /// The new process state of the instance (including instance events).
+ ///
+ [Authorize]
+ [HttpPut("instanceandevents")]
+ [Consumes("application/json")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ [ProducesResponseType(StatusCodes.Status400BadRequest)]
+ [Produces("application/json")]
+ public async Task> PutInstanceAndEvents(
+ int instanceOwnerPartyId,
+ Guid instanceGuid,
+ [FromBody] ProcessStateUpdate processStateUpdate)
+ {
+ (Instance existingInstance, _) = await _instanceRepository.GetOne(instanceGuid, true);
+
+ if (existingInstance is null)
{
- "data" or "feedback" => "write",
- "payment" => "pay",
- "confirmation" => "confirm",
- "signing" => "sign",
- _ => altinnTaskType,
- };
+ return NotFound();
+ }
+
+ ProcessState processState = processStateUpdate.State;
+
+ var (action, taskId) = ActionMapping(processState, existingInstance);
bool authorized = await _authorizationService.AuthorizeInstanceAction(existingInstance, action, taskId);
@@ -110,33 +143,16 @@ public async Task> PutProcess(
return Forbid();
}
- // Archiving instance if process was ended
- List updateProperties = [
- nameof(existingInstance.Process),
- nameof(existingInstance.LastChanged),
- nameof(existingInstance.LastChangedBy)
- ];
- if (existingInstance.Process?.Ended is null && processState?.Ended is not null)
- {
- existingInstance.Status ??= new InstanceStatus();
- existingInstance.Status.IsArchived = true;
- existingInstance.Status.Archived = processState.Ended;
- updateProperties.Add(nameof(existingInstance.Status));
- updateProperties.Add(nameof(existingInstance.Status.IsArchived));
- updateProperties.Add(nameof(existingInstance.Status.Archived));
- }
-
- existingInstance.Process = processState;
- existingInstance.LastChangedBy = User.GetUserOrOrgId();
- existingInstance.LastChanged = DateTime.UtcNow;
-
- Instance updatedInstance = await _instanceRepository.Update(existingInstance, updateProperties);
-
+ processStateUpdate.Events ??= [];
+ UpdateInstance(existingInstance, processState, out var updateProperties);
if (processState?.CurrentTask?.AltinnTaskType == "signing")
{
- await _instanceEventService.DispatchEvent(InstanceEventType.SentToSign, updatedInstance);
+ InstanceEvent instanceEvent = _instanceEventService.BuildInstanceEvent(InstanceEventType.SentToSign, existingInstance);
+ processStateUpdate.Events.Add(instanceEvent);
}
+ Instance updatedInstance = await _instanceAndEventsRepository.Update(existingInstance, updateProperties, processStateUpdate.Events);
+
updatedInstance.SetPlatformSelfLinks(_storageBaseAndHost);
return Ok(updatedInstance);
}
@@ -194,5 +210,56 @@ public async Task> GetForAuth(int instanceOwnerPartyId, G
return NotFound($"Unable to find instance {instanceOwnerPartyId}/{instanceGuid}: {message}");
}
+
+ private void UpdateInstance(Instance existingInstance, ProcessState processState, out List updateProperties)
+ {
+ // Archiving instance if process was ended
+ updateProperties = [
+ nameof(existingInstance.Process),
+ nameof(existingInstance.LastChanged),
+ nameof(existingInstance.LastChangedBy)
+ ];
+ if (existingInstance.Process?.Ended is null && processState?.Ended is not null)
+ {
+ existingInstance.Status ??= new InstanceStatus();
+ existingInstance.Status.IsArchived = true;
+ existingInstance.Status.Archived = processState.Ended;
+ updateProperties.Add(nameof(existingInstance.Status));
+ updateProperties.Add(nameof(existingInstance.Status.IsArchived));
+ updateProperties.Add(nameof(existingInstance.Status.Archived));
+ }
+
+ existingInstance.Process = processState;
+ existingInstance.LastChangedBy = User.GetUserOrOrgId();
+ existingInstance.LastChanged = DateTime.UtcNow;
+ }
+
+ private (string Action, string TaskId) ActionMapping(ProcessState processState, Instance existingInstance)
+ {
+ string taskId = null;
+ string altinnTaskType = existingInstance.Process?.CurrentTask?.AltinnTaskType;
+
+ if (processState?.CurrentTask?.FlowType == "AbandonCurrentMoveToNext")
+ {
+ altinnTaskType = "reject";
+ }
+ else if (processState?.CurrentTask?.FlowType is not null
+ && processState.CurrentTask.FlowType != "CompleteCurrentMoveToNext")
+ {
+ altinnTaskType = processState.CurrentTask.AltinnTaskType;
+ taskId = processState.CurrentTask.ElementId;
+ }
+
+ string action = altinnTaskType switch
+ {
+ "data" or "feedback" => "write",
+ "payment" => "pay",
+ "confirmation" => "confirm",
+ "signing" => "sign",
+ _ => altinnTaskType,
+ };
+
+ return (action, taskId);
+ }
}
}
diff --git a/src/Storage/Migration/FunctionsAndProcedures/insertinstanceevents.sql b/src/Storage/Migration/FunctionsAndProcedures/insertinstanceevents.sql
new file mode 100644
index 00000000..bcdc03b6
--- /dev/null
+++ b/src/Storage/Migration/FunctionsAndProcedures/insertinstanceevents.sql
@@ -0,0 +1,10 @@
+
+CREATE OR REPLACE PROCEDURE storage.insertinstanceevents(_instance UUID, _events JSONB)
+ LANGUAGE 'plpgsql'
+AS $BODY$
+BEGIN
+ INSERT INTO storage.instanceevents (instance, alternateid, event)
+ SELECT _instance, (evs->>'Id')::UUID, jsonb_strip_nulls(evs)
+ FROM jsonb_array_elements(_events) evs;
+END;
+$BODY$;
\ No newline at end of file
diff --git a/src/Storage/Migration/v0.15/01-functions-and-procedures.sql b/src/Storage/Migration/v0.15/01-functions-and-procedures.sql
new file mode 100644
index 00000000..1705b62a
--- /dev/null
+++ b/src/Storage/Migration/v0.15/01-functions-and-procedures.sql
@@ -0,0 +1,761 @@
+-- This script is autogenerated from the tool DbTools. Do not edit manually.
+
+-- deletedataelement.sql:
+CREATE OR REPLACE FUNCTION storage.deletedataelement_v2(_alternateid UUID, _instanceGuid UUID, _lastChangedBy TEXT)
+ RETURNS INT
+ LANGUAGE 'plpgsql'
+AS $BODY$
+DECLARE
+ _deleteCount INTEGER;
+BEGIN
+ IF (SELECT COUNT(*) FROM storage.dataelements WHERE element -> 'IsRead' = 'true' AND instanceguid = _instanceGuid) = 0 THEN
+ UPDATE storage.instances
+ SET instance = jsonb_set(instance, '{Status, ReadStatus}', '0')
+ WHERE alternateid = _instanceGuid AND instance -> 'Status' ->> 'ReadStatus' = '1';
+ END IF;
+
+ UPDATE storage.instances
+ SET lastchanged = NOW(),
+ instance = instance
+ || jsonb_set('{"LastChanged":""}', '{LastChanged}', to_jsonb(REPLACE((NOW() AT TIME ZONE 'UTC')::TEXT, ' ', 'T') || 'Z'))
+ || jsonb_set('{"LastChangedBy":""}', '{LastChangedBy}', to_jsonb(_lastChangedBy))
+ WHERE alternateid = (SELECT instanceguid FROM storage.dataelements WHERE alternateid = _alternateid);
+
+ DELETE FROM storage.dataelements WHERE alternateid = _alternateid;
+ GET DIAGNOSTICS _deleteCount = ROW_COUNT;
+
+ RETURN _deleteCount;
+END;
+$BODY$;
+
+-- deletedataelements.sql:
+CREATE OR REPLACE FUNCTION storage.deletedataelements(_instanceguid UUID)
+ RETURNS INT
+ LANGUAGE 'plpgsql'
+AS $BODY$
+DECLARE
+ _deleteCount INTEGER;
+BEGIN
+ UPDATE storage.instances
+ SET lastchanged = NOW(),
+ instance = instance
+ || jsonb_set('{"LastChanged":""}', '{LastChanged}', to_jsonb(REPLACE((NOW() AT TIME ZONE 'UTC')::TEXT, ' ', 'T') || 'Z'))
+ || jsonb_set('{"LastChangedBy":""}', '{LastChangedBy}', to_jsonb('altinn'::TEXT))
+ WHERE alternateid = _instanceguid;
+
+ DELETE FROM storage.dataelements d
+ USING storage.instances i
+ WHERE i.alternateid = d.instanceguid AND i.alternateid = _instanceguid;
+ GET DIAGNOSTICS _deleteCount = ROW_COUNT;
+ RETURN _deleteCount;
+END;
+$BODY$;
+
+-- deleteinstance.sql:
+CREATE OR REPLACE FUNCTION storage.deleteinstance(_alternateid UUID)
+ RETURNS INT
+ LANGUAGE 'plpgsql'
+AS $BODY$
+DECLARE
+ _deleteCount INTEGER;
+BEGIN
+ DELETE FROM storage.instances WHERE alternateid = _alternateid;
+ GET DIAGNOSTICS _deleteCount = ROW_COUNT;
+ RETURN _deleteCount;
+END;
+$BODY$;
+
+-- deleteinstanceevent.sql:
+CREATE OR REPLACE FUNCTION storage.deleteinstanceevent(_instance UUID)
+ RETURNS INT
+ LANGUAGE 'plpgsql'
+AS $BODY$
+DECLARE
+ _deleteCount INTEGER;
+BEGIN
+ DELETE FROM storage.instanceevents WHERE instance = _instance;
+ GET DIAGNOSTICS _deleteCount = ROW_COUNT;
+ RETURN _deleteCount;
+END;
+$BODY$;
+
+-- deletemigrationstate.sql:
+CREATE OR REPLACE PROCEDURE storage.deletemigrationstate (_instanceguid UUID)
+ LANGUAGE 'plpgsql'
+AS $BODY$
+BEGIN
+ DELETE FROM storage.a1migrationstate WHERE instanceguid = _instanceguid;
+ DELETE FROM storage.a2migrationstate WHERE instanceguid = _instanceguid;
+END;
+$BODY$;
+
+-- filterinstanceevent.sql:
+CREATE OR REPLACE FUNCTION storage.filterinstanceevent(_instance UUID, _from TIMESTAMP, _to TIMESTAMP, _eventtype TEXT[])
+ RETURNS TABLE (event JSONB)
+ LANGUAGE 'plpgsql'
+
+AS $BODY$
+BEGIN
+RETURN QUERY
+ SELECT ie.event
+ FROM storage.instanceevents ie
+ WHERE instance = _instance
+ AND (ie.event->>'Created')::TIMESTAMP >= _from
+ AND (ie.event->>'Created')::TIMESTAMP <= _to
+ AND (_eventtype IS NULL OR ie.event->>'EventType' = ANY (_eventtype))
+ ORDER BY ie.event->'Created';
+END;
+$BODY$;
+
+-- inserta1migrationstate.sql:
+CREATE OR REPLACE PROCEDURE storage.inserta1migrationstate (_a1archiveReference BIGINT)
+ LANGUAGE 'plpgsql'
+AS $BODY$
+BEGIN
+ INSERT INTO storage.a1migrationstate (a1archivereference) VALUES
+ (_a1archiveReference)
+ ON CONFLICT (a1archivereference) DO NOTHING;
+END;
+$BODY$;
+
+-- inserta2codelist.sql:
+CREATE OR REPLACE PROCEDURE storage.inserta2codelist (_name TEXT, _language TEXT, _version INT, _codelist TEXT)
+ LANGUAGE 'plpgsql'
+AS $BODY$
+BEGIN
+ INSERT INTO storage.a2codelists (name, language, version, codelist) VALUES
+ (_name, _language, _version, _codelist)
+ ON CONFLICT (name, language, version) DO UPDATE SET codelist = _codelist;
+END;
+$BODY$;
+
+-- inserta2image.sql:
+CREATE OR REPLACE PROCEDURE storage.inserta2image (_name TEXT, _image BYTEA)
+ LANGUAGE 'plpgsql'
+AS $BODY$
+DECLARE
+ _parentId INTEGER;
+BEGIN
+ SELECT id into _parentId FROM storage.a2images WHERE md5(_image) = md5(image);
+ IF _parentId IS NOT NULL THEN
+ INSERT INTO storage.a2images (name, parentid, image) VALUES
+ (_name, _parentId, null)
+ ON CONFLICT (name) DO NOTHING;
+ ELSE
+ INSERT INTO storage.a2images (name, parentid, image) VALUES
+ (_name, null, _image)
+ ON CONFLICT (name) DO UPDATE SET image = _image;
+ END IF;
+END;
+$BODY$;
+
+-- inserta2migrationstate.sql:
+CREATE OR REPLACE PROCEDURE storage.inserta2migrationstate (_a2archiveReference BIGINT)
+ LANGUAGE 'plpgsql'
+AS $BODY$
+BEGIN
+ INSERT INTO storage.a2migrationstate (a2archivereference) VALUES
+ (_a2archiveReference)
+ ON CONFLICT (a2archivereference) DO NOTHING;
+END;
+$BODY$;
+
+-- inserta2xsl.sql:
+CREATE OR REPLACE PROCEDURE storage.inserta2xsl (_org TEXT, _app TEXT, _lformid INT, _language TEXT, _pagenumber INT, _xsl TEXT, _xsltype INT, _isportrait BOOL)
+ LANGUAGE 'plpgsql'
+AS $BODY$
+DECLARE
+ _parentId INTEGER;
+ _appId INTEGER;
+ _applicationinternalid INTEGER;
+BEGIN
+ SELECT id into _applicationinternalid FROM storage.applications WHERE org = _org AND app = _app;
+ SELECT id into _parentId FROM storage.a2xsls WHERE org = _org AND app = _app AND xsl = _xsl AND xsltype = _xsltype;
+ SELECT id into _appId FROM storage.applications WHERE org = _org AND app = _app;
+ IF _parentId IS NOT NULL THEN
+ INSERT INTO storage.a2xsls (org, app, applicationinternalid, parentid, lformid, language, pagenumber, xsl, xsltype, isportrait) VALUES
+ (_org, _app, _applicationinternalid, _parentId, _lformid, _language, _pagenumber, NULL, _xsltype, _isportrait)
+ ON CONFLICT (app, org, lformid, pagenumber, language, xsltype) DO NOTHING;
+ ELSE
+ INSERT INTO storage.a2xsls (org, app, applicationinternalid, parentid, lformid, language, pagenumber, xsl, xsltype, isportrait) VALUES
+ (_org, _app, _applicationinternalid, NULL, _lformid, _language, _pagenumber, _xsl, _xsltype, _isportrait)
+ ON CONFLICT (app, org, lformid, pagenumber, language, xsltype) DO UPDATE SET xsl = _xsl;
+ END IF;
+END;
+$BODY$;
+
+-- insertdataelement.sql:
+CREATE OR REPLACE FUNCTION storage.insertdataelement_v2(
+ IN _instanceinternalid bigint,
+ IN _instanceguid uuid,
+ IN _alternateid uuid,
+ IN _element jsonb)
+ RETURNS TABLE (updatedElement JSONB)
+ LANGUAGE plpgsql
+AS $BODY$
+BEGIN
+ -- Make sure that lastChanged has the Postgres precision (6 digits). The timestamp from C# DateTime and then json serialize has 7 digits
+ _element := _element || jsonb_set('{"LastChanged":""}', '{LastChanged}', to_jsonb(REPLACE(((_element ->> 'LastChanged')::TIMESTAMPTZ AT TIME ZONE 'UTC')::TEXT, ' ', 'T') || 'Z'));
+
+ IF _element ->> 'IsRead' = 'false' THEN
+ UPDATE storage.instances
+ SET instance = jsonb_set(instance, '{Status, ReadStatus}', '2')
+ WHERE id = _instanceinternalid AND instance -> 'Status' ->> 'ReadStatus' = '1';
+ END IF;
+
+ UPDATE storage.instances
+ SET lastchanged = (_element ->> 'LastChanged')::TIMESTAMPTZ,
+ instance = instance
+ || jsonb_set('{"LastChanged":""}', '{LastChanged}', to_jsonb(_element ->> 'LastChanged'))
+ || jsonb_set('{"LastChangedBy":""}', '{LastChangedBy}', to_jsonb(_element ->> 'LastChangedBy'))
+ WHERE id = _instanceinternalid;
+
+ RETURN QUERY
+ INSERT INTO storage.dataelements(instanceinternalid, instanceGuid, alternateid, element) VALUES (_instanceinternalid, _instanceGuid, _alternateid, jsonb_strip_nulls(_element))
+ RETURNING element;
+END;
+$BODY$;
+
+-- insertinstance.sql:
+CREATE OR REPLACE PROCEDURE storage.insertinstance_v2(_partyid BIGINT, _alternateid UUID, _instance JSONB, _created TIMESTAMPTZ, _lastchanged TIMESTAMPTZ, _org TEXT, _appid TEXT, _taskid TEXT, _altinnmainversion INT)
+ LANGUAGE 'plpgsql'
+AS $BODY$
+BEGIN
+ INSERT INTO storage.instances(partyid, alternateid, instance, created, lastchanged, org, appid, taskid, altinnmainversion)
+ VALUES (_partyid, _alternateid, jsonb_strip_nulls(_instance), _created, _lastchanged, _org, _appid, _taskid, _altinnmainversion);
+END;
+$BODY$;
+
+-- insertinstanceevent.sql:
+CREATE OR REPLACE PROCEDURE storage.insertinstanceevent(_instance UUID, _alternateid UUID, _event JSONB)
+ LANGUAGE 'plpgsql'
+AS $BODY$
+BEGIN
+ -- Dummy comment to verify that new migration solution works end to end
+ INSERT INTO storage.instanceevents(instance, alternateid, event) VALUES (_instance, _alternateid, jsonb_strip_nulls(_event));
+END;
+$BODY$;
+
+-- insertinstanceevents.sql:
+
+CREATE OR REPLACE PROCEDURE storage.insertinstanceevents(_instance UUID, _events JSONB)
+ LANGUAGE 'plpgsql'
+AS $BODY$
+BEGIN
+ INSERT INTO storage.instanceevents (instance, alternateid, event)
+ SELECT _instance, (evs->>'Id')::UUID, jsonb_strip_nulls(evs)
+ FROM jsonb_array_elements(_events) evs;
+END;
+$BODY$;
+
+-- reada1migrationstate.sql:
+CREATE OR REPLACE FUNCTION storage.reada1migrationstate(_a1archivereference BIGINT)
+ RETURNS TABLE (instanceguid UUID)
+ LANGUAGE 'plpgsql'
+
+AS $BODY$
+BEGIN
+RETURN QUERY
+ SELECT ms.instanceguid FROM storage.a1migrationstate ms WHERE ms.a1archivereference = _a1archivereference;
+END;
+$BODY$;
+
+-- reada2codelist.sql:
+CREATE OR REPLACE FUNCTION storage.reada2codelist(_name TEXT, _language TEXT)
+ RETURNS TABLE (codelist TEXT)
+ LANGUAGE 'plpgsql'
+
+AS $BODY$
+BEGIN
+RETURN QUERY
+ SELECT c.codelist FROM storage.a2codelists c
+ WHERE name = _name AND language = _language
+ ORDER BY version DESC LIMIT 1;
+END;
+$BODY$;
+
+-- reada2image.sql:
+CREATE OR REPLACE FUNCTION storage.reada2image(_name TEXT)
+ RETURNS TABLE (image BYTEA)
+ LANGUAGE 'plpgsql'
+
+AS $BODY$
+BEGIN
+RETURN QUERY
+ SELECT
+ CASE
+ WHEN c.image IS NOT NULL THEN c.image
+ ELSE (SELECT p.image FROM storage.a2images p WHERE p.id = c.parentid)
+ END
+ FROM storage.a2images c WHERE c.name = _name;
+END;
+$BODY$;
+
+-- reada2migrationstate.sql:
+CREATE OR REPLACE FUNCTION storage.reada2migrationstate(_a2archivereference BIGINT)
+ RETURNS TABLE (instanceguid UUID)
+ LANGUAGE 'plpgsql'
+
+AS $BODY$
+BEGIN
+RETURN QUERY
+ SELECT ms.instanceguid FROM storage.a2migrationstate ms WHERE ms.a2archivereference = _a2archivereference;
+END;
+$BODY$;
+
+-- reada2xsls.sql:
+CREATE OR REPLACE FUNCTION storage.reada2xsls(_org TEXT, _app TEXT, _lformid INT, _language TEXT, _xsltype INT)
+ RETURNS TABLE (xsl TEXT, isportrait BOOL)
+ LANGUAGE 'plpgsql'
+
+AS $BODY$
+BEGIN
+RETURN QUERY
+ SELECT
+ CASE
+ WHEN x.xsl IS NOT NULL THEN x.xsl
+ ELSE (SELECT p.xsl FROM storage.a2xsls p WHERE p.id = x.parentid)
+ END,
+ x.isportrait
+ FROM storage.a2xsls x
+ WHERE x.org = _org and x.app = _app and x.lformid = _lformid and x.language = _language and x.xsltype = _xsltype
+ ORDER BY pagenumber;
+
+END;
+$BODY$;
+
+-- readdataelement.sql:
+CREATE OR REPLACE FUNCTION storage.readdataelement(_alternateid UUID)
+ RETURNS TABLE (element JSONB)
+ LANGUAGE 'plpgsql'
+
+AS $BODY$
+BEGIN
+RETURN QUERY
+ SELECT d.element FROM storage.dataelements d WHERE alternateid = _alternateid;
+
+END;
+$BODY$;
+
+-- readdeletedelements.sql:
+CREATE OR REPLACE FUNCTION storage.readdeletedelements()
+ RETURNS TABLE (id BIGINT, instance JSONB, element JSONB)
+ LANGUAGE 'plpgsql'
+AS $BODY$
+BEGIN
+RETURN QUERY
+ -- Use materialized cte to force join order
+ -- Target index dataelements_deletestatus_harddeleted. This index has a where clause that must match
+ -- the where clause in the data_elements query
+ WITH data_elements AS MATERIALIZED
+ (SELECT d.instanceinternalid, d.element FROM storage.dataelements d
+ WHERE (d.element -> 'DeleteStatus' -> 'IsHardDeleted')::BOOLEAN
+ AND (d.element -> 'DeleteStatus' ->> 'HardDeleted')::TIMESTAMPTZ <= NOW() - (7 ||' days')::interval
+ )
+ SELECT i.id, i.instance, data_elements.element FROM data_elements JOIN storage.instances i ON i.id = data_elements.instanceinternalid;
+ END;
+$BODY$;
+
+-- readdeletedinstances.sql:
+CREATE OR REPLACE FUNCTION storage.readdeletedinstances()
+ RETURNS TABLE (instance JSONB)
+ LANGUAGE 'plpgsql'
+
+AS $BODY$
+BEGIN
+RETURN QUERY
+ -- Make sure that part of the where clause is exactly as in filtered index instances_isharddeleted_and_more
+ SELECT i.instance FROM storage.instances i
+ WHERE (i.instance -> 'Status' -> 'IsHardDeleted')::BOOLEAN AND
+ (
+ NOT (i.instance -> 'Status' -> 'IsArchived')::BOOLEAN
+ OR (i.instance -> 'CompleteConfirmations') IS NOT NULL AND (i.instance -> 'Status' ->> 'HardDeleted')::TIMESTAMPTZ <= (NOW() - (7 ||' days')::INTERVAL)
+ );
+END;
+$BODY$;
+
+
+-- readinstance.sql:
+CREATE OR REPLACE FUNCTION storage.readinstance(_alternateid UUID)
+ RETURNS TABLE (id BIGINT, instance JSONB, element JSONB)
+ LANGUAGE 'plpgsql'
+
+AS $BODY$
+BEGIN
+RETURN QUERY
+ SELECT i.id, i.instance, d.element FROM storage.instances i
+ LEFT JOIN storage.dataelements d ON i.id = d.instanceinternalid
+ WHERE i.alternateid = _alternateid
+ ORDER BY d.id;
+
+END;
+$BODY$;
+
+
+-- readinstanceevent.sql:
+CREATE OR REPLACE FUNCTION storage.readinstanceevent(_alternateid UUID)
+ RETURNS TABLE (event JSONB)
+ LANGUAGE 'plpgsql'
+
+AS $BODY$
+BEGIN
+RETURN QUERY
+ SELECT ie.event FROM storage.instanceevents ie WHERE alternateid = _alternateid;
+
+END;
+$BODY$;
+
+-- readinstancefromquery.sql:
+CREATE OR REPLACE FUNCTION storage.readinstancefromquery_v5(
+ _appId TEXT DEFAULT NULL,
+ _appIds TEXT[] DEFAULT NULL,
+ _archiveReference TEXT DEFAULT NULL,
+ _continue_idx BIGINT DEFAULT 0,
+ _created_eq TIMESTAMPTZ DEFAULT NULL,
+ _created_gt TIMESTAMPTZ DEFAULT NULL,
+ _created_gte TIMESTAMPTZ DEFAULT NULL,
+ _created_lt TIMESTAMPTZ DEFAULT NULL,
+ _created_lte TIMESTAMPTZ DEFAULT NULL,
+ _dueBefore_eq TEXT DEFAULT NULL,
+ _dueBefore_gt TEXT DEFAULT NULL,
+ _dueBefore_gte TEXT DEFAULT NULL,
+ _dueBefore_lt TEXT DEFAULT NULL,
+ _dueBefore_lte TEXT DEFAULT NULL,
+ _excludeConfirmedBy JSONB[] DEFAULT NULL,
+ _includeElements BOOL DEFAULT TRUE,
+ _instanceOwner_partyId INTEGER DEFAULT NULL,
+ _instanceOwner_partyIds INTEGER[] DEFAULT NULL,
+ _lastChanged_eq TIMESTAMPTZ DEFAULT NULL,
+ _lastChanged_gt TIMESTAMPTZ DEFAULT NULL,
+ _lastChanged_gte TIMESTAMPTZ DEFAULT NULL,
+ _lastChanged_idx TIMESTAMPTZ DEFAULT NULL,
+ _lastChanged_lt TIMESTAMPTZ DEFAULT NULL,
+ _lastChanged_lte TIMESTAMPTZ DEFAULT NULL,
+ _mainVersionInclude SMALLINT DEFAULT NULL,
+ _mainVersionExclude SMALLINT DEFAULT NULL,
+ _msgBoxInterval_eq TIMESTAMPTZ DEFAULT NULL,
+ _msgBoxInterval_gt TIMESTAMPTZ DEFAULT NULL,
+ _msgBoxInterval_gte TIMESTAMPTZ DEFAULT NULL,
+ _msgBoxInterval_lt TIMESTAMPTZ DEFAULT NULL,
+ _msgBoxInterval_lte TIMESTAMPTZ DEFAULT NULL,
+ _org TEXT DEFAULT NULL,
+ _process_currentTask TEXT DEFAULT NULL,
+ _process_ended_eq TEXT DEFAULT NULL,
+ _process_ended_gt TEXT DEFAULT NULL,
+ _process_ended_gte TEXT DEFAULT NULL,
+ _process_ended_lt TEXT DEFAULT NULL,
+ _process_ended_lte TEXT DEFAULT NULL,
+ _process_isComplete BOOLEAN DEFAULT NULL,
+ _search_string TEXT DEFAULT NULL,
+ _size INTEGER DEFAULT 100,
+ _sort_ascending BOOLEAN DEFAULT FALSE,
+ _status_isActiveOrSoftDeleted BOOLEAN DEFAULT NULL,
+ _status_isArchived BOOLEAN DEFAULT NULL,
+ _status_isArchivedOrSoftDeleted BOOLEAN DEFAULT NULL,
+ _status_isHardDeleted BOOLEAN DEFAULT NULL,
+ _status_isSoftDeleted BOOLEAN DEFAULT NULL,
+ _visibleAfter_eq TEXT DEFAULT NULL,
+ _visibleAfter_gt TEXT DEFAULT NULL,
+ _visibleAfter_gte TEXT DEFAULT NULL,
+ _visibleAfter_lt TEXT DEFAULT NULL,
+ _visibleAfter_lte TEXT DEFAULT NULL
+ )
+ RETURNS TABLE (id BIGINT, instance JSONB, element JSONB)
+ LANGUAGE 'plpgsql'
+
+AS $BODY$
+BEGIN
+ IF _sort_ascending IS NULL THEN
+ _sort_ascending := false;
+ END IF;
+
+ RETURN QUERY
+ WITH filteredInstances AS
+ (
+ SELECT i.id, i.instance, i.lastchanged FROM storage.instances i
+ WHERE 1 = 1
+ AND (_continue_idx <= 0 OR
+ (_continue_idx > 0 AND _sort_ascending = true AND (i.lastchanged > _lastChanged_idx OR (i.lastchanged = _lastChanged_idx AND i.id > _continue_idx))) OR
+ (_continue_idx > 0 AND _sort_ascending = false AND (i.lastchanged < _lastChanged_idx OR (i.lastchanged = _lastChanged_idx AND i.id < _continue_idx))))
+ AND (_appId IS NULL OR i.appid = _appId)
+ AND (_archiveReference IS NULL OR i.instance ->> 'Id' like '%' || _archiveReference)
+ AND (_created_gte IS NULL OR i.created >= _created_gte)
+ AND (_created_gt IS NULL OR i.created > _created_gt)
+ AND (_created_lte IS NULL OR i.created <= _created_lte)
+ AND (_created_lt IS NULL OR i.created < _created_lt)
+ AND (_created_eq IS NULL OR i.created = _created_eq)
+ AND (_dueBefore_gte IS NULL OR i.instance ->> 'DueBefore' >= _dueBefore_gte)
+ AND (_dueBefore_gt IS NULL OR i.instance ->> 'DueBefore' > _dueBefore_gt)
+ AND (_dueBefore_lte IS NULL OR i.instance ->> 'DueBefore' <= _dueBefore_lte)
+ AND (_dueBefore_lt IS NULL OR i.instance ->> 'DueBefore' < _dueBefore_lt)
+ AND (_dueBefore_eq IS NULL OR i.instance ->> 'DueBefore' = _dueBefore_eq)
+ AND (_excludeConfirmedBy IS NULL OR i.instance -> 'CompleteConfirmations' IS NULL OR NOT i.instance -> 'CompleteConfirmations' @> ANY (_excludeConfirmedBy))
+ AND (_instanceOwner_partyId IS NULL OR partyId = _instanceOwner_partyId)
+ AND (_instanceOwner_partyIds IS NULL OR partyId = ANY(_instanceOwner_partyIds))
+ AND (_lastChanged_gte IS NULL OR i.lastchanged >= _lastChanged_gte)
+ AND (_lastChanged_gt IS NULL OR i.lastchanged > _lastChanged_gt)
+ AND (_lastChanged_lte IS NULL OR i.lastchanged <= _lastChanged_lte)
+ AND (_lastChanged_lt IS NULL OR i.lastchanged < _lastChanged_lt)
+ AND (_lastChanged_eq IS NULL OR i.lastchanged = _lastChanged_eq)
+ AND (_mainVersionInclude IS NULL OR i.altinnmainversion = _mainVersionInclude)
+ AND (_mainVersionExclude IS NULL OR i.altinnmainversion <> _mainVersionExclude)
+ AND (_msgBoxInterval_gte IS NULL OR ((i.instance -> 'Status' -> 'IsArchived')::boolean = false AND i.created >= _msgBoxInterval_gte OR (i.instance -> 'Status' -> 'IsArchived')::boolean = true AND i.lastchanged >= _msgBoxInterval_gte))
+ AND (_msgBoxInterval_gt IS NULL OR ((i.instance -> 'Status' -> 'IsArchived')::boolean = false AND i.created > _msgBoxInterval_gt OR (i.instance -> 'Status' -> 'IsArchived')::boolean = true AND i.lastchanged > _msgBoxInterval_gt))
+ AND (_msgBoxInterval_lte IS NULL OR ((i.instance -> 'Status' -> 'IsArchived')::boolean = false AND i.created <= _msgBoxInterval_lte OR (i.instance -> 'Status' -> 'IsArchived')::boolean = true AND i.lastchanged <= _msgBoxInterval_lte))
+ AND (_msgBoxInterval_lt IS NULL OR ((i.instance -> 'Status' -> 'IsArchived')::boolean = false AND i.created < _msgBoxInterval_lt OR (i.instance -> 'Status' -> 'IsArchived')::boolean = true AND i.lastchanged < _msgBoxInterval_lt))
+ AND (_msgBoxInterval_eq IS NULL OR ((i.instance -> 'Status' -> 'IsArchived')::boolean = false AND i.created = _msgBoxInterval_eq OR (i.instance -> 'Status' -> 'IsArchived')::boolean = true AND i.lastchanged = _msgBoxInterval_eq))
+ AND (_org IS NULL OR i.org = _org)
+ AND (_process_currentTask IS NULL OR i.instance -> 'Process' -> 'CurrentTask' ->> 'ElementId' = _process_currentTask)
+ AND (_process_ended_gte IS NULL OR i.instance -> 'Process' ->> 'Ended' >= _process_ended_gte)
+ AND (_process_ended_gt IS NULL OR i.instance -> 'Process' ->> 'Ended' > _process_ended_gt)
+ AND (_process_ended_lte IS NULL OR i.instance -> 'Process' ->> 'Ended' <= _process_ended_lte)
+ AND (_process_ended_lt IS NULL OR i.instance -> 'Process' ->> 'Ended' < _process_ended_lt)
+ AND (_process_ended_eq IS NULL OR i.instance -> 'Process' ->> 'Ended' = _process_ended_eq)
+ AND (_process_isComplete IS NULL OR (_process_isComplete = TRUE AND i.instance -> 'Process' -> 'Ended' IS NOT NULL) OR (_process_isComplete = FALSE AND i.instance -> 'Process' -> 'CurrentTask' IS NOT NULL))
+ AND (_search_string IS NULL OR (i.appid = ANY(_appIds) OR (EXISTS (SELECT value FROM jsonb_each_text(i.instance -> 'PresentationTexts') WHERE value ilike _search_string))))
+ AND ((_status_isActiveOrSoftDeleted IS NULL OR _status_isActiveOrSoftDeleted = false) OR ((i.instance -> 'Status' -> 'IsArchived')::boolean = false OR (i.instance -> 'Status' -> 'IsSoftDeleted')::boolean = true))
+ AND (_status_isArchived IS NULL OR (i.instance -> 'Status' -> 'IsArchived')::boolean = _status_isArchived)
+ AND ((_status_isArchivedOrSoftDeleted IS NULL OR _status_isArchivedOrSoftDeleted = false) OR ((i.instance -> 'Status' -> 'IsArchived')::boolean = true OR (i.instance -> 'Status' -> 'IsSoftDeleted')::boolean = true))
+ AND (_status_isHardDeleted IS NULL OR (i.instance -> 'Status' -> 'IsHardDeleted')::boolean = _status_isHardDeleted)
+ AND (_status_isSoftDeleted IS NULL OR (i.instance -> 'Status' -> 'IsSoftDeleted')::boolean = _status_isSoftDeleted)
+ AND (_visibleAfter_gte IS NULL OR i.instance ->> 'VisibleAfter' >= _visibleAfter_gte)
+ AND (_visibleAfter_gt IS NULL OR i.instance ->> 'VisibleAfter' > _visibleAfter_gt)
+ AND (_visibleAfter_lte IS NULL OR i.instance ->> 'VisibleAfter' <= _visibleAfter_lte)
+ AND (_visibleAfter_lt IS NULL OR i.instance ->> 'VisibleAfter' < _visibleAfter_lt)
+ AND (_visibleAfter_eq IS NULL OR i.instance ->> 'VisibleAfter' = _visibleAfter_eq)
+ ORDER BY
+ (CASE WHEN _sort_ascending = true THEN i.lastChanged END) ASC,
+ (CASE WHEN _sort_ascending = false THEN i.lastChanged END) DESC,
+ i.id
+ FETCH FIRST _size ROWS ONLY
+ )
+ SELECT filteredInstances.id, filteredInstances.instance, d.element FROM filteredInstances
+ LEFT JOIN storage.dataelements d ON filteredInstances.id = d.instanceInternalId AND _includeElements = TRUE
+ ORDER BY
+ (CASE WHEN _sort_ascending = true THEN filteredInstances.lastChanged END) ASC,
+ (CASE WHEN _sort_ascending = false THEN filteredInstances.lastChanged END) DESC,
+ filteredInstances.id;
+END;
+$BODY$;
+
+-- readinstancenoelements.sql:
+CREATE OR REPLACE FUNCTION storage.readinstancenoelements(_alternateid UUID)
+ RETURNS TABLE (id BIGINT, instance JSONB)
+ LANGUAGE 'plpgsql'
+
+AS $BODY$
+BEGIN
+RETURN QUERY
+ SELECT i.id, i.instance FROM storage.instances i
+ WHERE i.alternateid = _alternateid;
+END;
+$BODY$;
+
+-- updatea1migrationstatestarted.sql:
+CREATE OR REPLACE PROCEDURE storage.updatea1migrationstatestarted (_a1archivereference BIGINT, _instanceguid UUID)
+ LANGUAGE 'plpgsql'
+AS $BODY$
+BEGIN
+ UPDATE storage.a1migrationstate SET instanceguid = _instanceguid, started = now()
+ WHERE a1archivereference = _a1archivereference;
+END;
+$BODY$;
+
+-- updatea2migrationstatestarted.sql:
+CREATE OR REPLACE PROCEDURE storage.updatea2migrationstatestarted (_a2archivereference BIGINT, _instanceguid UUID)
+ LANGUAGE 'plpgsql'
+AS $BODY$
+BEGIN
+ UPDATE storage.a2migrationstate SET instanceguid = _instanceguid, started = now()
+ WHERE a2archivereference = _a2archivereference;
+END;
+$BODY$;
+
+-- updatedataelement.sql:
+CREATE OR REPLACE FUNCTION storage.updatedataelement_v2(_dataelementGuid UUID, _instanceGuid UUID, _elementChanges JSONB, _instanceChanges JSONB, _isReadChangedToFalse BOOL, _lastChanged TIMESTAMPTZ)
+ RETURNS TABLE (updatedElement JSONB)
+ LANGUAGE 'plpgsql'
+AS $BODY$
+DECLARE
+ _lastChanged6digits TEXT;
+BEGIN
+ IF _lastChanged IS NOT NULL
+ THEN
+ -- Make sure that lastChanged has the Postgres precision (6 digits). The timestamp from C# DateTime and then json serialize has 7 digits
+ _lastChanged6digits = REPLACE((_lastChanged AT TIME ZONE 'UTC')::TEXT, ' ', 'T') || 'Z';
+ _elementChanges := _elementChanges || jsonb_set('{"LastChanged":""}', '{LastChanged}', to_jsonb(_lastChanged6digits));
+ END IF;
+
+ IF _isReadChangedToFalse = true AND
+ (SELECT COUNT(*) FROM storage.dataelements
+ WHERE element -> 'IsRead' = 'true' AND instanceguid = _instanceGuid AND alternateid <> _dataelementGuid) = 0
+ THEN
+ UPDATE storage.instances
+ SET instance = jsonb_set(instance, '{Status, ReadStatus}', '0')
+ WHERE alternateid = _instanceGuid AND instance -> 'Status' ->> 'ReadStatus' = '1';
+ END IF;
+
+ IF _lastChanged IS NOT NULL
+ THEN
+ UPDATE storage.instances
+ SET lastchanged = _lastChanged,
+ instance = instance || _instanceChanges || jsonb_set('{"LastChanged":""}', '{LastChanged}', to_jsonb(_lastChanged6digits))
+ WHERE alternateid = _instanceGuid;
+ END IF;
+
+ RETURN QUERY
+ UPDATE storage.dataelements SET element = element || _elementChanges WHERE alternateid = _dataelementGuid
+ RETURNING element;
+END;
+$BODY$;
+
+-- updateinstance.sql:
+CREATE OR REPLACE FUNCTION storage.updateinstance_v2(
+ _alternateid UUID,
+ _toplevelsimpleprops JSONB,
+ _datavalues JSONB,
+ _completeconfirmations JSONB,
+ _presentationtexts JSONB,
+ _status JSONB,
+ _substatus JSONB,
+ _process JSONB,
+ _lastchanged TIMESTAMPTZ,
+ _taskid TEXT)
+ RETURNS TABLE (updatedInstance JSONB)
+ LANGUAGE 'plpgsql'
+AS $BODY$
+BEGIN
+ IF _datavalues IS NOT NULL THEN
+ RETURN QUERY
+ UPDATE storage.instances SET
+ instance = instance || _toplevelsimpleprops ||
+ jsonb_strip_nulls(
+ jsonb_set(
+ '{"DataValues":""}',
+ '{DataValues}',
+ CASE WHEN instance -> 'DataValues' IS NOT NULL THEN
+ instance -> 'DataValues' || _datavalues
+ ELSE
+ _datavalues
+ END
+ )
+ ),
+ lastchanged = _lastchanged
+ WHERE _alternateid = alternateid
+ RETURNING instance;
+ ELSIF _presentationtexts IS NOT NULL THEN
+ RETURN QUERY
+ UPDATE storage.instances SET
+ instance = instance || _toplevelsimpleprops ||
+ jsonb_strip_nulls(
+ jsonb_set(
+ '{"PresentationTexts":""}',
+ '{PresentationTexts}',
+ CASE WHEN instance -> 'PresentationTexts' IS NOT NULL THEN
+ instance -> 'PresentationTexts' || _presentationtexts
+ ELSE
+ _presentationtexts
+ END
+ )
+ ),
+ lastchanged = _lastchanged
+ WHERE _alternateid = alternateid
+ RETURNING instance;
+ ELSIF _completeconfirmations IS NOT NULL THEN
+ RETURN QUERY
+ UPDATE storage.instances SET
+ instance = instance || _toplevelsimpleprops ||
+ jsonb_set(
+ '{"CompleteConfirmations":""}',
+ '{CompleteConfirmations}',
+ CASE WHEN instance -> 'CompleteConfirmations' IS NOT NULL THEN
+ instance -> 'CompleteConfirmations' || _completeconfirmations
+ ELSE
+ _completeconfirmations
+ END
+ ),
+ lastchanged = _lastchanged
+ WHERE _alternateid = alternateid
+ RETURNING instance;
+ ELSIF _status IS NOT NULL AND _process IS NULL THEN
+ RETURN QUERY
+ UPDATE storage.instances SET
+ instance = instance ||
+ jsonb_set(
+ instance || _toplevelsimpleprops,
+ '{Status}',
+ CASE WHEN instance -> 'Status' IS NOT NULL THEN
+ instance -> 'Status' || _status
+ ELSE
+ _status
+ END
+ ),
+ lastchanged = _lastchanged
+ WHERE _alternateid = alternateid
+ RETURNING instance;
+ ELSIF _substatus IS NOT NULL THEN
+ RETURN QUERY
+ UPDATE storage.instances SET
+ instance = instance ||
+ jsonb_set(
+ instance || _toplevelsimpleprops,
+ '{Status, Substatus}',
+ jsonb_strip_nulls(_substatus)
+ ),
+ lastchanged = _lastchanged
+ WHERE _alternateid = alternateid
+ RETURNING instance;
+ ELSIF _process IS NOT NULL AND _status IS NOT NULL THEN
+ RETURN QUERY
+ UPDATE storage.instances SET
+ instance = instance ||
+ jsonb_set(
+ instance || _toplevelsimpleprops,
+ '{Process}',
+ jsonb_strip_nulls(_process)
+ ) ||
+ jsonb_set(
+ '{"Status":""}',
+ '{Status}',
+ CASE WHEN instance -> 'Status' IS NOT NULL THEN
+ instance -> 'Status' || _status
+ ELSE
+ _status
+ END
+ ),
+ lastchanged = _lastchanged,
+ taskid = _taskid
+ WHERE _alternateid = alternateid
+ RETURNING instance;
+ ELSIF _process IS NOT NULL THEN
+ RETURN QUERY
+ UPDATE storage.instances SET
+ instance = instance ||
+ jsonb_set(
+ instance || _toplevelsimpleprops,
+ '{Process}',
+ jsonb_strip_nulls(_process)
+ ),
+ lastchanged = _lastchanged,
+ taskid = _taskid
+ WHERE _alternateid = alternateid
+ RETURNING instance;
+ ELSE
+ RAISE EXCEPTION 'Unexpected parameters to update instance';
+ END IF;
+END;
+$BODY$;
+
+
+-- updatemigrationstatecompleted.sql:
+CREATE OR REPLACE PROCEDURE storage.updatemigrationstatecompleted (_instanceguid UUID)
+ LANGUAGE 'plpgsql'
+AS $BODY$
+BEGIN
+ UPDATE storage.a1migrationstate SET completed = now()
+ WHERE instanceguid = _instanceguid;
+ UPDATE storage.a2migrationstate SET completed = now()
+ WHERE instanceguid = _instanceguid;
+END;
+$BODY$;
+
diff --git a/src/Storage/Repository/IInstanceAndEventsRepository.cs b/src/Storage/Repository/IInstanceAndEventsRepository.cs
new file mode 100644
index 00000000..a1c115fa
--- /dev/null
+++ b/src/Storage/Repository/IInstanceAndEventsRepository.cs
@@ -0,0 +1,20 @@
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using Altinn.Platform.Storage.Interface.Models;
+
+namespace Altinn.Platform.Storage.Repository;
+
+///
+/// Represents an implementation of .
+///
+public interface IInstanceAndEventsRepository
+{
+ ///
+ /// update existing instance including instance events
+ ///
+ /// the instance to update
+ /// a list of which properties should be updated
+ /// the events to add
+ /// The updated instance
+ Task Update(Instance instance, List updateProperties, List events);
+}
diff --git a/src/Storage/Repository/PgInstanceAndEventsRepository.cs b/src/Storage/Repository/PgInstanceAndEventsRepository.cs
new file mode 100644
index 00000000..e0fc0bf8
--- /dev/null
+++ b/src/Storage/Repository/PgInstanceAndEventsRepository.cs
@@ -0,0 +1,86 @@
+using System;
+using System.Collections.Generic;
+using System.Data;
+using System.Linq;
+using System.Threading.Tasks;
+using Altinn.Platform.Storage.Interface.Models;
+using Microsoft.ApplicationInsights;
+using Microsoft.Extensions.Logging;
+using Npgsql;
+using NpgsqlTypes;
+
+namespace Altinn.Platform.Storage.Repository;
+
+///
+/// Represents an implementation of .
+///
+public class PgInstanceAndEventsRepository : IInstanceAndEventsRepository
+{
+ private readonly ILogger _logger;
+ private readonly NpgsqlDataSource _dataSource;
+ private readonly TelemetryClient _telemetryClient;
+ private readonly IInstanceRepository _instanceRepository;
+
+ private readonly string _insertInstanceEventsSql = "call storage.insertinstanceevents($1, $2)";
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The logger to use when writing to logs.
+ /// The npgsql data source.
+ /// Instance repo
+ /// Telemetry client
+ public PgInstanceAndEventsRepository(
+ ILogger logger,
+ NpgsqlDataSource dataSource,
+ IInstanceRepository instanceRepository,
+ TelemetryClient telemetryClient = null)
+ {
+ _logger = logger;
+ _dataSource = dataSource;
+ _instanceRepository = instanceRepository;
+ _telemetryClient = telemetryClient;
+ }
+
+ ///
+ public async Task Update(Instance instance, List updateProperties, List events)
+ {
+ if (events.Count == 0)
+ {
+ return await _instanceRepository.Update(instance, updateProperties);
+ }
+
+ foreach (var instanceEvent in events)
+ {
+ instanceEvent.Id ??= Guid.NewGuid();
+ }
+
+ // Remove last decimal digit to make postgres TIMESTAMPTZ equal to json serialized DateTime
+ instance.LastChanged = instance.LastChanged != null ? new DateTime((((DateTime)instance.LastChanged).Ticks / 10) * 10, DateTimeKind.Utc) : null;
+ List dataElements = instance.Data;
+
+ PgInstanceRepository.ToInternal(instance);
+ instance.Data = null;
+ await using NpgsqlBatch batch = _dataSource.CreateBatch();
+
+ NpgsqlBatchCommand updateCommand = new(PgInstanceRepository.UpdateSql);
+ PgInstanceRepository.BuildUpdateCommand(instance, updateProperties, updateCommand.Parameters);
+ batch.BatchCommands.Add(updateCommand);
+
+ NpgsqlBatchCommand insertEventsComand = new(_insertInstanceEventsSql);
+ insertEventsComand.Parameters.AddWithValue(NpgsqlDbType.Uuid, new Guid(instance.Id.Split('/').Last()));
+ insertEventsComand.Parameters.AddWithValue(NpgsqlDbType.Jsonb, events);
+ batch.BatchCommands.Add(insertEventsComand);
+
+ await using NpgsqlDataReader reader = await batch.ExecuteReaderAsync();
+
+ if (await reader.ReadAsync())
+ {
+ instance = await reader.GetFieldValueAsync("updatedInstance");
+ }
+
+ instance.Data = dataElements; // TODO: requery instead?
+
+ return PgInstanceRepository.ToExternal(instance);
+ }
+}
diff --git a/src/Storage/Repository/PgInstanceRepository.cs b/src/Storage/Repository/PgInstanceRepository.cs
index 6d8456fd..cbdf59e5 100644
--- a/src/Storage/Repository/PgInstanceRepository.cs
+++ b/src/Storage/Repository/PgInstanceRepository.cs
@@ -28,7 +28,12 @@ public class PgInstanceRepository : IInstanceRepository
private const string _readSqlFilteredInitial = "select * from storage.readinstancefromquery_v5 (";
private readonly string _deleteSql = "select * from storage.deleteinstance ($1)";
private readonly string _insertSql = "call storage.insertinstance_v2 (@_partyid, @_alternateid, @_instance, @_created, @_lastchanged, @_org, @_appid, @_taskid, @_altinnmainversion)";
- private readonly string _updateSql = "select * from storage.updateinstance_v2 (@_alternateid, @_toplevelsimpleprops, @_datavalues, @_completeconfirmations, @_presentationtexts, @_status, @_substatus, @_process, @_lastchanged, @_taskid)";
+
+ ///
+ /// SQL for updating an instance.
+ ///
+ internal static readonly string UpdateSql = "select * from storage.updateinstance_v2 (@_alternateid, @_toplevelsimpleprops, @_datavalues, @_completeconfirmations, @_presentationtexts, @_status, @_substatus, @_process, @_lastchanged, @_taskid)";
+
private readonly string _readSql = "select * from storage.readinstance ($1)";
private readonly string _readSqlFiltered = _readSqlFilteredInitial;
private readonly string _readDeletedSql = "select * from storage.readdeletedinstances ()";
@@ -372,18 +377,9 @@ public async Task Update(Instance instance, List updatePropert
ToInternal(instance);
instance.Data = null;
- await using NpgsqlCommand pgcom = _dataSource.CreateCommand(_updateSql);
+ await using NpgsqlCommand pgcom = _dataSource.CreateCommand(UpdateSql);
using TelemetryTracker tracker = new(_telemetryClient, pgcom);
- pgcom.Parameters.AddWithValue("_alternateid", NpgsqlDbType.Uuid, new Guid(instance.Id));
- pgcom.Parameters.AddWithValue("_toplevelsimpleprops", NpgsqlDbType.Jsonb, CustomSerializer.Serialize(instance, updateProperties));
- pgcom.Parameters.AddWithValue("_datavalues", NpgsqlDbType.Jsonb, updateProperties.Contains(nameof(instance.DataValues)) ? instance.DataValues : DBNull.Value);
- pgcom.Parameters.AddWithValue("_completeconfirmations", NpgsqlDbType.Jsonb, updateProperties.Contains(nameof(instance.CompleteConfirmations)) ? instance.CompleteConfirmations : DBNull.Value);
- pgcom.Parameters.AddWithValue("_presentationtexts", NpgsqlDbType.Jsonb, updateProperties.Contains(nameof(instance.PresentationTexts)) ? instance.PresentationTexts : DBNull.Value);
- pgcom.Parameters.AddWithValue("_status", NpgsqlDbType.Jsonb, updateProperties.Contains(nameof(instance.Status)) ? CustomSerializer.Serialize(instance.Status, updateProperties) : DBNull.Value);
- pgcom.Parameters.AddWithValue("_substatus", NpgsqlDbType.Jsonb, updateProperties.Contains(nameof(instance.Status.Substatus)) ? instance.Status.Substatus : DBNull.Value);
- pgcom.Parameters.AddWithValue("_process", NpgsqlDbType.Jsonb, updateProperties.Contains(nameof(instance.Process)) ? instance.Process : DBNull.Value);
- pgcom.Parameters.AddWithValue("_lastchanged", NpgsqlDbType.TimestampTz, instance.LastChanged ?? DateTime.UtcNow);
- pgcom.Parameters.AddWithValue("_taskid", NpgsqlDbType.Text, instance.Process?.CurrentTask?.ElementId ?? (object)DBNull.Value);
+ BuildUpdateCommand(instance, updateProperties, pgcom.Parameters);
await using NpgsqlDataReader reader = await pgcom.ExecuteReaderAsync();
if (await reader.ReadAsync())
@@ -396,15 +392,44 @@ public async Task Update(Instance instance, List updatePropert
return ToExternal(instance);
}
- private static void ToInternal(Instance instance)
+ ///
+ /// Builds the update command for the instance.
+ ///
+ /// Instance
+ /// Updated props
+ /// Parameters
+ internal static void BuildUpdateCommand(Instance instance, List updateProperties, NpgsqlParameterCollection parameters)
+ {
+ parameters.AddWithValue("_alternateid", NpgsqlDbType.Uuid, new Guid(instance.Id));
+ parameters.AddWithValue("_toplevelsimpleprops", NpgsqlDbType.Jsonb, CustomSerializer.Serialize(instance, updateProperties));
+ parameters.AddWithValue("_datavalues", NpgsqlDbType.Jsonb, updateProperties.Contains(nameof(instance.DataValues)) ? instance.DataValues : DBNull.Value);
+ parameters.AddWithValue("_completeconfirmations", NpgsqlDbType.Jsonb, updateProperties.Contains(nameof(instance.CompleteConfirmations)) ? instance.CompleteConfirmations : DBNull.Value);
+ parameters.AddWithValue("_presentationtexts", NpgsqlDbType.Jsonb, updateProperties.Contains(nameof(instance.PresentationTexts)) ? instance.PresentationTexts : DBNull.Value);
+ parameters.AddWithValue("_status", NpgsqlDbType.Jsonb, updateProperties.Contains(nameof(instance.Status)) ? CustomSerializer.Serialize(instance.Status, updateProperties) : DBNull.Value);
+ parameters.AddWithValue("_substatus", NpgsqlDbType.Jsonb, updateProperties.Contains(nameof(instance.Status.Substatus)) ? instance.Status.Substatus : DBNull.Value);
+ parameters.AddWithValue("_process", NpgsqlDbType.Jsonb, updateProperties.Contains(nameof(instance.Process)) ? instance.Process : DBNull.Value);
+ parameters.AddWithValue("_lastchanged", NpgsqlDbType.TimestampTz, instance.LastChanged ?? DateTime.UtcNow);
+ parameters.AddWithValue("_taskid", NpgsqlDbType.Text, instance.Process?.CurrentTask?.ElementId ?? (object)DBNull.Value);
+ }
+
+ ///
+ /// Converts the instance to internal format.
+ ///
+ /// Instance
+ internal static void ToInternal(Instance instance)
{
if (instance.Id.Contains('/', StringComparison.Ordinal))
{
instance.Id = instance.Id.Split('/')[1];
}
}
-
- private static Instance ToExternal(Instance instance)
+
+ ///
+ /// Converts the instance to external format.
+ ///
+ /// Instance
+ ///
+ internal static Instance ToExternal(Instance instance)
{
if (!instance.Id.Contains('/', StringComparison.Ordinal))
{
diff --git a/src/Storage/Services/IInstanceEventService.cs b/src/Storage/Services/IInstanceEventService.cs
index e1878e88..70db2db8 100644
--- a/src/Storage/Services/IInstanceEventService.cs
+++ b/src/Storage/Services/IInstanceEventService.cs
@@ -10,6 +10,14 @@ namespace Altinn.Platform.Storage.Services
///
public interface IInstanceEventService
{
+ ///
+ /// Construct an instance event given a type
+ ///
+ /// Event type
+ /// Instance
+ ///
+ public InstanceEvent BuildInstanceEvent(InstanceEventType eventType, Instance instance);
+
///
/// Dispatch an instance event to the repository
///
diff --git a/src/Storage/Services/InstanceEventService.cs b/src/Storage/Services/InstanceEventService.cs
index 438dab17..0f1abfb0 100644
--- a/src/Storage/Services/InstanceEventService.cs
+++ b/src/Storage/Services/InstanceEventService.cs
@@ -28,7 +28,7 @@ public InstanceEventService(IInstanceEventRepository repository, IHttpContextAcc
}
///
- public async Task DispatchEvent(InstanceEventType eventType, Instance instance)
+ public InstanceEvent BuildInstanceEvent(InstanceEventType eventType, Instance instance)
{
var user = _contextAccessor.HttpContext.User;
@@ -48,6 +48,14 @@ public async Task DispatchEvent(InstanceEventType eventType, Instance instance)
Created = DateTime.UtcNow,
};
+ return instanceEvent;
+ }
+
+ ///
+ public async Task DispatchEvent(InstanceEventType eventType, Instance instance)
+ {
+ var instanceEvent = BuildInstanceEvent(eventType, instance);
+
await _repository.InsertInstanceEvent(instanceEvent);
}
diff --git a/test/UnitTest/Extensions/ServiceCollectionExtensions.cs b/test/UnitTest/Extensions/ServiceCollectionExtensions.cs
index 79b054a1..9ffc373c 100644
--- a/test/UnitTest/Extensions/ServiceCollectionExtensions.cs
+++ b/test/UnitTest/Extensions/ServiceCollectionExtensions.cs
@@ -31,6 +31,7 @@ public static IServiceCollection AddPostgresRepositories(this IServiceCollection
.AddSingleton()
.AddSingleton()
.AddSingleton()
+ .AddSingleton()
.AddSingleton()
.AddNpgsqlDataSource(connectionString, builder => builder.EnableDynamicJson());
}
diff --git a/test/UnitTest/Mocks/Repository/InstanceAndEventsRepositoryMock.cs b/test/UnitTest/Mocks/Repository/InstanceAndEventsRepositoryMock.cs
new file mode 100644
index 00000000..984252ed
--- /dev/null
+++ b/test/UnitTest/Mocks/Repository/InstanceAndEventsRepositoryMock.cs
@@ -0,0 +1,22 @@
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using Altinn.Platform.Storage.Interface.Models;
+using Altinn.Platform.Storage.Repository;
+
+namespace Altinn.Platform.Storage.UnitTest.Mocks.Repository
+{
+ public class InstanceAndEventsRepositoryMock : IInstanceAndEventsRepository
+ {
+ public Task Update(Instance instance, List updateProperties, List events)
+ {
+ if (instance.Id.Equals("1337/d3b326de-2dd8-49a1-834a-b1d23b11e540"))
+ {
+ return Task.FromResult(null);
+ }
+
+ instance.Data = new List();
+
+ return Task.FromResult(instance);
+ }
+ }
+}
diff --git a/test/UnitTest/TestingControllers/ProcessControllerTest.cs b/test/UnitTest/TestingControllers/ProcessControllerTest.cs
index 6dc4f092..5c213e27 100644
--- a/test/UnitTest/TestingControllers/ProcessControllerTest.cs
+++ b/test/UnitTest/TestingControllers/ProcessControllerTest.cs
@@ -5,15 +5,13 @@
using System.Net.Http.Headers;
using System.Net.Http.Json;
using System.Threading.Tasks;
-
+using Altinn.Common.AccessToken.Configuration;
using Altinn.Common.AccessToken.Services;
using Altinn.Common.PEP.Interfaces;
using Altinn.Platform.Storage.Clients;
using Altinn.Platform.Storage.Controllers;
-using Altinn.Platform.Storage.Interface.Enums;
using Altinn.Platform.Storage.Interface.Models;
using Altinn.Platform.Storage.Repository;
-using Altinn.Platform.Storage.Services;
using Altinn.Platform.Storage.Tests.Mocks;
using Altinn.Platform.Storage.UnitTest.Fixture;
using Altinn.Platform.Storage.UnitTest.Mocks;
@@ -49,6 +47,46 @@ public ProcessControllerTest(TestApplicationFactory factory)
_factory = factory;
}
+ private async Task SendUpdateRequest(
+ bool useInstanceAndEventsEndpoint,
+ string token,
+ string instanceId = null,
+ IInstanceRepository instanceRepository = null,
+ IInstanceAndEventsRepository instanceAndEventsRepository = null,
+ Action configure = null)
+ {
+ instanceId ??= "1337/20b1353e-91cf-44d6-8ff7-f68993638ffe";
+ string requestUri = $"storage/api/v1/instances/{instanceId}/process/";
+ JsonContent jsonString;
+ if (useInstanceAndEventsEndpoint)
+ {
+ requestUri += "instanceandevents/";
+ ProcessStateUpdate update = new();
+ ProcessState state = update.State = new();
+ configure?.Invoke(state);
+ jsonString = JsonContent.Create(update, new MediaTypeHeaderValue("application/json"));
+ }
+ else
+ {
+ ProcessState state = new();
+ configure?.Invoke(state);
+ jsonString = JsonContent.Create(state, new MediaTypeHeaderValue("application/json"));
+ }
+
+ HttpClient client = GetTestClient(instanceRepository, instanceAndEventsRepository);
+ client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
+
+ // Act
+ return await client.PutAsync(requestUri, jsonString);
+ }
+
+ public static TheoryData UpdateTestParameters =>
+ new()
+ {
+ { true },
+ { false },
+ };
+
///
/// Test case: User has to low authentication level.
/// Expected: Returns status forbidden.
@@ -64,7 +102,7 @@ public async Task GetProcessHistory_UserHasToLowAuthLv_ReturnStatusForbidden()
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
// Act
- HttpResponseMessage response = await client.GetAsync(requestUri);
+ using HttpResponseMessage response = await client.GetAsync(requestUri);
// Assert
Assert.Equal(HttpStatusCode.Forbidden, response.StatusCode);
@@ -84,7 +122,7 @@ public async Task GetProcessHistory_ReponseIsDeny_ReturnStatusForbidden()
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
// Act
- HttpResponseMessage response = await client.GetAsync(requestUri);
+ using HttpResponseMessage response = await client.GetAsync(requestUri);
// Assert
Assert.Equal(HttpStatusCode.Forbidden, response.StatusCode);
@@ -105,7 +143,7 @@ public async Task GetProcessHistory_UserIsAuthorized_ReturnsEmptyProcessHistoryR
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
// Act
- HttpResponseMessage response = await client.GetAsync(requestUri);
+ using HttpResponseMessage response = await client.GetAsync(requestUri);
string responseString = await response.Content.ReadAsStringAsync();
ProcessHistoryList processHistory = JsonConvert.DeserializeObject(responseString);
@@ -118,21 +156,15 @@ public async Task GetProcessHistory_UserIsAuthorized_ReturnsEmptyProcessHistoryR
/// Test case: User has to low authentication level.
/// Expected: Returns status forbidden.
///
- [Fact]
- public async Task PutProcess_UserHasToLowAuthLv_ReturnStatusForbidden()
+ [Theory]
+ [MemberData(nameof(UpdateTestParameters))]
+ public async Task PutProcess_UserHasToLowAuthLv_ReturnStatusForbidden(bool useInstanceAndEventsEndpoint)
{
// Arrange
- string requestUri = $"storage/api/v1/instances/1337/ae3fe2fa-1fcb-42b4-8e63-69a42d4e3502/process/";
-
- ProcessState state = new ProcessState();
- JsonContent jsonString = JsonContent.Create(state, new MediaTypeHeaderValue("application/json"));
-
- HttpClient client = GetTestClient();
string token = PrincipalUtil.GetToken(3, 1337, 1);
- client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
// Act
- HttpResponseMessage response = await client.PutAsync(requestUri, jsonString);
+ using HttpResponseMessage response = await SendUpdateRequest(useInstanceAndEventsEndpoint, token: token, instanceId: "1337/ae3fe2fa-1fcb-42b4-8e63-69a42d4e3502");
// Assert
Assert.Equal(HttpStatusCode.Forbidden, response.StatusCode);
@@ -142,21 +174,15 @@ public async Task PutProcess_UserHasToLowAuthLv_ReturnStatusForbidden()
/// Test case: Response is deny.
/// Expected: Returns status forbidden.
///
- [Fact]
- public async Task PutProcess_PDPResponseIsDeny_ReturnStatusForbidden()
+ [Theory]
+ [MemberData(nameof(UpdateTestParameters))]
+ public async Task PutProcess_PDPResponseIsDeny_ReturnStatusForbidden(bool useInstanceAndEventsEndpoint)
{
// Arrange
- string requestUri = $"storage/api/v1/instances/1337/ae3fe2fa-1fcb-42b4-8e63-69a42d4e3502/process/";
-
- ProcessState state = new ProcessState();
- JsonContent jsonString = JsonContent.Create(state, new MediaTypeHeaderValue("application/json"));
-
- HttpClient client = GetTestClient();
string token = PrincipalUtil.GetToken(-1, 1);
- client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
// Act
- HttpResponseMessage response = await client.PutAsync(requestUri, jsonString);
+ using HttpResponseMessage response = await SendUpdateRequest(useInstanceAndEventsEndpoint, token: token, instanceId: "1337/ae3fe2fa-1fcb-42b4-8e63-69a42d4e3502");
// Assert
Assert.Equal(HttpStatusCode.Forbidden, response.StatusCode);
@@ -166,20 +192,15 @@ public async Task PutProcess_PDPResponseIsDeny_ReturnStatusForbidden()
/// Test case: User is Authorized
/// Expected: Returns status ok.
///
- [Fact]
- public async Task PutProcess_UserIsAuthorized_ReturnStatusOK()
+ [Theory]
+ [MemberData(nameof(UpdateTestParameters))]
+ public async Task PutProcess_UserIsAuthorized_ReturnStatusOK(bool useInstanceAndEventsEndpoint)
{
- // Arrange
- string requestUri = $"storage/api/v1/instances/1337/20a1353e-91cf-44d6-8ff7-f68993638ffe/process/";
- ProcessState state = new ProcessState();
- JsonContent jsonString = JsonContent.Create(state, new MediaTypeHeaderValue("application/json"));
-
- HttpClient client = GetTestClient();
+ // Arrange
string token = PrincipalUtil.GetToken(3, 1337, 3);
- client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
// Act
- HttpResponseMessage response = await client.PutAsync(requestUri, jsonString);
+ using HttpResponseMessage response = await SendUpdateRequest(useInstanceAndEventsEndpoint, token: token, instanceId: "1337/20a1353e-91cf-44d6-8ff7-f68993638ffe");
// Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
@@ -189,24 +210,23 @@ public async Task PutProcess_UserIsAuthorized_ReturnStatusOK()
/// Test case: Uses want to go back to a earlier state
/// Expected: Returns status ok.
///
- [Fact]
- public async Task PutProcessGatewayReturn_UserIsAuthorized_ReturnStatusOK()
+ [Theory]
+ [MemberData(nameof(UpdateTestParameters))]
+ public async Task PutProcessGatewayReturn_UserIsAuthorized_ReturnStatusOK(bool useInstanceAndEventsEndpoint)
{
- // Arrange
- string requestUri = $"storage/api/v1/instances/1337/20b1353e-91cf-44d6-8ff7-f68993638ffe/process/";
- ProcessState state = new ProcessState();
- state.CurrentTask = new ProcessElementInfo();
- state.CurrentTask.ElementId = "Task_1";
- state.CurrentTask.FlowType = "AbandonCurrentReturnToNext";
- state.CurrentTask.AltinnTaskType = "data";
- JsonContent jsonString = JsonContent.Create(state, new MediaTypeHeaderValue("application/json"));
-
- HttpClient client = GetTestClient();
+ // Arrange
string token = PrincipalUtil.GetToken(3, 1337, 3);
- client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
// Act
- HttpResponseMessage response = await client.PutAsync(requestUri, jsonString);
+ using HttpResponseMessage response = await SendUpdateRequest(useInstanceAndEventsEndpoint, token: token, instanceId: "1337/20b1353e-91cf-44d6-8ff7-f68993638ffe", configure: state =>
+ {
+ state.CurrentTask = new ProcessElementInfo
+ {
+ ElementId = "Task_1",
+ FlowType = "AbandonCurrentReturnToNext",
+ AltinnTaskType = "data",
+ };
+ });
// Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
@@ -216,20 +236,15 @@ public async Task PutProcessGatewayReturn_UserIsAuthorized_ReturnStatusOK()
/// Test case: User wants to updates process on confirimation task. User does not have role required
/// Expected: Returns forbidden.
///
- [Fact]
- public async Task PutProcessConfirm_UserIsNotAuthorized_ReturnDenied()
+ [Theory]
+ [MemberData(nameof(UpdateTestParameters))]
+ public async Task PutProcessConfirm_UserIsNotAuthorized_ReturnDenied(bool useInstanceAndEventsEndpoint)
{
- // Arrange
- string requestUri = $"storage/api/v1/instances/1337/20b1353e-91cf-44d6-8ff7-f68993638ffe/process/";
- ProcessState state = new ProcessState();
- JsonContent jsonString = JsonContent.Create(state, new MediaTypeHeaderValue("application/json"));
-
- HttpClient client = GetTestClient();
+ // Arrange
string token = PrincipalUtil.GetToken(3, 1337, 3);
- client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
// Act
- HttpResponseMessage response = await client.PutAsync(requestUri, jsonString);
+ using HttpResponseMessage response = await SendUpdateRequest(useInstanceAndEventsEndpoint, token: token, instanceId: "1337/20b1353e-91cf-44d6-8ff7-f68993638ffe");
// Assert
Assert.Equal(HttpStatusCode.Forbidden, response.StatusCode);
@@ -239,37 +254,40 @@ public async Task PutProcessConfirm_UserIsNotAuthorized_ReturnDenied()
/// Test case: User is Authorized
/// Expected: Returns status ok.
///
- [Fact]
- public async Task PutProcess_EndProcess_EnsureArchivedStateIsSet()
+ [Theory]
+ [MemberData(nameof(UpdateTestParameters))]
+ public async Task PutProcess_EndProcess_EnsureArchivedStateIsSet(bool useInstanceAndEventsEndpoint)
{
// Arrange
- string requestUri = $"storage/api/v1/instances/1337/377efa97-80ee-4cc6-8d48-09de12cc273d/process/";
+ string token = PrincipalUtil.GetToken(3, 1337, 3);
Instance testInstance = TestDataUtil.GetInstance(new Guid("377efa97-80ee-4cc6-8d48-09de12cc273d"));
testInstance.Id = $"{testInstance.InstanceOwner.PartyId}/{testInstance.Id}";
- ProcessState state = new ProcessState
- {
- Started = DateTime.Parse("2020-04-29T13:53:01.7020218Z"),
- StartEvent = "StartEvent_1",
- Ended = DateTime.UtcNow,
- EndEvent = "EndEvent_1"
- };
-
- JsonContent jsonString = JsonContent.Create(state, new MediaTypeHeaderValue("application/json"));
-
+
Mock repositoryMock = new Mock();
+ Mock batchRepositoryMock = new Mock();
repositoryMock.Setup(ir => ir.GetOne(It.IsAny(), true)).ReturnsAsync((testInstance, 0));
- repositoryMock.Setup(ir => ir.Update(It.IsAny(), It.IsAny>())).ReturnsAsync((Instance i, List props) => i);
-
- HttpClient client = GetTestClient(repositoryMock.Object);
- string token = PrincipalUtil.GetToken(3, 1337, 3);
- client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
+ repositoryMock.Setup(ir => ir.Update(It.IsAny(), It.IsAny>())).ReturnsAsync((Instance i, List _) => i);
+ batchRepositoryMock.Setup(ir => ir.Update(It.IsAny(), It.IsAny>(), It.IsAny>())).ReturnsAsync((Instance i, List _, List _) => i);
// Act
- HttpResponseMessage response = await client.PutAsync(requestUri, jsonString);
- string responseContent = await response.Content.ReadAsStringAsync();
- Instance actual = (Instance)JsonConvert.DeserializeObject(responseContent, typeof(Instance));
+ using HttpResponseMessage response = await SendUpdateRequest(
+ useInstanceAndEventsEndpoint,
+ token: token,
+ instanceId: "1337/377efa97-80ee-4cc6-8d48-09de12cc273d",
+ instanceRepository: repositoryMock.Object,
+ instanceAndEventsRepository: batchRepositoryMock.Object,
+ configure: state =>
+ {
+ state.Started = DateTime.Parse("2020-04-29T13:53:01.7020218Z");
+ state.StartEvent = "StartEvent_1";
+ state.Ended = DateTime.UtcNow;
+ state.EndEvent = "EndEvent_1";
+ });
// Assert
+ string responseContent = await response.Content.ReadAsStringAsync();
+ Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+ Instance actual = (Instance)JsonConvert.DeserializeObject(responseContent, typeof(Instance));
Assert.True(actual.Status.IsArchived);
}
@@ -277,50 +295,38 @@ public async Task PutProcess_EndProcess_EnsureArchivedStateIsSet()
/// Test case: User pushes process to signing step.
/// Expected: An instance event of type "sentToSign" is registered.
///
- [Fact]
- public async Task PutProcess_MoveToSigning_SentToSignEventGenerated()
+ [Theory]
+ [MemberData(nameof(UpdateTestParameters))]
+ public async Task PutProcess_MoveToSigning_SentToSignEventGenerated(bool useInstanceAndEventsEndpoint)
{
- // Arrange
- string requestUri = $"storage/api/v1/instances/1337/20a1353e-91cf-44d6-8ff7-f68993638ffe/process/";
- ProcessState state = new()
- {
- CurrentTask = new()
- {
- ElementId = "Task_2",
- AltinnTaskType = "signing",
- FlowType = "CompleteCurrentMoveToNext"
- }
- };
-
- JsonContent jsonString = JsonContent.Create(state, new MediaTypeHeaderValue("application/json"));
-
- var serviceMock = new Mock();
- serviceMock.Setup(m => m.DispatchEvent(It.Is(t => t == InstanceEventType.SentToSign), It.IsAny()));
-
- HttpClient client = GetTestClient(instanceEventService: serviceMock.Object);
+ // Arrange
string token = PrincipalUtil.GetToken(3, 1337, 3);
- client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
// Act
- HttpResponseMessage response = await client.PutAsync(requestUri, jsonString);
+ using HttpResponseMessage response = await SendUpdateRequest(
+ useInstanceAndEventsEndpoint,
+ token: token,
+ instanceId: "1337/20a1353e-91cf-44d6-8ff7-f68993638ffe",
+ configure: state =>
+ {
+ state.CurrentTask = new ProcessElementInfo
+ {
+ ElementId = "Task_2",
+ AltinnTaskType = "signing",
+ FlowType = "CompleteCurrentMoveToNext"
+ };
+ });
// Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
- serviceMock.VerifyAll();
}
- private HttpClient GetTestClient(IInstanceRepository instanceRepository = null, IInstanceEventService instanceEventService = null)
+ private HttpClient GetTestClient(IInstanceRepository instanceRepository = null, IInstanceAndEventsRepository instanceAndEventsRepository = null)
{
// No setup required for these services. They are not in use by the ApplicationController
Mock keyVaultWrapper = new Mock();
Mock partiesWrapper = new Mock();
- if (instanceEventService == null)
- {
- var mock = new Mock();
- instanceEventService = mock.Object;
- }
-
HttpClient client = _factory.WithWebHostBuilder(builder =>
{
IConfiguration configuration = new ConfigurationBuilder().AddJsonFile(ServiceUtil.GetAppsettingsPath()).Build();
@@ -338,7 +344,7 @@ private HttpClient GetTestClient(IInstanceRepository instanceRepository = null,
services.AddSingleton();
services.AddSingleton();
services.AddSingleton, JwtCookiePostConfigureOptionsStub>();
- services.AddSingleton(instanceEventService);
+ services.AddSingleton();
if (instanceRepository != null)
{
@@ -348,6 +354,15 @@ private HttpClient GetTestClient(IInstanceRepository instanceRepository = null,
{
services.AddSingleton();
}
+
+ if (instanceAndEventsRepository != null)
+ {
+ services.AddSingleton(instanceAndEventsRepository);
+ }
+ else
+ {
+ services.AddSingleton();
+ }
});
}).CreateClient();
diff --git a/test/UnitTest/TestingRepositories/InstanceTests.cs b/test/UnitTest/TestingRepositories/InstanceTests.cs
index bad043e3..0f056af4 100644
--- a/test/UnitTest/TestingRepositories/InstanceTests.cs
+++ b/test/UnitTest/TestingRepositories/InstanceTests.cs
@@ -82,6 +82,54 @@ public async Task Instance_Update_Task_Ok()
Assert.Equal(newInstance.LastChangedBy, updatedInstance.LastChangedBy);
}
+ ///
+ /// Test update task with events
+ ///
+ [Theory]
+ [InlineData(0)]
+ [InlineData(1)]
+ [InlineData(3)]
+ public async Task Instance_Update_Task_With_Events_Ok(int eventCount)
+ {
+ // Arrange
+ Instance newInstance = TestData.Instance_1_1.Clone();
+ newInstance.Process.CurrentTask.Name = "Before update";
+ newInstance.Process.StartEvent = "s1";
+ newInstance = await _instanceFixture.InstanceRepo.Create(newInstance);
+ newInstance.Process.CurrentTask.ElementId = "Task_2";
+ newInstance.Process.CurrentTask.Name = "After update";
+ newInstance.Process.StartEvent = null;
+ newInstance.Process.EndEvent = "e1";
+ newInstance.LastChanged = DateTime.UtcNow;
+ newInstance.LastChangedBy = "unittest";
+
+ List updateProperties = [];
+ updateProperties.Add(nameof(newInstance.LastChanged));
+ updateProperties.Add(nameof(newInstance.LastChangedBy));
+ updateProperties.Add(nameof(newInstance.Process));
+
+ List instanceEvents = [];
+ for (int i = 0; i < eventCount; i++)
+ {
+ InstanceEvent instanceEvent = new() { Id = Guid.NewGuid(), InstanceId = newInstance.Id, EventType = $"et{i}", Created = DateTime.Parse("1994-06-16T11:06:59.0851832Z") };
+ instanceEvents.Add(instanceEvent);
+ }
+
+ // Act
+ Instance updatedInstance = await _instanceFixture.InstanceAndEventsRepo.Update(newInstance, updateProperties, instanceEvents);
+
+ // Assert
+ if (instanceEvents.Count > 0)
+ {
+ string ids = string.Join(", ", instanceEvents.Select(e => $"'{e.Id}'"));
+ string sql = $"select count(*) from storage.instanceevents where alternateid in ({ids}) AND instance = '{TestData.Instance_1_1.Id.Split('/').Last()}'";
+ int count = await PostgresUtil.RunCountQuery(sql);
+ Assert.Equal(instanceEvents.Count, count);
+ }
+
+ Assert.Equal("Task_2", updatedInstance.Process.CurrentTask.ElementId);
+ }
+
///
/// Test update status
///
@@ -733,12 +781,15 @@ public class InstanceFixture
{
public IInstanceRepository InstanceRepo { get; set; }
+ public IInstanceAndEventsRepository InstanceAndEventsRepo { get; set; }
+
public IDataRepository DataRepo { get; set; }
public InstanceFixture()
{
- var serviceList = ServiceUtil.GetServices(new List() { typeof(IInstanceRepository), typeof(IDataRepository) });
+ var serviceList = ServiceUtil.GetServices(new List() { typeof(IInstanceRepository), typeof(IInstanceAndEventsRepository), typeof(IDataRepository) });
InstanceRepo = (IInstanceRepository)serviceList.First(i => i.GetType() == typeof(PgInstanceRepository));
+ InstanceAndEventsRepo = (IInstanceAndEventsRepository)serviceList.First(i => i.GetType() == typeof(PgInstanceAndEventsRepository));
DataRepo = (IDataRepository)serviceList.First(i => i.GetType() == typeof(PgDataRepository));
}
}