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>
1464 lines
34 KiB
Go
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
|
|
}
|