Files
probo/pkg/coredata/membership_profile.go
Cursor Agent 8b6e9e420a Allow ordering profiles by email address
GraphQL profile ordering rejected EMAIL_ADDRESS because ProfileOrderField
did not expose this enum value in connect and console schemas.

Add EMAIL_ADDRESS to MembershipProfileOrderField and its validation list
so order input coercion accepts the value consistently. Extend
MembershipProfile cursor key encoding to support email ordering and avoid
runtime panics during pagination.

Update the MCP profile order enum to keep API surface definitions aligned
with the same ordering capability.

Signed-off-by: Cursor Agent <cursoragent@cursor.com>

Co-authored-by: Bryan FRIMIN <bryan@frimin.fr>
2026-05-28 12:23:17 -07:00

1464 lines
34 KiB
Go

// Copyright (c) 2025-2026 Probo Inc <hello@getprobo.com>.
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
package coredata
import (
"context"
"errors"
"fmt"
"maps"
"time"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgconn"
"go.gearno.de/kit/pg"
"go.probo.inc/probo/pkg/gid"
"go.probo.inc/probo/pkg/iam/policy"
"go.probo.inc/probo/pkg/mail"
"go.probo.inc/probo/pkg/page"
)
type (
MembershipProfile struct {
ID gid.GID `db:"id"`
IdentityID gid.GID `db:"identity_id"`
OrganizationID gid.GID `db:"organization_id"`
EmailAddress mail.Addr `db:"email_address"`
Source ProfileSource `db:"source"`
State ProfileState `db:"state"`
FullName string `db:"full_name"`
Kind *string `db:"kind"`
AdditionalEmailAddresses mail.Addrs `db:"additional_email_addresses"`
Position *string `db:"position"`
ContractStartDate *time.Time `db:"contract_start_date"`
ContractEndDate *time.Time `db:"contract_end_date"`
OrganizationName string `db:"organization_name"`
UserName *string `db:"user_name"`
ExternalID *string `db:"external_id"`
Nickname *string `db:"nickname"`
Locale *string `db:"locale"`
Timezone *string `db:"timezone"`
ProfileUrl *string `db:"profile_url"`
PreferredLanguage *string `db:"preferred_language"`
GivenName *string `db:"given_name"`
FamilyName *string `db:"family_name"`
FormattedName *string `db:"formatted_name"`
MiddleName *string `db:"middle_name"`
HonorificPrefix *string `db:"honorific_prefix"`
HonorificSuffix *string `db:"honorific_suffix"`
EmployeeNumber *string `db:"employee_number"`
Department *string `db:"department"`
CostCenter *string `db:"cost_center"`
EnterpriseOrganization *string `db:"enterprise_organization"`
Division *string `db:"division"`
ManagerValue *string `db:"manager_value"`
CreatedAt time.Time `db:"created_at"`
UpdatedAt time.Time `db:"updated_at"`
}
MembershipProfiles []*MembershipProfile
)
func (p MembershipProfile) CursorKey(orderBy MembershipProfileOrderField) page.CursorKey {
switch orderBy {
case MembershipProfileOrderFieldCreatedAt:
return page.NewCursorKey(p.ID, p.CreatedAt)
case MembershipProfileOrderFieldFullName:
return page.NewCursorKey(p.ID, p.FullName)
case MembershipProfileOrderFieldEmailAddress:
return page.NewCursorKey(p.ID, p.EmailAddress)
case MembershipProfileOrderFieldKind:
return page.NewCursorKey(p.ID, p.Kind)
case MembershipProfileOrderFieldOrganizationName:
return page.NewCursorKey(p.ID, p.OrganizationName)
case MembershipProfileOrderFieldState:
return page.NewCursorKey(p.ID, p.State)
}
panic(fmt.Sprintf("unsupported order by: %s", orderBy))
}
func (p *MembershipProfile) AuthorizationAttributes(
ctx context.Context,
conn pg.Querier,
resourceIDs []gid.GID,
) (policy.AttributesByID, error) {
q := `
SELECT
id,
organization_id,
identity_id
FROM
iam_membership_profiles
WHERE
id = ANY(@resource_ids::text[])
`
args := pgx.StrictNamedArgs{
"resource_ids": resourceIDs,
}
rows, err := conn.Query(ctx, q, args)
if err != nil {
return nil, fmt.Errorf("cannot query profile authorization attributes: %w", err)
}
defer rows.Close()
attrsByID := make(policy.AttributesByID, len(resourceIDs))
for rows.Next() {
var (
id gid.GID
organizationID gid.GID
identityID gid.GID
)
err = rows.Scan(&id, &organizationID, &identityID)
if err != nil {
return nil, fmt.Errorf("cannot scan profile authorization attributes: %w", err)
}
attrsByID[id] = policy.Attributes{
"organization_id": organizationID.String(),
"identity_id": identityID.String(),
}
}
if err = rows.Err(); err != nil {
return nil, fmt.Errorf("cannot iterate profile authorization attributes: %w", err)
}
return attrsByID, nil
}
func (p *MembershipProfile) LoadByID(
ctx context.Context,
conn pg.Querier,
scope Scoper,
profileID gid.GID,
) error {
q := `
SELECT
p.id,
p.identity_id,
p.organization_id,
i.email_address,
p.source,
p.state,
p.full_name,
p.kind,
p.additional_email_addresses,
p.position,
p.contract_start_date,
p.contract_end_date,
'' AS organization_name,
p.user_name,
p.external_id,
p.nickname,
p.locale,
p.timezone,
p.profile_url,
p.preferred_language,
p.given_name,
p.family_name,
p.formatted_name,
p.middle_name,
p.honorific_prefix,
p.honorific_suffix,
p.employee_number,
p.department,
p.cost_center,
p.enterprise_organization,
p.division,
p.manager_value,
p.created_at,
p.updated_at
FROM
iam_membership_profiles p
INNER JOIN identities i
ON i.id = p.identity_id
WHERE
p.%s
AND p.id = @profile_id
LIMIT 1;
`
q = fmt.Sprintf(q, scope.SQLFragment())
args := pgx.StrictNamedArgs{"profile_id": profileID}
maps.Copy(args, scope.SQLArguments())
rows, err := conn.Query(ctx, q, args)
if err != nil {
return fmt.Errorf("cannot query profile: %w", err)
}
profile, err := pgx.CollectExactlyOneRow(rows, pgx.RowToStructByName[MembershipProfile])
if err != nil {
if errors.Is(err, pgx.ErrNoRows) {
return ErrResourceNotFound
}
return fmt.Errorf("cannot collect profile: %w", err)
}
*p = profile
return nil
}
func (p *MembershipProfile) LoadByIdentityIDAndOrganizationID(
ctx context.Context,
conn pg.Querier,
scope Scoper,
identityID gid.GID,
organizationID gid.GID,
) error {
q := `
SELECT
p.id,
p.identity_id,
p.organization_id,
i.email_address,
p.source,
p.state,
p.full_name,
p.kind,
p.additional_email_addresses,
p.position,
p.contract_start_date,
p.contract_end_date,
'' AS organization_name,
p.user_name,
p.external_id,
p.nickname,
p.locale,
p.timezone,
p.profile_url,
p.preferred_language,
p.given_name,
p.family_name,
p.formatted_name,
p.middle_name,
p.honorific_prefix,
p.honorific_suffix,
p.employee_number,
p.department,
p.cost_center,
p.enterprise_organization,
p.division,
p.manager_value,
p.created_at,
p.updated_at
FROM
iam_membership_profiles p
INNER JOIN identities i
ON i.id = p.identity_id
WHERE
p.%s
AND p.identity_id = @identity_id
AND p.organization_id = @organization_id
LIMIT 1;
`
q = fmt.Sprintf(q, scope.SQLFragment())
args := pgx.StrictNamedArgs{
"identity_id": identityID,
"organization_id": organizationID,
}
maps.Copy(args, scope.SQLArguments())
rows, err := conn.Query(ctx, q, args)
if err != nil {
return fmt.Errorf("cannot query profile: %w", err)
}
profile, err := pgx.CollectExactlyOneRow(rows, pgx.RowToStructByName[MembershipProfile])
if err != nil {
if errors.Is(err, pgx.ErrNoRows) {
return ErrResourceNotFound
}
return fmt.Errorf("cannot collect profile: %w", err)
}
*p = profile
return nil
}
func (p *MembershipProfile) LoadByExternalIDAndOrganizationID(
ctx context.Context,
conn pg.Querier,
scope Scoper,
externalID string,
organizationID gid.GID,
) error {
q := `
SELECT
p.id,
p.identity_id,
p.organization_id,
i.email_address,
p.source,
p.state,
p.full_name,
p.kind,
p.additional_email_addresses,
p.position,
p.contract_start_date,
p.contract_end_date,
'' AS organization_name,
p.user_name,
p.external_id,
p.nickname,
p.locale,
p.timezone,
p.profile_url,
p.preferred_language,
p.given_name,
p.family_name,
p.formatted_name,
p.middle_name,
p.honorific_prefix,
p.honorific_suffix,
p.employee_number,
p.department,
p.cost_center,
p.enterprise_organization,
p.division,
p.manager_value,
p.created_at,
p.updated_at
FROM
iam_membership_profiles p
INNER JOIN identities i
ON i.id = p.identity_id
WHERE
p.%s
AND p.external_id = @external_id
AND p.organization_id = @organization_id
LIMIT 1;
`
q = fmt.Sprintf(q, scope.SQLFragment())
args := pgx.StrictNamedArgs{
"external_id": externalID,
"organization_id": organizationID,
}
maps.Copy(args, scope.SQLArguments())
rows, err := conn.Query(ctx, q, args)
if err != nil {
return fmt.Errorf("cannot query profile: %w", err)
}
profile, err := pgx.CollectExactlyOneRow(rows, pgx.RowToStructByName[MembershipProfile])
if err != nil {
if errors.Is(err, pgx.ErrNoRows) {
return ErrResourceNotFound
}
return fmt.Errorf("cannot collect profile: %w", err)
}
*p = profile
return nil
}
func (p *MembershipProfiles) LoadByIDs(
ctx context.Context,
conn pg.Querier,
scope Scoper,
profileIDs []gid.GID,
) error {
q := `
SELECT
p.id,
p.identity_id,
p.organization_id,
i.email_address,
p.source,
p.state,
p.full_name,
p.kind,
p.additional_email_addresses,
p.position,
p.contract_start_date,
p.contract_end_date,
'' AS organization_name,
p.user_name,
p.external_id,
p.nickname,
p.locale,
p.timezone,
p.profile_url,
p.preferred_language,
p.given_name,
p.family_name,
p.formatted_name,
p.middle_name,
p.honorific_prefix,
p.honorific_suffix,
p.employee_number,
p.department,
p.cost_center,
p.enterprise_organization,
p.division,
p.manager_value,
p.created_at,
p.updated_at
FROM
iam_membership_profiles p
INNER JOIN identities i
ON i.id = p.identity_id
WHERE
p.%s
AND p.id = ANY(@profile_ids)
`
q = fmt.Sprintf(q, scope.SQLFragment())
args := pgx.NamedArgs{"profile_ids": profileIDs}
maps.Copy(args, scope.SQLArguments())
rows, err := conn.Query(ctx, q, args)
if err != nil {
return fmt.Errorf("cannot query profiles: %w", err)
}
profiles, err := pgx.CollectRows(rows, pgx.RowToAddrOfStructByName[MembershipProfile])
if err != nil {
return fmt.Errorf("cannot collect profiles: %w", err)
}
*p = profiles
return nil
}
func (p *MembershipProfiles) LoadByOrganizationID(
ctx context.Context,
conn pg.Querier,
scope Scoper,
organizationID gid.GID,
cursor *page.Cursor[MembershipProfileOrderField],
filter *MembershipProfileFilter,
) error {
q := `
WITH profiles AS (
SELECT
p.id,
p.identity_id,
p.organization_id,
i.email_address,
p.source,
p.state,
p.full_name,
p.kind,
p.additional_email_addresses,
p.position,
p.contract_start_date,
p.contract_end_date,
p.user_name,
p.external_id,
p.nickname,
p.locale,
p.timezone,
p.profile_url,
p.preferred_language,
p.given_name,
p.family_name,
p.formatted_name,
p.middle_name,
p.honorific_prefix,
p.honorific_suffix,
p.employee_number,
p.department,
p.cost_center,
p.enterprise_organization,
p.division,
p.manager_value,
p.created_at,
p.updated_at
FROM
iam_membership_profiles p
INNER JOIN identities i ON i.id = p.identity_id
WHERE
p.%s
AND p.organization_id = @organization_id
AND %s
)
SELECT
id,
identity_id,
organization_id,
email_address,
source,
state,
full_name,
kind,
additional_email_addresses,
position,
contract_start_date,
contract_end_date,
'' AS organization_name,
user_name,
external_id,
nickname,
locale,
timezone,
profile_url,
preferred_language,
given_name,
family_name,
formatted_name,
middle_name,
honorific_prefix,
honorific_suffix,
employee_number,
department,
cost_center,
enterprise_organization,
division,
manager_value,
created_at,
updated_at
FROM profiles
WHERE
%s
`
q = fmt.Sprintf(q, scope.SQLFragment(), filter.SQLFragment(), cursor.SQLFragment())
args := pgx.NamedArgs{"organization_id": organizationID}
maps.Copy(args, scope.SQLArguments())
maps.Copy(args, filter.SQLArguments())
maps.Copy(args, cursor.SQLArguments())
rows, err := conn.Query(ctx, q, args)
if err != nil {
return fmt.Errorf("cannot query profiles: %w", err)
}
profiles, err := pgx.CollectRows(rows, pgx.RowToAddrOfStructByName[MembershipProfile])
if err != nil {
return fmt.Errorf("cannot collect profiles: %w", err)
}
*p = profiles
return nil
}
func (p *MembershipProfiles) LoadAllByOrganizationID(
ctx context.Context,
conn pg.Querier,
scope Scoper,
organizationID gid.GID,
filter *MembershipProfileFilter,
) error {
q := `
WITH profiles AS (
SELECT
p.id,
p.identity_id,
p.organization_id,
i.email_address,
p.source,
p.state,
p.full_name,
p.kind,
p.additional_email_addresses,
p.position,
p.contract_start_date,
p.contract_end_date,
p.user_name,
p.external_id,
p.nickname,
p.locale,
p.timezone,
p.profile_url,
p.preferred_language,
p.given_name,
p.family_name,
p.formatted_name,
p.middle_name,
p.honorific_prefix,
p.honorific_suffix,
p.employee_number,
p.department,
p.cost_center,
p.enterprise_organization,
p.division,
p.manager_value,
p.created_at,
p.updated_at
FROM
iam_membership_profiles p
INNER JOIN identities i ON i.id = p.identity_id
WHERE
p.%s
AND p.organization_id = @organization_id
AND %s
)
SELECT
id,
identity_id,
organization_id,
email_address,
source,
state,
full_name,
kind,
additional_email_addresses,
position,
contract_start_date,
contract_end_date,
'' AS organization_name,
user_name,
external_id,
nickname,
locale,
timezone,
profile_url,
preferred_language,
given_name,
family_name,
formatted_name,
middle_name,
honorific_prefix,
honorific_suffix,
employee_number,
department,
cost_center,
enterprise_organization,
division,
manager_value,
created_at,
updated_at
FROM profiles
`
q = fmt.Sprintf(q, scope.SQLFragment(), filter.SQLFragment())
args := pgx.NamedArgs{
"organization_id": organizationID,
}
maps.Copy(args, scope.SQLArguments())
maps.Copy(args, filter.SQLArguments())
rows, err := conn.Query(ctx, q, args)
if err != nil {
return fmt.Errorf("cannot query profiles: %w", err)
}
profiles, err := pgx.CollectRows(rows, pgx.RowToAddrOfStructByName[MembershipProfile])
if err != nil {
return fmt.Errorf("cannot collect profiles: %w", err)
}
*p = profiles
return nil
}
func (p *MembershipProfiles) LoadByIdentityID(
ctx context.Context,
conn pg.Querier,
identityID gid.GID,
cursor *page.Cursor[MembershipProfileOrderField],
filter *MembershipProfileFilter,
) error {
q := `
WITH profiles AS (
SELECT
p.id,
p.identity_id,
p.organization_id,
i.email_address,
p.source,
p.state,
p.full_name,
p.kind,
p.additional_email_addresses,
p.position,
p.contract_start_date,
p.contract_end_date,
p.user_name,
p.external_id,
p.nickname,
p.locale,
p.timezone,
p.profile_url,
p.preferred_language,
p.given_name,
p.family_name,
p.formatted_name,
p.middle_name,
p.honorific_prefix,
p.honorific_suffix,
p.employee_number,
p.department,
p.cost_center,
p.enterprise_organization,
p.division,
p.manager_value,
p.created_at,
p.updated_at
FROM
iam_membership_profiles p
INNER JOIN identities i ON i.id = p.identity_id
WHERE
p.identity_id = @identity_id
AND %s
)
SELECT
p.id,
p.identity_id,
p.organization_id,
p.email_address,
p.source,
p.state,
p.full_name,
p.kind,
p.additional_email_addresses,
p.position,
p.contract_start_date,
p.contract_end_date,
o.name AS organization_name,
p.user_name,
p.external_id,
p.nickname,
p.locale,
p.timezone,
p.profile_url,
p.preferred_language,
p.given_name,
p.family_name,
p.formatted_name,
p.middle_name,
p.honorific_prefix,
p.honorific_suffix,
p.employee_number,
p.department,
p.cost_center,
p.enterprise_organization,
p.division,
p.manager_value,
p.created_at,
p.updated_at
FROM profiles p
INNER JOIN organizations o ON o.id = p.organization_id
WHERE
%s
`
q = fmt.Sprintf(q, filter.SQLFragment(), cursor.SQLFragment())
args := pgx.NamedArgs{"identity_id": identityID}
maps.Copy(args, filter.SQLArguments())
maps.Copy(args, cursor.SQLArguments())
rows, err := conn.Query(ctx, q, args)
if err != nil {
return fmt.Errorf("cannot query profiles: %w", err)
}
profiles, err := pgx.CollectRows(rows, pgx.RowToAddrOfStructByName[MembershipProfile])
if err != nil {
return fmt.Errorf("cannot collect profiles: %w", err)
}
*p = profiles
return nil
}
func (p *MembershipProfiles) LoadByDocumentVersionID(
ctx context.Context,
conn pg.Querier,
scope Scoper,
documentVersionID gid.GID,
cursor *page.Cursor[MembershipProfileOrderField],
) error {
q := `
WITH latest_quorum AS (
SELECT id
FROM document_version_approval_quorums
WHERE version_id = @version_id
ORDER BY created_at DESC
LIMIT 1
),
version_approvers AS (
SELECT d.approver_id
FROM document_version_approval_decisions d
WHERE d.quorum_id = (SELECT id FROM latest_quorum)
),
profiles AS (
SELECT
mp.id,
mp.identity_id,
mp.organization_id,
mp.full_name,
mp.source,
mp.state,
mp.kind,
mp.additional_email_addresses,
mp.position,
mp.contract_start_date,
mp.contract_end_date,
mp.user_name,
mp.external_id,
mp.nickname,
mp.locale,
mp.timezone,
mp.profile_url,
mp.preferred_language,
mp.given_name,
mp.family_name,
mp.formatted_name,
mp.middle_name,
mp.honorific_prefix,
mp.honorific_suffix,
mp.employee_number,
mp.department,
mp.cost_center,
mp.enterprise_organization,
mp.division,
mp.manager_value,
mp.created_at,
mp.updated_at
FROM
iam_membership_profiles mp
INNER JOIN version_approvers va ON va.approver_id = mp.id
WHERE
mp.%s
AND %s
)
SELECT
p.id,
p.identity_id,
p.organization_id,
i.email_address,
p.source,
p.state,
p.full_name,
p.kind,
p.additional_email_addresses,
p.position,
p.contract_start_date,
p.contract_end_date,
'' AS organization_name,
p.user_name,
p.external_id,
p.nickname,
p.locale,
p.timezone,
p.profile_url,
p.preferred_language,
p.given_name,
p.family_name,
p.formatted_name,
p.middle_name,
p.honorific_prefix,
p.honorific_suffix,
p.employee_number,
p.department,
p.cost_center,
p.enterprise_organization,
p.division,
p.manager_value,
p.created_at,
p.updated_at
FROM profiles p
INNER JOIN identities i ON i.id = p.identity_id
`
q = fmt.Sprintf(q, scope.SQLFragment(), cursor.SQLFragment())
args := pgx.NamedArgs{"version_id": documentVersionID}
maps.Copy(args, scope.SQLArguments())
maps.Copy(args, cursor.SQLArguments())
rows, err := conn.Query(ctx, q, args)
if err != nil {
return fmt.Errorf("cannot query document version approver profiles: %w", err)
}
profiles, err := pgx.CollectRows(rows, pgx.RowToAddrOfStructByName[MembershipProfile])
if err != nil {
return fmt.Errorf("cannot collect document version approver profiles: %w", err)
}
*p = profiles
return nil
}
func (p *MembershipProfiles) CountByDocumentVersionID(
ctx context.Context,
conn pg.Querier,
scope Scoper,
documentVersionID gid.GID,
) (int, error) {
q := `
WITH latest_quorum AS (
SELECT id
FROM document_version_approval_quorums
WHERE version_id = @version_id
ORDER BY created_at DESC
LIMIT 1
)
SELECT
COUNT(DISTINCT mp.id)
FROM
iam_membership_profiles mp
INNER JOIN document_version_approval_decisions dvad ON mp.id = dvad.approver_id
INNER JOIN latest_quorum lq ON lq.id = dvad.quorum_id
WHERE
mp.%s
`
q = fmt.Sprintf(q, scope.SQLFragment())
args := pgx.StrictNamedArgs{"version_id": documentVersionID}
maps.Copy(args, scope.SQLArguments())
var count int
err := conn.QueryRow(ctx, q, args).Scan(&count)
if err != nil {
return 0, fmt.Errorf("cannot query document version approver profiles count: %w", err)
}
return count, nil
}
func (p *MembershipProfiles) LoadAwaitingSigning(
ctx context.Context,
conn pg.Querier,
scope Scoper,
) error {
q := `
WITH signatories AS (
SELECT
signed_by_profile_id
FROM
document_version_signatures
WHERE
%s
AND state = 'REQUESTED'
GROUP BY
signed_by_profile_id
)
SELECT
p.id,
p.identity_id,
p.organization_id,
p.kind,
p.full_name,
i.email_address,
p.source,
p.state,
p.additional_email_addresses,
p.position,
p.contract_start_date,
p.contract_end_date,
'' AS organization_name,
p.user_name,
p.external_id,
p.nickname,
p.locale,
p.timezone,
p.profile_url,
p.preferred_language,
p.given_name,
p.family_name,
p.formatted_name,
p.middle_name,
p.honorific_prefix,
p.honorific_suffix,
p.employee_number,
p.department,
p.cost_center,
p.enterprise_organization,
p.division,
p.manager_value,
p.created_at,
p.updated_at
FROM
iam_membership_profiles p
INNER JOIN identities i
ON i.id = p.identity_id
INNER JOIN signatories ON p.id = signatories.signed_by_profile_id
`
q = fmt.Sprintf(q, scope.SQLFragment())
rows, err := conn.Query(ctx, q, scope.SQLArguments())
if err != nil {
return fmt.Errorf("cannot query profiles: %w", err)
}
profiles, err := pgx.CollectRows(rows, pgx.RowToAddrOfStructByName[MembershipProfile])
if err != nil {
return fmt.Errorf("cannot collect profiles: %w", err)
}
*p = profiles
return nil
}
func (p *MembershipProfiles) CountByIdentityID(
ctx context.Context,
conn pg.Querier,
identityID gid.GID,
filter *MembershipProfileFilter,
) (int, error) {
q := `
SELECT
COUNT(*)
FROM
iam_membership_profiles p
INNER JOIN identities i ON i.id = p.identity_id
WHERE
%s
AND p.identity_id = @identity_id
`
q = fmt.Sprintf(q, filter.SQLFragment())
args := pgx.StrictNamedArgs{"identity_id": identityID}
maps.Copy(args, filter.SQLArguments())
row := conn.QueryRow(ctx, q, args)
var count int
err := row.Scan(&count)
if err != nil {
return 0, fmt.Errorf("cannot collect count: %w", err)
}
return count, nil
}
func (p *MembershipProfiles) CountByOrganizationID(
ctx context.Context,
conn pg.Querier,
scope Scoper,
organizationID gid.GID,
filter *MembershipProfileFilter,
) (int, error) {
q := `
SELECT
COUNT(*)
FROM
iam_membership_profiles p
INNER JOIN identities i ON i.id = p.identity_id
WHERE
p.%s
AND %s
AND p.organization_id = @organization_id
`
q = fmt.Sprintf(q, scope.SQLFragment(), filter.SQLFragment())
args := pgx.StrictNamedArgs{"organization_id": organizationID}
maps.Copy(args, scope.SQLArguments())
maps.Copy(args, filter.SQLArguments())
row := conn.QueryRow(ctx, q, args)
var count int
err := row.Scan(&count)
if err != nil {
return 0, fmt.Errorf("cannot collect count: %w", err)
}
return count, nil
}
func (p *MembershipProfiles) CountActiveOwnerByOrganizationID(
ctx context.Context,
conn pg.Querier,
scope Scoper,
organizationID gid.GID,
) (int, error) {
q := `
SELECT
COUNT(*)
FROM
iam_membership_profiles p
INNER JOIN iam_memberships m ON m.identity_id = p.identity_id AND m.organization_id = p.organization_id
WHERE
p.%s
AND p.organization_id = @organization_id
AND p.state = @state
AND m.role = @role
`
q = fmt.Sprintf(q, scope.SQLFragment())
args := pgx.StrictNamedArgs{
"state": ProfileStateActive,
"role": MembershipRoleOwner,
"organization_id": organizationID,
}
maps.Copy(args, scope.SQLArguments())
row := conn.QueryRow(ctx, q, args)
var count int
err := row.Scan(&count)
if err != nil {
return 0, fmt.Errorf("cannot collect count: %w", err)
}
return count, nil
}
func (p *MembershipProfile) Insert(
ctx context.Context,
conn pg.Tx,
) error {
q := `
INSERT INTO
iam_membership_profiles (
tenant_id,
id,
identity_id,
organization_id,
source,
state,
full_name,
kind,
additional_email_addresses,
position,
contract_start_date,
contract_end_date,
user_name,
external_id,
nickname,
locale,
timezone,
profile_url,
preferred_language,
given_name,
family_name,
formatted_name,
middle_name,
honorific_prefix,
honorific_suffix,
employee_number,
department,
cost_center,
enterprise_organization,
division,
manager_value,
created_at,
updated_at
)
VALUES (
@tenant_id,
@id,
@identity_id,
@organization_id,
@source,
@state,
@full_name,
@kind,
COALESCE(@additional_email_addresses, '{}'::CITEXT[]),
@position,
@contract_start_date,
@contract_end_date,
@user_name,
@external_id,
@nickname,
@locale,
@timezone,
@profile_url,
@preferred_language,
@given_name,
@family_name,
@formatted_name,
@middle_name,
@honorific_prefix,
@honorific_suffix,
@employee_number,
@department,
@cost_center,
@enterprise_organization,
@division,
@manager_value,
@created_at,
@updated_at
)
`
args := pgx.StrictNamedArgs{
"tenant_id": p.ID.TenantID().String(),
"id": p.ID,
"identity_id": p.IdentityID,
"organization_id": p.OrganizationID,
"source": p.Source,
"state": p.State,
"full_name": p.FullName,
"kind": p.Kind,
"additional_email_addresses": p.AdditionalEmailAddresses,
"position": p.Position,
"contract_start_date": p.ContractStartDate,
"contract_end_date": p.ContractEndDate,
"user_name": p.UserName,
"external_id": p.ExternalID,
"nickname": p.Nickname,
"locale": p.Locale,
"timezone": p.Timezone,
"profile_url": p.ProfileUrl,
"preferred_language": p.PreferredLanguage,
"given_name": p.GivenName,
"family_name": p.FamilyName,
"formatted_name": p.FormattedName,
"middle_name": p.MiddleName,
"honorific_prefix": p.HonorificPrefix,
"honorific_suffix": p.HonorificSuffix,
"employee_number": p.EmployeeNumber,
"department": p.Department,
"cost_center": p.CostCenter,
"enterprise_organization": p.EnterpriseOrganization,
"division": p.Division,
"manager_value": p.ManagerValue,
"created_at": p.CreatedAt,
"updated_at": p.UpdatedAt,
}
_, err := conn.Exec(ctx, q, args)
if err != nil {
if pgErr, ok := errors.AsType[*pgconn.PgError](err); ok && pgErr.Code == "23505" {
switch pgErr.ConstraintName {
case "idx_profiles_identity_id_organization_id",
"idx_profiles_external_id_organization_id",
"idx_profiles_user_name_organization_id":
return ErrResourceAlreadyExists
}
}
return fmt.Errorf("cannot insert profile: %w", err)
}
return nil
}
func (p *MembershipProfile) Update(
ctx context.Context,
conn pg.Tx,
scope Scoper,
) error {
q := `
UPDATE
iam_membership_profiles
SET
identity_id = @identity_id,
source = @source,
state = @state,
full_name = @full_name,
kind = @kind,
additional_email_addresses = COALESCE(@additional_email_addresses, '{}'::CITEXT[]),
position = @position,
contract_start_date = @contract_start_date,
contract_end_date = @contract_end_date,
user_name = @user_name,
external_id = @external_id,
nickname = @nickname,
locale = @locale,
timezone = @timezone,
profile_url = @profile_url,
preferred_language = @preferred_language,
given_name = @given_name,
family_name = @family_name,
formatted_name = @formatted_name,
middle_name = @middle_name,
honorific_prefix = @honorific_prefix,
honorific_suffix = @honorific_suffix,
employee_number = @employee_number,
department = @department,
cost_center = @cost_center,
enterprise_organization = @enterprise_organization,
division = @division,
manager_value = @manager_value,
updated_at = @updated_at
WHERE
id = @id
AND %s
`
q = fmt.Sprintf(q, scope.SQLFragment())
args := pgx.StrictNamedArgs{
"id": p.ID,
"identity_id": p.IdentityID,
"source": p.Source,
"state": p.State,
"full_name": p.FullName,
"kind": p.Kind,
"additional_email_addresses": p.AdditionalEmailAddresses,
"position": p.Position,
"contract_start_date": p.ContractStartDate,
"contract_end_date": p.ContractEndDate,
"user_name": p.UserName,
"external_id": p.ExternalID,
"nickname": p.Nickname,
"locale": p.Locale,
"timezone": p.Timezone,
"profile_url": p.ProfileUrl,
"preferred_language": p.PreferredLanguage,
"given_name": p.GivenName,
"family_name": p.FamilyName,
"formatted_name": p.FormattedName,
"middle_name": p.MiddleName,
"honorific_prefix": p.HonorificPrefix,
"honorific_suffix": p.HonorificSuffix,
"employee_number": p.EmployeeNumber,
"department": p.Department,
"cost_center": p.CostCenter,
"enterprise_organization": p.EnterpriseOrganization,
"division": p.Division,
"manager_value": p.ManagerValue,
"updated_at": p.UpdatedAt,
}
maps.Copy(args, scope.SQLArguments())
result, err := conn.Exec(ctx, q, args)
if err != nil {
return fmt.Errorf("cannot update profile: %w", err)
}
if result.RowsAffected() == 0 {
return ErrResourceNotFound
}
return nil
}
func (p *MembershipProfiles) ResetSCIMSources(
ctx context.Context,
conn pg.Querier,
scope Scoper,
organizationID gid.GID,
) error {
q := `
UPDATE iam_membership_profiles
SET
source = 'MANUAL',
external_id = NULL,
user_name = NULL,
updated_at = @updated_at
WHERE
%s
AND organization_id = @organization_id
AND source = 'SCIM'
`
q = fmt.Sprintf(q, scope.SQLFragment())
args := pgx.NamedArgs{
"organization_id": organizationID,
"updated_at": time.Now(),
}
maps.Copy(args, scope.SQLArguments())
_, err := conn.Exec(ctx, q, args)
if err != nil {
return fmt.Errorf("cannot reset SCIM user sources: %w", err)
}
return nil
}
func (p *MembershipProfile) ClearExternalID(
ctx context.Context,
conn pg.Querier,
scope Scoper,
externalID string,
organizationID gid.GID,
) error {
q := `
UPDATE iam_membership_profiles
SET
external_id = NULL,
updated_at = @updated_at
WHERE
%s
AND external_id = @external_id
AND organization_id = @organization_id
AND id != @exclude_id
`
q = fmt.Sprintf(q, scope.SQLFragment())
args := pgx.NamedArgs{
"external_id": externalID,
"organization_id": organizationID,
"exclude_id": p.ID,
"updated_at": time.Now(),
}
maps.Copy(args, scope.SQLArguments())
_, err := conn.Exec(ctx, q, args)
if err != nil {
return fmt.Errorf("cannot clear external id: %w", err)
}
return nil
}
func (p *MembershipProfile) Delete(
ctx context.Context,
conn pg.Tx,
scope Scoper,
profileID gid.GID,
) error {
q := `
DELETE FROM
iam_membership_profiles
WHERE
id = @profile_id
AND %s
`
q = fmt.Sprintf(q, scope.SQLFragment())
args := pgx.StrictNamedArgs{"profile_id": profileID}
maps.Copy(args, scope.SQLArguments())
_, err := conn.Exec(ctx, q, args)
if err != nil {
if pgErr, ok := errors.AsType[*pgconn.PgError](err); ok {
if pgErr.Code == "23503" {
return ErrResourceInUse
}
}
return fmt.Errorf("cannot delete profile: %w", err)
}
return nil
}