records, outbox, lifecycle requests, relay, push, read-back, and FedWiki lifecycle workflows - Add core.provider_connections table and unique provider connection per provider - Add core.target_mappings table linking connections to resource keys and targets - Add core.desired_state_records table storing desired entitlements per connection and recipient with generation and content hash - Add core.observed_projections table for application-reported state per connection and recipient - Add core.lifecycle_requests table recording lifecycle requests by instance and generation - Add core.person_cut_offs table and setter function to record persons cut off everywhere - Add core.desired_state_outbox table with triggers on entitlement, membership, organization, connection, mapping, and cut-off tables to write change notices - Enforce constraints on recipients, generations, and statuses across tables - Implement desired_state_content SQL function computing record content per recipient rules - Implement relay workflow to claim outbox rows, expand to records, recompute records, and dispatch pushes - Implement push workflow to push records, handle transport answers, and record push results - Implement read-back workflow to page through application state, update projections, and raise records as needed - Add lifecycle request writers with idempotent recording, finishing, and acknowledgment - Add FedWiki integration lifecycle workflows for create, set status, swap, delete, and reconcile - Add FedWiki web handlers and forms for lifecycle requests, including create form with instance and generation - Add tests for schema, triggers, relay, push, read-back, lifecycle requests, FedWiki workflows, and operator pages - Add documentation updates for provider integration, entitlements, identity, organizations, lifecycle, and FedWiki integration - Add benchmarks for delivery and push workflows - Ensure schema-per-provider for in-tree integrations; connector state lives in generic core tables keyed by connection - Secure functions and triggers with SECURITY DEFINER and restrict core_writer role privileges accordingly - Handle Temporal client integration for workflow execution and scheduling - Provide a recording transport for integration tests - Close Issues 32 and 33 by implementing provider connections and desired-state records with lifecycle requests and relay mechanisms
302 lines
12 KiB
Go
302 lines
12 KiB
Go
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Commercial
|
|
// SPDX-FileCopyrightText: 2025-2026 Christian Galo
|
|
|
|
// Code generated by sqlc. DO NOT EDIT.
|
|
// versions:
|
|
// sqlc v1.29.0
|
|
// source: observed.sql
|
|
|
|
package integration
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"encoding/json"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
)
|
|
|
|
const getObservedProjection = `-- name: GetObservedProjection :one
|
|
SELECT projection_id, connection_id, person_id, org_id, applied_generation, applied_content_hash, observed, result, result_generation, next_action, observed_at, ahead_generation, ahead_at, created_at, updated_at FROM core.observed_projections op
|
|
WHERE (op.connection_id = $1::uuid AND op.person_id = $2::uuid)
|
|
OR (op.connection_id = $1::uuid AND op.org_id = $3::uuid)
|
|
`
|
|
|
|
type GetObservedProjectionParams struct {
|
|
ConnectionID string `json:"connection_id"`
|
|
PersonID uuid.NullUUID `json:"person_id"`
|
|
OrgID uuid.NullUUID `json:"org_id"`
|
|
}
|
|
|
|
// The projection at one address; exactly one of person_id and org_id is set.
|
|
// sql.ErrNoRows when the application has never answered for the recipient.
|
|
func (q *Queries) GetObservedProjection(ctx context.Context, arg GetObservedProjectionParams) (ObservedProjection, error) {
|
|
row := q.db.QueryRowContext(ctx, getObservedProjection, arg.ConnectionID, arg.PersonID, arg.OrgID)
|
|
var i ObservedProjection
|
|
err := row.Scan(
|
|
&i.ProjectionID,
|
|
&i.ConnectionID,
|
|
&i.PersonID,
|
|
&i.OrgID,
|
|
&i.AppliedGeneration,
|
|
&i.AppliedContentHash,
|
|
&i.Observed,
|
|
&i.Result,
|
|
&i.ResultGeneration,
|
|
&i.NextAction,
|
|
&i.ObservedAt,
|
|
&i.AheadGeneration,
|
|
&i.AheadAt,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const markObservedProjectionAheadForOrganization = `-- name: MarkObservedProjectionAheadForOrganization :exec
|
|
INSERT INTO core.observed_projections
|
|
(connection_id, org_id, observed_at, ahead_generation, ahead_at)
|
|
VALUES ($1::uuid, $2::uuid, NOW(),
|
|
$3::bigint, NOW())
|
|
ON CONFLICT (connection_id, org_id) DO UPDATE SET
|
|
ahead_generation = excluded.ahead_generation,
|
|
ahead_at = excluded.ahead_at
|
|
`
|
|
|
|
type MarkObservedProjectionAheadForOrganizationParams struct {
|
|
ConnectionID string `json:"connection_id"`
|
|
OrgID string `json:"org_id"`
|
|
AheadGeneration int64 `json:"ahead_generation"`
|
|
}
|
|
|
|
// The restore marker for an organization; as the person form.
|
|
func (q *Queries) MarkObservedProjectionAheadForOrganization(ctx context.Context, arg MarkObservedProjectionAheadForOrganizationParams) error {
|
|
_, err := q.db.ExecContext(ctx, markObservedProjectionAheadForOrganization, arg.ConnectionID, arg.OrgID, arg.AheadGeneration)
|
|
return err
|
|
}
|
|
|
|
const markObservedProjectionAheadForPerson = `-- name: MarkObservedProjectionAheadForPerson :exec
|
|
INSERT INTO core.observed_projections
|
|
(connection_id, person_id, observed_at, ahead_generation, ahead_at)
|
|
VALUES ($1::uuid, $2::uuid, NOW(),
|
|
$3::bigint, NOW())
|
|
ON CONFLICT (connection_id, person_id) DO UPDATE SET
|
|
ahead_generation = excluded.ahead_generation,
|
|
ahead_at = excluded.ahead_at
|
|
`
|
|
|
|
type MarkObservedProjectionAheadForPersonParams struct {
|
|
ConnectionID string `json:"connection_id"`
|
|
PersonID string `json:"person_id"`
|
|
AheadGeneration int64 `json:"ahead_generation"`
|
|
}
|
|
|
|
// The restore marker (design D19): the application reported a generation
|
|
// above the record's, or the record's own generation with another hash. Only
|
|
// the latest occurrence is kept.
|
|
func (q *Queries) MarkObservedProjectionAheadForPerson(ctx context.Context, arg MarkObservedProjectionAheadForPersonParams) error {
|
|
_, err := q.db.ExecContext(ctx, markObservedProjectionAheadForPerson, arg.ConnectionID, arg.PersonID, arg.AheadGeneration)
|
|
return err
|
|
}
|
|
|
|
const recordPushResultForOrganization = `-- name: RecordPushResultForOrganization :exec
|
|
INSERT INTO core.observed_projections
|
|
(connection_id, org_id, applied_generation, applied_content_hash,
|
|
result, result_generation, next_action, observed_at)
|
|
VALUES ($1::uuid, $2::uuid,
|
|
$3::bigint, $4::bytea,
|
|
$5::text, $6::bigint,
|
|
$7::text, $8::timestamptz)
|
|
ON CONFLICT (connection_id, org_id) DO UPDATE SET
|
|
result = excluded.result,
|
|
result_generation = excluded.result_generation,
|
|
next_action = excluded.next_action,
|
|
observed_at = excluded.observed_at,
|
|
applied_content_hash = CASE
|
|
WHEN excluded.applied_generation IS NOT NULL
|
|
AND (observed_projections.applied_generation IS NULL
|
|
OR excluded.applied_generation >= observed_projections.applied_generation)
|
|
THEN excluded.applied_content_hash
|
|
ELSE observed_projections.applied_content_hash END,
|
|
applied_generation = CASE
|
|
WHEN excluded.applied_generation IS NOT NULL
|
|
AND (observed_projections.applied_generation IS NULL
|
|
OR excluded.applied_generation >= observed_projections.applied_generation)
|
|
THEN excluded.applied_generation
|
|
ELSE observed_projections.applied_generation END
|
|
`
|
|
|
|
type RecordPushResultForOrganizationParams struct {
|
|
ConnectionID string `json:"connection_id"`
|
|
OrgID string `json:"org_id"`
|
|
AppliedGeneration sql.NullInt64 `json:"applied_generation"`
|
|
AppliedContentHash []byte `json:"applied_content_hash"`
|
|
Result string `json:"result"`
|
|
ResultGeneration int64 `json:"result_generation"`
|
|
NextAction sql.NullString `json:"next_action"`
|
|
ObservedAt time.Time `json:"observed_at"`
|
|
}
|
|
|
|
// A push answer for an organization; as the person form.
|
|
func (q *Queries) RecordPushResultForOrganization(ctx context.Context, arg RecordPushResultForOrganizationParams) error {
|
|
_, err := q.db.ExecContext(ctx, recordPushResultForOrganization,
|
|
arg.ConnectionID,
|
|
arg.OrgID,
|
|
arg.AppliedGeneration,
|
|
arg.AppliedContentHash,
|
|
arg.Result,
|
|
arg.ResultGeneration,
|
|
arg.NextAction,
|
|
arg.ObservedAt,
|
|
)
|
|
return err
|
|
}
|
|
|
|
const recordPushResultForPerson = `-- name: RecordPushResultForPerson :exec
|
|
INSERT INTO core.observed_projections
|
|
(connection_id, person_id, applied_generation, applied_content_hash,
|
|
result, result_generation, next_action, observed_at)
|
|
VALUES ($1::uuid, $2::uuid,
|
|
$3::bigint, $4::bytea,
|
|
$5::text, $6::bigint,
|
|
$7::text, $8::timestamptz)
|
|
ON CONFLICT (connection_id, person_id) DO UPDATE SET
|
|
result = excluded.result,
|
|
result_generation = excluded.result_generation,
|
|
next_action = excluded.next_action,
|
|
observed_at = excluded.observed_at,
|
|
applied_content_hash = CASE
|
|
WHEN excluded.applied_generation IS NOT NULL
|
|
AND (observed_projections.applied_generation IS NULL
|
|
OR excluded.applied_generation >= observed_projections.applied_generation)
|
|
THEN excluded.applied_content_hash
|
|
ELSE observed_projections.applied_content_hash END,
|
|
applied_generation = CASE
|
|
WHEN excluded.applied_generation IS NOT NULL
|
|
AND (observed_projections.applied_generation IS NULL
|
|
OR excluded.applied_generation >= observed_projections.applied_generation)
|
|
THEN excluded.applied_generation
|
|
ELSE observed_projections.applied_generation END
|
|
`
|
|
|
|
type RecordPushResultForPersonParams struct {
|
|
ConnectionID string `json:"connection_id"`
|
|
PersonID string `json:"person_id"`
|
|
AppliedGeneration sql.NullInt64 `json:"applied_generation"`
|
|
AppliedContentHash []byte `json:"applied_content_hash"`
|
|
Result string `json:"result"`
|
|
ResultGeneration int64 `json:"result_generation"`
|
|
NextAction sql.NullString `json:"next_action"`
|
|
ObservedAt time.Time `json:"observed_at"`
|
|
}
|
|
|
|
// A push answer for a person: the result, the generation it was about and
|
|
// the next-action key always replace what was stored; the applied generation
|
|
// and its hash move together and only upwards. An applied answer reports the
|
|
// pushed generation and hash; an unlinked one reports the generation the
|
|
// application still holds and no hash.
|
|
func (q *Queries) RecordPushResultForPerson(ctx context.Context, arg RecordPushResultForPersonParams) error {
|
|
_, err := q.db.ExecContext(ctx, recordPushResultForPerson,
|
|
arg.ConnectionID,
|
|
arg.PersonID,
|
|
arg.AppliedGeneration,
|
|
arg.AppliedContentHash,
|
|
arg.Result,
|
|
arg.ResultGeneration,
|
|
arg.NextAction,
|
|
arg.ObservedAt,
|
|
)
|
|
return err
|
|
}
|
|
|
|
const upsertObservedProjectionForOrganization = `-- name: UpsertObservedProjectionForOrganization :execrows
|
|
INSERT INTO core.observed_projections
|
|
(connection_id, org_id, applied_generation, applied_content_hash, observed, observed_at)
|
|
VALUES ($1::uuid, $2::uuid,
|
|
$3::bigint, $4::bytea,
|
|
$5::jsonb, $6::timestamptz)
|
|
ON CONFLICT (connection_id, org_id) DO UPDATE SET
|
|
applied_generation = excluded.applied_generation,
|
|
applied_content_hash = excluded.applied_content_hash,
|
|
observed = excluded.observed,
|
|
observed_at = excluded.observed_at
|
|
WHERE excluded.applied_generation IS NOT NULL
|
|
AND (observed_projections.applied_generation IS NULL
|
|
OR excluded.applied_generation >= observed_projections.applied_generation)
|
|
`
|
|
|
|
type UpsertObservedProjectionForOrganizationParams struct {
|
|
ConnectionID string `json:"connection_id"`
|
|
OrgID string `json:"org_id"`
|
|
AppliedGeneration sql.NullInt64 `json:"applied_generation"`
|
|
AppliedContentHash []byte `json:"applied_content_hash"`
|
|
Observed json.RawMessage `json:"observed"`
|
|
ObservedAt time.Time `json:"observed_at"`
|
|
}
|
|
|
|
// A read-back item for an organization; as the person form.
|
|
func (q *Queries) UpsertObservedProjectionForOrganization(ctx context.Context, arg UpsertObservedProjectionForOrganizationParams) (int64, error) {
|
|
result, err := q.db.ExecContext(ctx, upsertObservedProjectionForOrganization,
|
|
arg.ConnectionID,
|
|
arg.OrgID,
|
|
arg.AppliedGeneration,
|
|
arg.AppliedContentHash,
|
|
arg.Observed,
|
|
arg.ObservedAt,
|
|
)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
return result.RowsAffected()
|
|
}
|
|
|
|
const upsertObservedProjectionForPerson = `-- name: UpsertObservedProjectionForPerson :execrows
|
|
INSERT INTO core.observed_projections
|
|
(connection_id, person_id, applied_generation, applied_content_hash, observed, observed_at)
|
|
VALUES ($1::uuid, $2::uuid,
|
|
$3::bigint, $4::bytea,
|
|
$5::jsonb, $6::timestamptz)
|
|
ON CONFLICT (connection_id, person_id) DO UPDATE SET
|
|
applied_generation = excluded.applied_generation,
|
|
applied_content_hash = excluded.applied_content_hash,
|
|
observed = excluded.observed,
|
|
observed_at = excluded.observed_at
|
|
WHERE excluded.applied_generation IS NOT NULL
|
|
AND (observed_projections.applied_generation IS NULL
|
|
OR excluded.applied_generation >= observed_projections.applied_generation)
|
|
`
|
|
|
|
type UpsertObservedProjectionForPersonParams struct {
|
|
ConnectionID string `json:"connection_id"`
|
|
PersonID string `json:"person_id"`
|
|
AppliedGeneration sql.NullInt64 `json:"applied_generation"`
|
|
AppliedContentHash []byte `json:"applied_content_hash"`
|
|
Observed json.RawMessage `json:"observed"`
|
|
ObservedAt time.Time `json:"observed_at"`
|
|
}
|
|
|
|
// Observed projections (design D10, D19, D21). Every statement here runs in a
|
|
// transaction that already holds the address's advisory lock and the record
|
|
// row, so the projection row is the last lock taken. Each write comes in a
|
|
// person and an organization form because an upsert names one unique
|
|
// constraint as its conflict target.
|
|
//
|
|
// A read-back item for a person. The stored applied generation is never
|
|
// lowered: a page fetched before a push was applied can arrive after the push
|
|
// answer, and then changes nothing.
|
|
func (q *Queries) UpsertObservedProjectionForPerson(ctx context.Context, arg UpsertObservedProjectionForPersonParams) (int64, error) {
|
|
result, err := q.db.ExecContext(ctx, upsertObservedProjectionForPerson,
|
|
arg.ConnectionID,
|
|
arg.PersonID,
|
|
arg.AppliedGeneration,
|
|
arg.AppliedContentHash,
|
|
arg.Observed,
|
|
arg.ObservedAt,
|
|
)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
return result.RowsAffected()
|
|
}
|