mirror of
https://github.com/andwati/zitadel.git
synced 2026-10-18 05:52:51 +00:00
fix(eventstore): allow overwriting resource owner of events (#12261)
# Which Problems Are Solved - The eventstore did not support intentionally overwriting the resource owner when creating events for aggregates that may be reused across owners. - Resource owner handling was implicit and could not be controlled per command/event type. - We needed a safe way to distinguish between: - keeping the existing aggregate owner, and - explicitly setting a new owner for specific create-like events. # How the Problems Are Solved - Introduced a new eventstore command type with an explicit enforce_owner flag. - Updated eventstore.commands_to_events and eventstore.push so owner assignment is now explicit: - if enforce_owner is true, the command owner is written - if enforce_owner is false, the existing aggregate owner is retained when present - Added EnforceResourceOwnerCommand and wiring so command types can opt in to enforced owner behavior. - Wired the new behavior through the v3 eventstore push path, including compatibility fallback for older command type mapping. - Added migration/setup changes to register and use the new command type and SQL functions. - Added and updated tests for owner overwrite and aggregate ID reuse scenarios. # Additional Changes - Added small migration/setup robustness improvements related to eventstore setup ordering and helper reuse. - Added focused test coverage for enforced owner behavior and sequencing. - Events that currently allow owner changes (implement EnforceResourceOwner) are: - AddedEvent (action) - GroupAddedEvent - StartedEvent (idp intent) - ProjectAddedEvent - HumanAddedEvent - HumanRegisteredEvent - MachineAddedEvent - CreatedEvent (schema user) # Additional Context - Follow-up for eventstore owner-handling correctness in create flows and aggregate ID reuse cases. - No additional issue link was attached for this change. --------- Co-authored-by: abhishek kumar gupta <[email protected]>
This commit is contained in:
co-authored by
abhishek kumar gupta
parent
4459a1380c
commit
6082e59d47
@@ -23,7 +23,7 @@ CREATE INDEX IF NOT EXISTS es_projection ON eventstore.events2 (instance_id, agg
|
||||
|
||||
-- represents an event to be created.
|
||||
DO $$ BEGIN
|
||||
CREATE TYPE eventstore.command AS (
|
||||
CREATE TYPE eventstore.command2 AS (
|
||||
instance_id TEXT
|
||||
, aggregate_type TEXT
|
||||
, aggregate_id TEXT
|
||||
@@ -32,90 +32,131 @@ DO $$ BEGIN
|
||||
, payload JSONB
|
||||
, creator TEXT
|
||||
, owner TEXT
|
||||
, enforce_owner BOOLEAN
|
||||
);
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
|
||||
CREATE OR REPLACE FUNCTION eventstore.commands_to_events(commands eventstore.command[]) RETURNS SETOF eventstore.events2 VOLATILE AS $$
|
||||
SELECT
|
||||
c.instance_id
|
||||
, c.aggregate_type
|
||||
, c.aggregate_id
|
||||
, c.command_type AS event_type
|
||||
, cs.sequence + ROW_NUMBER() OVER (PARTITION BY c.instance_id, c.aggregate_type, c.aggregate_id ORDER BY c.in_tx_order) AS sequence
|
||||
, c.revision
|
||||
, NOW() AS created_at
|
||||
, c.payload
|
||||
, c.creator
|
||||
, cs.owner
|
||||
, EXTRACT(EPOCH FROM NOW()) AS position
|
||||
, c.in_tx_order
|
||||
FROM (
|
||||
SELECT
|
||||
DO $$ BEGIN
|
||||
CREATE TYPE eventstore.latest_command AS (
|
||||
instance_id TEXT
|
||||
, aggregate_type TEXT
|
||||
, aggregate_id TEXT
|
||||
, owner TEXT
|
||||
, sequence BIGINT
|
||||
);
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
|
||||
CREATE OR REPLACE FUNCTION eventstore.commands_to_events(commands eventstore.command2[])
|
||||
RETURNS SETOF eventstore.events2
|
||||
LANGUAGE 'plpgsql'
|
||||
STABLE PARALLEL SAFE
|
||||
ROWS 10
|
||||
AS $$
|
||||
DECLARE
|
||||
_latest_events eventstore.latest_command[];
|
||||
command eventstore.command2;
|
||||
last_owner TEXT;
|
||||
i INTEGER;
|
||||
BEGIN
|
||||
|
||||
SELECT array_agg(res) INTO _latest_events
|
||||
FROM (
|
||||
SELECT
|
||||
ROW(
|
||||
c.instance_id,
|
||||
c.aggregate_type,
|
||||
c.aggregate_id,
|
||||
COALESCE(e.owner, c.owner),
|
||||
COALESCE(e.sequence, 0)
|
||||
)::eventstore.latest_command AS res
|
||||
FROM (
|
||||
SELECT DISTINCT ON (c.instance_id, c.aggregate_type, c.aggregate_id)
|
||||
c.instance_id,
|
||||
c.aggregate_type,
|
||||
c.aggregate_id,
|
||||
c.owner,
|
||||
c.ordinality
|
||||
FROM (
|
||||
SELECT *
|
||||
FROM UNNEST(commands) WITH ORDINALITY AS c
|
||||
) AS c
|
||||
ORDER BY
|
||||
c.instance_id,
|
||||
c.aggregate_type,
|
||||
c.aggregate_id,
|
||||
c.ordinality
|
||||
) AS c
|
||||
LEFT JOIN LATERAL (
|
||||
SELECT
|
||||
e.owner,
|
||||
e.sequence
|
||||
FROM eventstore.events2 AS e
|
||||
WHERE e.instance_id = c.instance_id
|
||||
AND e.aggregate_type = c.aggregate_type
|
||||
AND e.aggregate_id = c.aggregate_id
|
||||
ORDER BY e.sequence DESC
|
||||
LIMIT 1
|
||||
) AS e ON TRUE
|
||||
) AS latest;
|
||||
|
||||
FOR command IN SELECT * FROM UNNEST(commands) LOOP
|
||||
IF NOT command.enforce_owner THEN
|
||||
SELECT owner INTO command.owner
|
||||
FROM UNNEST(_latest_events) AS e
|
||||
WHERE e.instance_id = command.instance_id
|
||||
AND e.aggregate_type = command.aggregate_type
|
||||
AND e.aggregate_id = command.aggregate_id;
|
||||
ELSE
|
||||
FOR i IN 1..array_length(_latest_events, 1) LOOP
|
||||
IF _latest_events[i].instance_id = command.instance_id
|
||||
AND _latest_events[i].aggregate_type = command.aggregate_type
|
||||
AND _latest_events[i].aggregate_id = command.aggregate_id
|
||||
THEN
|
||||
_latest_events[i].owner := command.owner;
|
||||
EXIT;
|
||||
END IF;
|
||||
END LOOP;
|
||||
END IF;
|
||||
|
||||
END LOOP;
|
||||
|
||||
|
||||
RETURN QUERY SELECT
|
||||
c.instance_id
|
||||
, c.aggregate_type
|
||||
, c.aggregate_id
|
||||
, c.command_type
|
||||
, c.command_type AS event_type
|
||||
, COALESCE(e.sequence, 0) + ROW_NUMBER() OVER (PARTITION BY c.instance_id, c.aggregate_type, c.aggregate_id ORDER BY c.in_tx_order) AS sequence
|
||||
, c.revision
|
||||
, NOW() AS created_at
|
||||
, c.payload
|
||||
, c.creator
|
||||
, c.owner
|
||||
, ROW_NUMBER() OVER () AS in_tx_order
|
||||
FROM
|
||||
UNNEST(commands) AS c
|
||||
) AS c
|
||||
JOIN (
|
||||
SELECT
|
||||
cmds.instance_id
|
||||
, cmds.aggregate_type
|
||||
, cmds.aggregate_id
|
||||
, CASE WHEN (e.owner IS NOT NULL OR e.owner <> '') THEN e.owner ELSE command_owners.owner END AS owner
|
||||
, COALESCE(MAX(e.sequence), 0) AS sequence
|
||||
, CASE WHEN c.enforce_owner
|
||||
THEN c.owner
|
||||
ELSE COALESCE(e.owner, c.owner)
|
||||
END AS owner
|
||||
, EXTRACT(EPOCH FROM NOW()) AS position
|
||||
, c.in_tx_order
|
||||
FROM (
|
||||
SELECT DISTINCT
|
||||
instance_id
|
||||
, aggregate_type
|
||||
, aggregate_id
|
||||
, owner
|
||||
FROM UNNEST(commands)
|
||||
) AS cmds
|
||||
LEFT JOIN eventstore.events2 AS e
|
||||
ON cmds.instance_id = e.instance_id
|
||||
AND cmds.aggregate_type = e.aggregate_type
|
||||
AND cmds.aggregate_id = e.aggregate_id
|
||||
JOIN (
|
||||
SELECT
|
||||
DISTINCT ON (
|
||||
instance_id
|
||||
, aggregate_type
|
||||
, aggregate_id
|
||||
)
|
||||
instance_id
|
||||
, aggregate_type
|
||||
, aggregate_id
|
||||
, owner
|
||||
FROM
|
||||
UNNEST(commands)
|
||||
) AS command_owners ON
|
||||
cmds.instance_id = command_owners.instance_id
|
||||
AND cmds.aggregate_type = command_owners.aggregate_type
|
||||
AND cmds.aggregate_id = command_owners.aggregate_id
|
||||
GROUP BY
|
||||
cmds.instance_id
|
||||
, cmds.aggregate_type
|
||||
, cmds.aggregate_id
|
||||
, 4 -- owner
|
||||
) AS cs
|
||||
ON c.instance_id = cs.instance_id
|
||||
AND c.aggregate_type = cs.aggregate_type
|
||||
AND c.aggregate_id = cs.aggregate_id
|
||||
ORDER BY
|
||||
in_tx_order;
|
||||
$$ LANGUAGE SQL;
|
||||
SELECT c.*, CAST(ROW_NUMBER() OVER () AS INTEGER) AS in_tx_order
|
||||
FROM UNNEST(commands) AS c
|
||||
) AS c
|
||||
LEFT JOIN UNNEST(_latest_events) AS e
|
||||
ON c.instance_id = e.instance_id
|
||||
AND c.aggregate_type = e.aggregate_type
|
||||
AND c.aggregate_id = e.aggregate_id
|
||||
ORDER BY
|
||||
in_tx_order;
|
||||
END;
|
||||
$$;
|
||||
|
||||
CREATE OR REPLACE FUNCTION eventstore.push(commands eventstore.command[]) RETURNS SETOF eventstore.events2 VOLATILE AS $$
|
||||
CREATE OR REPLACE FUNCTION eventstore.push(commands eventstore.command2[]) RETURNS SETOF eventstore.events2 VOLATILE AS $$
|
||||
INSERT INTO eventstore.events2
|
||||
SELECT * FROM eventstore.commands_to_events(commands)
|
||||
ORDER BY in_tx_order
|
||||
RETURNING *
|
||||
$$ LANGUAGE SQL;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
-- represents an event to be created.
|
||||
DO $$ BEGIN
|
||||
CREATE TYPE eventstore.command AS (
|
||||
CREATE TYPE eventstore.command2 AS (
|
||||
instance_id TEXT
|
||||
, aggregate_type TEXT
|
||||
, aggregate_id TEXT
|
||||
@@ -9,6 +9,7 @@ DO $$ BEGIN
|
||||
, payload JSONB
|
||||
, creator TEXT
|
||||
, owner TEXT
|
||||
, enforce_owner BOOLEAN
|
||||
);
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
|
||||
+103
-75
@@ -1,92 +1,120 @@
|
||||
CREATE OR REPLACE FUNCTION eventstore.latest_aggregate_state(
|
||||
instance_id TEXT
|
||||
, aggregate_type TEXT
|
||||
, aggregate_id TEXT
|
||||
|
||||
, sequence OUT BIGINT
|
||||
, owner OUT TEXT
|
||||
)
|
||||
LANGUAGE 'plpgsql'
|
||||
STABLE PARALLEL SAFE
|
||||
AS $$
|
||||
BEGIN
|
||||
SELECT
|
||||
COALESCE(e.sequence, 0) AS sequence
|
||||
, e.owner
|
||||
INTO
|
||||
sequence
|
||||
, owner
|
||||
FROM
|
||||
eventstore.events2 e
|
||||
WHERE
|
||||
e.instance_id = $1
|
||||
AND e.aggregate_type = $2
|
||||
AND e.aggregate_id = $3
|
||||
ORDER BY
|
||||
e.sequence DESC
|
||||
LIMIT 1;
|
||||
DO $$ BEGIN
|
||||
CREATE TYPE eventstore.latest_command AS (
|
||||
instance_id TEXT
|
||||
, aggregate_type TEXT
|
||||
, aggregate_id TEXT
|
||||
, owner TEXT
|
||||
, sequence BIGINT
|
||||
);
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
|
||||
RETURN;
|
||||
END;
|
||||
$$;
|
||||
|
||||
CREATE OR REPLACE FUNCTION eventstore.commands_to_events(commands eventstore.command[])
|
||||
CREATE OR REPLACE FUNCTION eventstore.commands_to_events(commands eventstore.command2[])
|
||||
RETURNS SETOF eventstore.events2
|
||||
LANGUAGE 'plpgsql'
|
||||
STABLE PARALLEL SAFE
|
||||
ROWS 10
|
||||
AS $$
|
||||
DECLARE
|
||||
"aggregate" RECORD;
|
||||
current_sequence BIGINT;
|
||||
current_owner TEXT;
|
||||
created_at TIMESTAMPTZ;
|
||||
_latest_events eventstore.latest_command[];
|
||||
command eventstore.command2;
|
||||
last_owner TEXT;
|
||||
i INTEGER;
|
||||
BEGIN
|
||||
created_at := statement_timestamp();
|
||||
FOR "aggregate" IN
|
||||
SELECT DISTINCT
|
||||
instance_id
|
||||
, aggregate_type
|
||||
, aggregate_id
|
||||
FROM UNNEST(commands)
|
||||
LOOP
|
||||
SELECT
|
||||
*
|
||||
INTO
|
||||
current_sequence
|
||||
, current_owner
|
||||
FROM eventstore.latest_aggregate_state(
|
||||
"aggregate".instance_id
|
||||
, "aggregate".aggregate_type
|
||||
, "aggregate".aggregate_id
|
||||
);
|
||||
|
||||
RETURN QUERY
|
||||
SELECT array_agg(res) INTO _latest_events
|
||||
FROM (
|
||||
SELECT
|
||||
c.instance_id
|
||||
, c.aggregate_type
|
||||
, c.aggregate_id
|
||||
, c.command_type -- AS event_type
|
||||
, COALESCE(current_sequence, 0) + ROW_NUMBER() OVER () -- AS sequence
|
||||
, c.revision
|
||||
, created_at
|
||||
, c.payload
|
||||
, c.creator
|
||||
, COALESCE(current_owner, c.owner) -- AS owner
|
||||
, EXTRACT(EPOCH FROM created_at) -- AS position
|
||||
, c.ordinality::{{ .InTxOrderType }} -- AS in_tx_order
|
||||
FROM
|
||||
UNNEST(commands) WITH ORDINALITY AS c
|
||||
WHERE
|
||||
c.instance_id = aggregate.instance_id
|
||||
AND c.aggregate_type = aggregate.aggregate_type
|
||||
AND c.aggregate_id = aggregate.aggregate_id;
|
||||
ROW(
|
||||
c.instance_id,
|
||||
c.aggregate_type,
|
||||
c.aggregate_id,
|
||||
COALESCE(e.owner, c.owner),
|
||||
COALESCE(e.sequence, 0)
|
||||
)::eventstore.latest_command AS res
|
||||
FROM (
|
||||
SELECT DISTINCT ON (c.instance_id, c.aggregate_type, c.aggregate_id)
|
||||
c.instance_id,
|
||||
c.aggregate_type,
|
||||
c.aggregate_id,
|
||||
c.owner,
|
||||
c.ordinality
|
||||
FROM (
|
||||
SELECT *
|
||||
FROM UNNEST(commands) WITH ORDINALITY AS c
|
||||
) AS c
|
||||
ORDER BY
|
||||
c.instance_id,
|
||||
c.aggregate_type,
|
||||
c.aggregate_id,
|
||||
c.ordinality
|
||||
) AS c
|
||||
LEFT JOIN LATERAL (
|
||||
SELECT
|
||||
e.owner,
|
||||
e.sequence
|
||||
FROM eventstore.events2 AS e
|
||||
WHERE e.instance_id = c.instance_id
|
||||
AND e.aggregate_type = c.aggregate_type
|
||||
AND e.aggregate_id = c.aggregate_id
|
||||
ORDER BY e.sequence DESC
|
||||
LIMIT 1
|
||||
) AS e ON TRUE
|
||||
) AS latest;
|
||||
|
||||
FOR command IN SELECT * FROM UNNEST(commands) LOOP
|
||||
IF NOT command.enforce_owner THEN
|
||||
SELECT owner INTO command.owner
|
||||
FROM UNNEST(_latest_events) AS e
|
||||
WHERE e.instance_id = command.instance_id
|
||||
AND e.aggregate_type = command.aggregate_type
|
||||
AND e.aggregate_id = command.aggregate_id;
|
||||
ELSE
|
||||
FOR i IN 1..array_length(_latest_events, 1) LOOP
|
||||
IF _latest_events[i].instance_id = command.instance_id
|
||||
AND _latest_events[i].aggregate_type = command.aggregate_type
|
||||
AND _latest_events[i].aggregate_id = command.aggregate_id
|
||||
THEN
|
||||
_latest_events[i].owner := command.owner;
|
||||
EXIT;
|
||||
END IF;
|
||||
END LOOP;
|
||||
END IF;
|
||||
|
||||
END LOOP;
|
||||
RETURN;
|
||||
|
||||
|
||||
RETURN QUERY SELECT
|
||||
c.instance_id
|
||||
, c.aggregate_type
|
||||
, c.aggregate_id
|
||||
, c.command_type AS event_type
|
||||
, COALESCE(e.sequence, 0) + ROW_NUMBER() OVER (PARTITION BY c.instance_id, c.aggregate_type, c.aggregate_id ORDER BY c.in_tx_order) AS sequence
|
||||
, c.revision
|
||||
, NOW() AS created_at
|
||||
, c.payload
|
||||
, c.creator
|
||||
, CASE WHEN c.enforce_owner
|
||||
THEN c.owner
|
||||
ELSE COALESCE(e.owner, c.owner)
|
||||
END AS owner
|
||||
, EXTRACT(EPOCH FROM NOW()) AS position
|
||||
, c.in_tx_order
|
||||
FROM (
|
||||
SELECT c.*, CAST(ROW_NUMBER() OVER () AS INTEGER) AS in_tx_order
|
||||
FROM UNNEST(commands) AS c
|
||||
) AS c
|
||||
LEFT JOIN UNNEST(_latest_events) AS e
|
||||
ON c.instance_id = e.instance_id
|
||||
AND c.aggregate_type = e.aggregate_type
|
||||
AND c.aggregate_id = e.aggregate_id
|
||||
ORDER BY
|
||||
in_tx_order;
|
||||
END;
|
||||
$$;
|
||||
|
||||
CREATE OR REPLACE FUNCTION eventstore.push(commands eventstore.command[]) RETURNS SETOF eventstore.events2 VOLATILE AS $$
|
||||
CREATE OR REPLACE FUNCTION eventstore.push(commands eventstore.command2[]) RETURNS SETOF eventstore.events2 VOLATILE AS $$
|
||||
INSERT INTO eventstore.events2
|
||||
SELECT * FROM eventstore.commands_to_events(commands)
|
||||
ORDER BY in_tx_order
|
||||
|
||||
+3
-3
@@ -20,7 +20,7 @@ type ChangePushPosition struct {
|
||||
}
|
||||
|
||||
func (mig *ChangePushPosition) Execute(ctx context.Context, _ eventstore.Event) error {
|
||||
inTxOrderType, err := mig.inTxOrderType(ctx)
|
||||
inTxOrderType, err := inTxOrderType(ctx, mig.dbClient)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -33,8 +33,8 @@ func (mig *ChangePushPosition) String() string {
|
||||
return "64_change_push_position"
|
||||
}
|
||||
|
||||
func (mig *ChangePushPosition) inTxOrderType(ctx context.Context) (typeName string, err error) {
|
||||
err = mig.dbClient.QueryRowContext(ctx, func(row *sql.Row) error {
|
||||
func inTxOrderType(ctx context.Context, client *database.DB) (typeName string, err error) {
|
||||
err = client.QueryRowContext(ctx, func(row *sql.Row) error {
|
||||
return row.Scan(&typeName)
|
||||
}, `SELECT data_type FROM information_schema.columns WHERE table_schema = 'eventstore' AND table_name = 'events2' AND column_name = 'in_tx_order'`)
|
||||
if err != nil {
|
||||
|
||||
+107
-43
@@ -1,55 +1,119 @@
|
||||
CREATE OR REPLACE FUNCTION eventstore.commands_to_events(commands eventstore.command[])
|
||||
DO $$ BEGIN
|
||||
CREATE TYPE eventstore.latest_command AS (
|
||||
instance_id TEXT
|
||||
, aggregate_type TEXT
|
||||
, aggregate_id TEXT
|
||||
, owner TEXT
|
||||
, sequence BIGINT
|
||||
);
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
|
||||
CREATE OR REPLACE FUNCTION eventstore.commands_to_events(commands eventstore.command2[])
|
||||
RETURNS SETOF eventstore.events2
|
||||
LANGUAGE 'plpgsql'
|
||||
STABLE PARALLEL SAFE
|
||||
ROWS 10
|
||||
AS $$
|
||||
DECLARE
|
||||
"aggregate" RECORD;
|
||||
current_sequence BIGINT;
|
||||
current_owner TEXT;
|
||||
created_at TIMESTAMPTZ;
|
||||
_latest_events eventstore.latest_command[];
|
||||
command eventstore.command2;
|
||||
last_owner TEXT;
|
||||
i INTEGER;
|
||||
BEGIN
|
||||
created_at := statement_timestamp();
|
||||
FOR "aggregate" IN
|
||||
SELECT DISTINCT
|
||||
instance_id
|
||||
, aggregate_type
|
||||
, aggregate_id
|
||||
FROM UNNEST(commands)
|
||||
LOOP
|
||||
SELECT
|
||||
*
|
||||
INTO
|
||||
current_sequence
|
||||
, current_owner
|
||||
FROM eventstore.latest_aggregate_state(
|
||||
"aggregate".instance_id
|
||||
, "aggregate".aggregate_type
|
||||
, "aggregate".aggregate_id
|
||||
);
|
||||
|
||||
RETURN QUERY
|
||||
SELECT array_agg(res) INTO _latest_events
|
||||
FROM (
|
||||
SELECT
|
||||
c.instance_id
|
||||
, c.aggregate_type
|
||||
, c.aggregate_id
|
||||
, c.command_type -- AS event_type
|
||||
, COALESCE(current_sequence, 0) + ROW_NUMBER() OVER () -- AS sequence
|
||||
, c.revision
|
||||
, created_at
|
||||
, c.payload
|
||||
, c.creator
|
||||
, COALESCE(current_owner, c.owner) -- AS owner
|
||||
, EXTRACT(EPOCH FROM created_at) -- AS position
|
||||
, c.ordinality::%s -- AS in_tx_order
|
||||
FROM
|
||||
UNNEST(commands) WITH ORDINALITY AS c
|
||||
WHERE
|
||||
c.instance_id = aggregate.instance_id
|
||||
AND c.aggregate_type = aggregate.aggregate_type
|
||||
AND c.aggregate_id = aggregate.aggregate_id;
|
||||
ROW(
|
||||
c.instance_id,
|
||||
c.aggregate_type,
|
||||
c.aggregate_id,
|
||||
COALESCE(e.owner, c.owner),
|
||||
COALESCE(e.sequence, 0)
|
||||
)::eventstore.latest_command AS res
|
||||
FROM (
|
||||
SELECT DISTINCT ON (c.instance_id, c.aggregate_type, c.aggregate_id)
|
||||
c.instance_id,
|
||||
c.aggregate_type,
|
||||
c.aggregate_id,
|
||||
c.owner,
|
||||
c.ordinality
|
||||
FROM (
|
||||
SELECT *
|
||||
FROM UNNEST(commands) WITH ORDINALITY AS c
|
||||
) AS c
|
||||
ORDER BY
|
||||
c.instance_id,
|
||||
c.aggregate_type,
|
||||
c.aggregate_id,
|
||||
c.ordinality
|
||||
) AS c
|
||||
LEFT JOIN LATERAL (
|
||||
SELECT
|
||||
e.owner,
|
||||
e.sequence
|
||||
FROM eventstore.events2 AS e
|
||||
WHERE e.instance_id = c.instance_id
|
||||
AND e.aggregate_type = c.aggregate_type
|
||||
AND e.aggregate_id = c.aggregate_id
|
||||
ORDER BY e.sequence DESC
|
||||
LIMIT 1
|
||||
) AS e ON TRUE
|
||||
) AS latest;
|
||||
|
||||
FOR command IN SELECT * FROM UNNEST(commands) LOOP
|
||||
IF NOT command.enforce_owner THEN
|
||||
SELECT owner INTO command.owner
|
||||
FROM UNNEST(_latest_events) AS e
|
||||
WHERE e.instance_id = command.instance_id
|
||||
AND e.aggregate_type = command.aggregate_type
|
||||
AND e.aggregate_id = command.aggregate_id;
|
||||
ELSE
|
||||
FOR i IN 1..array_length(_latest_events, 1) LOOP
|
||||
IF _latest_events[i].instance_id = command.instance_id
|
||||
AND _latest_events[i].aggregate_type = command.aggregate_type
|
||||
AND _latest_events[i].aggregate_id = command.aggregate_id
|
||||
THEN
|
||||
_latest_events[i].owner := command.owner;
|
||||
EXIT;
|
||||
END IF;
|
||||
END LOOP;
|
||||
END IF;
|
||||
|
||||
END LOOP;
|
||||
RETURN;
|
||||
|
||||
|
||||
RETURN QUERY SELECT
|
||||
c.instance_id
|
||||
, c.aggregate_type
|
||||
, c.aggregate_id
|
||||
, c.command_type AS event_type
|
||||
, COALESCE(e.sequence, 0) + ROW_NUMBER() OVER (PARTITION BY c.instance_id, c.aggregate_type, c.aggregate_id ORDER BY c.ordinality) AS sequence
|
||||
, c.revision
|
||||
, NOW() AS created_at
|
||||
, c.payload
|
||||
, c.creator
|
||||
, CASE WHEN c.enforce_owner
|
||||
THEN c.owner
|
||||
ELSE COALESCE(e.owner, c.owner)
|
||||
END AS owner
|
||||
, EXTRACT(EPOCH FROM NOW()) AS position
|
||||
, c.ordinality::%s AS in_tx_order
|
||||
FROM UNNEST(commands) WITH ORDINALITY AS c
|
||||
LEFT JOIN unnest(_latest_events) AS e
|
||||
ON c.instance_id = e.instance_id
|
||||
AND c.aggregate_type = e.aggregate_type
|
||||
AND c.aggregate_id = e.aggregate_id
|
||||
ORDER BY
|
||||
in_tx_order;
|
||||
END;
|
||||
$$;
|
||||
|
||||
CREATE OR REPLACE FUNCTION eventstore.push(commands eventstore.command2[]) RETURNS SETOF eventstore.events2 VOLATILE AS $$
|
||||
INSERT INTO eventstore.events2
|
||||
SELECT * FROM eventstore.commands_to_events(commands)
|
||||
ORDER BY in_tx_order
|
||||
RETURNING *
|
||||
$$ LANGUAGE SQL;
|
||||
@@ -0,0 +1,34 @@
|
||||
package setup
|
||||
|
||||
import (
|
||||
"context"
|
||||
_ "embed"
|
||||
"fmt"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/database"
|
||||
"github.com/zitadel/zitadel/internal/eventstore"
|
||||
)
|
||||
|
||||
var (
|
||||
//go:embed 70.sql
|
||||
addEventStoreCommandEnforceOwnerColumn string
|
||||
)
|
||||
|
||||
type AddEventStoreCommandEnforceOwnerColumn struct {
|
||||
dbClient *database.DB
|
||||
}
|
||||
|
||||
func (mig *AddEventStoreCommandEnforceOwnerColumn) Execute(ctx context.Context, _ eventstore.Event) error {
|
||||
inTxOrderType, err := inTxOrderType(ctx, mig.dbClient)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
stmt := fmt.Sprintf(addEventStoreCommandEnforceOwnerColumn, inTxOrderType)
|
||||
_, err = mig.dbClient.ExecContext(ctx, stmt)
|
||||
return err
|
||||
}
|
||||
|
||||
func (mig *AddEventStoreCommandEnforceOwnerColumn) String() string {
|
||||
return "70_add_eventstore_command_type_enforce_owner_column"
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
-- represents an event to be created.
|
||||
DO $$ BEGIN
|
||||
CREATE TYPE eventstore.command2 AS (
|
||||
instance_id TEXT
|
||||
, aggregate_type TEXT
|
||||
, aggregate_id TEXT
|
||||
, command_type TEXT
|
||||
, revision INT2
|
||||
, payload JSONB
|
||||
, creator TEXT
|
||||
, owner TEXT
|
||||
, enforce_owner BOOLEAN
|
||||
);
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
|
||||
DO $$ BEGIN
|
||||
CREATE TYPE eventstore.latest_command AS (
|
||||
instance_id TEXT
|
||||
, aggregate_type TEXT
|
||||
, aggregate_id TEXT
|
||||
, owner TEXT
|
||||
, sequence BIGINT
|
||||
);
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
|
||||
CREATE OR REPLACE FUNCTION eventstore.commands_to_events(commands eventstore.command2[])
|
||||
RETURNS SETOF eventstore.events2
|
||||
LANGUAGE 'plpgsql'
|
||||
STABLE PARALLEL SAFE
|
||||
ROWS 10
|
||||
AS $$
|
||||
DECLARE
|
||||
_latest_events eventstore.latest_command[];
|
||||
command eventstore.command2;
|
||||
last_owner TEXT;
|
||||
i INTEGER;
|
||||
BEGIN
|
||||
|
||||
SELECT array_agg(res) INTO _latest_events
|
||||
FROM (
|
||||
SELECT
|
||||
ROW(
|
||||
c.instance_id,
|
||||
c.aggregate_type,
|
||||
c.aggregate_id,
|
||||
COALESCE(e.owner, c.owner),
|
||||
COALESCE(e.sequence, 0)
|
||||
)::eventstore.latest_command AS res
|
||||
FROM (
|
||||
SELECT DISTINCT ON (c.instance_id, c.aggregate_type, c.aggregate_id)
|
||||
c.instance_id,
|
||||
c.aggregate_type,
|
||||
c.aggregate_id,
|
||||
c.owner,
|
||||
c.ordinality
|
||||
FROM (
|
||||
SELECT *
|
||||
FROM UNNEST(commands) WITH ORDINALITY AS c
|
||||
) AS c
|
||||
ORDER BY
|
||||
c.instance_id,
|
||||
c.aggregate_type,
|
||||
c.aggregate_id,
|
||||
c.ordinality
|
||||
) AS c
|
||||
LEFT JOIN LATERAL (
|
||||
SELECT
|
||||
e.owner,
|
||||
e.sequence
|
||||
FROM eventstore.events2 AS e
|
||||
WHERE e.instance_id = c.instance_id
|
||||
AND e.aggregate_type = c.aggregate_type
|
||||
AND e.aggregate_id = c.aggregate_id
|
||||
ORDER BY e.sequence DESC
|
||||
LIMIT 1
|
||||
) AS e ON TRUE
|
||||
) AS latest;
|
||||
|
||||
FOR command IN SELECT * FROM UNNEST(commands) LOOP
|
||||
IF NOT command.enforce_owner THEN
|
||||
SELECT owner INTO command.owner
|
||||
FROM UNNEST(_latest_events) AS e
|
||||
WHERE e.instance_id = command.instance_id
|
||||
AND e.aggregate_type = command.aggregate_type
|
||||
AND e.aggregate_id = command.aggregate_id;
|
||||
ELSE
|
||||
FOR i IN 1..array_length(_latest_events, 1) LOOP
|
||||
IF _latest_events[i].instance_id = command.instance_id
|
||||
AND _latest_events[i].aggregate_type = command.aggregate_type
|
||||
AND _latest_events[i].aggregate_id = command.aggregate_id
|
||||
THEN
|
||||
_latest_events[i].owner := command.owner;
|
||||
EXIT;
|
||||
END IF;
|
||||
END LOOP;
|
||||
END IF;
|
||||
|
||||
END LOOP;
|
||||
|
||||
|
||||
RETURN QUERY SELECT
|
||||
c.instance_id
|
||||
, c.aggregate_type
|
||||
, c.aggregate_id
|
||||
, c.command_type AS event_type
|
||||
, COALESCE(e.sequence, 0) + ROW_NUMBER() OVER (PARTITION BY c.instance_id, c.aggregate_type, c.aggregate_id ORDER BY c.ordinality) AS sequence
|
||||
, c.revision
|
||||
, NOW() AS created_at
|
||||
, c.payload
|
||||
, c.creator
|
||||
, CASE WHEN c.enforce_owner
|
||||
THEN c.owner
|
||||
ELSE COALESCE(e.owner, c.owner)
|
||||
END AS owner
|
||||
, EXTRACT(EPOCH FROM NOW()) AS position
|
||||
, c.ordinality::%s AS in_tx_order
|
||||
FROM UNNEST(commands) WITH ORDINALITY AS c
|
||||
LEFT JOIN unnest(_latest_events) AS e
|
||||
ON c.instance_id = e.instance_id
|
||||
AND c.aggregate_type = e.aggregate_type
|
||||
AND c.aggregate_id = e.aggregate_id
|
||||
ORDER BY
|
||||
in_tx_order;
|
||||
END;
|
||||
$$;
|
||||
|
||||
CREATE OR REPLACE FUNCTION eventstore.push(commands eventstore.command2[]) RETURNS SETOF eventstore.events2 VOLATILE AS $$
|
||||
INSERT INTO eventstore.events2
|
||||
SELECT * FROM eventstore.commands_to_events(commands)
|
||||
ORDER BY in_tx_order
|
||||
RETURNING *
|
||||
$$ LANGUAGE SQL;
|
||||
@@ -185,6 +185,7 @@ type Steps struct {
|
||||
s67SyncMemberRoleFields *SyncMemberRoleFields
|
||||
s68TargetAddPayloadTypeColumn *TargetAddPayloadTypeColumn
|
||||
s69CacheTablesLogged *CacheTablesLogged
|
||||
s70AddEventStoreCommandEnforceOwner *AddEventStoreCommandEnforceOwnerColumn
|
||||
RelationalTables *TransactionalTables
|
||||
}
|
||||
|
||||
|
||||
@@ -248,6 +248,7 @@ func Setup(ctx context.Context, config *Config, steps *Steps, masterKey string)
|
||||
steps.s67SyncMemberRoleFields = &SyncMemberRoleFields{dbClient: dbClient}
|
||||
steps.s68TargetAddPayloadTypeColumn = &TargetAddPayloadTypeColumn{dbClient: dbClient}
|
||||
steps.s69CacheTablesLogged = &CacheTablesLogged{dbClient: dbClient}
|
||||
steps.s70AddEventStoreCommandEnforceOwner = &AddEventStoreCommandEnforceOwnerColumn{dbClient: dbClient}
|
||||
|
||||
err = projection.Create(ctx, dbClient, eventstoreClient, config.Projections, nil, nil, nil)
|
||||
if err != nil {
|
||||
@@ -257,6 +258,7 @@ func Setup(ctx context.Context, config *Config, steps *Steps, masterKey string)
|
||||
for _, step := range []migration.Migration{
|
||||
steps.s14NewEventsTable,
|
||||
steps.s40InitPushFunc,
|
||||
steps.s70AddEventStoreCommandEnforceOwner,
|
||||
steps.s1ProjectionTable,
|
||||
steps.s2AssetsTable,
|
||||
steps.s28AddFieldTable,
|
||||
|
||||
Reference in New Issue
Block a user