Add employee page

Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
This commit is contained in:
Sacha Al Himdani
2025-11-18 22:56:22 +01:00
parent 2f4cfd7ac0
commit 975d16c623
44 changed files with 5731 additions and 1842 deletions

View File

@@ -124,6 +124,60 @@ LIMIT 1;
return nil
}
func (p *Document) LoadByIDWithFilter(
ctx context.Context,
conn pg.Conn,
scope Scoper,
documentID gid.GID,
filter *DocumentFilter,
) error {
q := `
SELECT
id,
organization_id,
owner_id,
title,
document_type,
classification,
current_published_version,
trust_center_visibility,
created_at,
updated_at
FROM
documents
WHERE
%s
AND deleted_at IS NULL
AND id = @document_id
AND %s
LIMIT 1;
`
q = fmt.Sprintf(q, scope.SQLFragment(), filter.SQLFragment())
args := pgx.StrictNamedArgs{"document_id": documentID}
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 documents: %w", err)
}
document, err := pgx.CollectExactlyOneRow(rows, pgx.RowToStructByName[Document])
if err != nil {
if errors.Is(err, pgx.ErrNoRows) {
return &ErrDocumentNotFound{Identifier: documentID.String()}
}
return fmt.Errorf("cannot collect document: %w", err)
}
*p = document
return nil
}
func (p *Documents) CountByOrganizationID(
ctx context.Context,
conn pg.Conn,
@@ -400,28 +454,17 @@ func (p *Documents) CountByControlID(
filter *DocumentFilter,
) (int, error) {
q := `
WITH plcs AS (
SELECT
p.id,
p.tenant_id,
p.search_vector,
p.trust_center_visibility,
p.deleted_at
FROM
documents p
INNER JOIN
controls_documents cp ON p.id = cp.document_id
WHERE
cp.control_id = @control_id
WITH scoped_documents AS (
SELECT *
FROM documents
WHERE %s
AND deleted_at IS NULL
AND %s
)
SELECT
COUNT(id)
FROM
plcs
WHERE
%s
AND deleted_at IS NULL
AND %s
SELECT COUNT(scoped_documents.id)
FROM scoped_documents
INNER JOIN controls_documents cp ON scoped_documents.id = cp.document_id
WHERE cp.control_id = @control_id
`
q = fmt.Sprintf(q, scope.SQLFragment(), filter.SQLFragment())
@@ -448,46 +491,28 @@ func (p *Documents) LoadByControlID(
filter *DocumentFilter,
) error {
q := `
WITH plcs AS (
SELECT
p.id,
p.tenant_id,
p.search_vector,
p.organization_id,
p.owner_id,
p.title,
p.document_type,
p.classification,
p.current_published_version,
p.trust_center_visibility,
p.created_at,
p.updated_at,
p.deleted_at
FROM
documents p
INNER JOIN
controls_documents cp ON p.id = cp.document_id
WHERE
cp.control_id = @control_id
WITH scoped_documents AS (
SELECT *
FROM documents
WHERE %s
AND deleted_at IS NULL
AND %s
AND %s
)
SELECT
id,
organization_id,
owner_id,
title,
document_type,
classification,
current_published_version,
trust_center_visibility,
created_at,
updated_at
FROM
plcs
WHERE
%s
AND deleted_at IS NULL
AND %s
AND %s
scoped_documents.id,
scoped_documents.organization_id,
scoped_documents.owner_id,
scoped_documents.title,
scoped_documents.document_type,
scoped_documents.classification,
scoped_documents.current_published_version,
scoped_documents.trust_center_visibility,
scoped_documents.created_at,
scoped_documents.updated_at
FROM scoped_documents
INNER JOIN controls_documents cp ON scoped_documents.id = cp.document_id
WHERE cp.control_id = @control_id
`
q = fmt.Sprintf(q, scope.SQLFragment(), filter.SQLFragment(), cursor.SQLFragment())
@@ -519,28 +544,17 @@ func (p *Documents) CountByRiskID(
filter *DocumentFilter,
) (int, error) {
q := `
WITH plcs AS (
SELECT
p.id,
p.tenant_id,
p.search_vector,
p.trust_center_visibility,
p.deleted_at
FROM
documents p
INNER JOIN
risks_documents rp ON p.id = rp.document_id
WHERE
rp.risk_id = @risk_id
WITH scoped_documents AS (
SELECT *
FROM documents
WHERE %s
AND deleted_at IS NULL
AND %s
)
SELECT
COUNT(id)
FROM
plcs
WHERE
%s
AND deleted_at IS NULL
AND %s
SELECT COUNT(scoped_documents.id)
FROM scoped_documents
INNER JOIN risks_documents rp ON scoped_documents.id = rp.document_id
WHERE rp.risk_id = @risk_id
`
q = fmt.Sprintf(q, scope.SQLFragment(), filter.SQLFragment())
@@ -567,46 +581,28 @@ func (p *Documents) LoadByRiskID(
filter *DocumentFilter,
) error {
q := `
WITH plcs AS (
SELECT
p.id,
p.tenant_id,
p.organization_id,
p.owner_id,
p.title,
p.document_type,
p.classification,
p.current_published_version,
p.trust_center_visibility,
p.created_at,
p.updated_at,
p.search_vector,
p.deleted_at
FROM
documents p
INNER JOIN
risks_documents rp ON p.id = rp.document_id
WHERE
rp.risk_id = @risk_id
WITH scoped_documents AS (
SELECT *
FROM documents
WHERE %s
AND deleted_at IS NULL
AND %s
AND %s
)
SELECT
id,
organization_id,
owner_id,
title,
document_type,
classification,
current_published_version,
trust_center_visibility,
created_at,
updated_at
FROM
plcs
WHERE
%s
AND deleted_at IS NULL
AND %s
AND %s
scoped_documents.id,
scoped_documents.organization_id,
scoped_documents.owner_id,
scoped_documents.title,
scoped_documents.document_type,
scoped_documents.classification,
scoped_documents.current_published_version,
scoped_documents.trust_center_visibility,
scoped_documents.created_at,
scoped_documents.updated_at
FROM scoped_documents
INNER JOIN risks_documents rp ON scoped_documents.id = rp.document_id
WHERE rp.risk_id = @risk_id
`
q = fmt.Sprintf(q, scope.SQLFragment(), filter.SQLFragment(), cursor.SQLFragment())
@@ -653,3 +649,61 @@ UPDATE documents SET deleted_at = @deleted_at WHERE %s AND id = ANY(@document_id
_, err := conn.Exec(ctx, q, args)
return err
}
func (p *Document) IsLastSignableVersionSignedByUserEmail(
ctx context.Context,
conn pg.Conn,
scope Scoper,
documentID gid.GID,
userEmail string,
) (bool, error) {
q := `
WITH last_signable_version AS (
SELECT
d.id AS document_id,
d.tenant_id,
dv.version_number,
dvs.state
FROM documents d
INNER JOIN document_versions dv ON dv.document_id = d.id
INNER JOIN document_version_signatures dvs ON dvs.document_version_id = dv.id
INNER JOIN peoples p ON dvs.signed_by = p.id
WHERE d.id = @document_id
AND p.primary_email_address = @user_email
AND dv.version_number = (
SELECT MAX(dv2.version_number)
FROM document_versions dv2
INNER JOIN document_version_signatures dvs2 ON dvs2.document_version_id = dv2.id
INNER JOIN peoples p2 ON dvs2.signed_by = p2.id
WHERE dv2.document_id = d.id
AND p2.primary_email_address = @user_email
)
)
SELECT EXISTS (
SELECT 1
FROM last_signable_version
WHERE %s
AND state = 'SIGNED'
) AS signed
`
q = fmt.Sprintf(q, scope.SQLFragment())
args := pgx.StrictNamedArgs{
"document_id": documentID,
"user_email": userEmail,
}
maps.Copy(args, scope.SQLArguments())
rows, err := conn.Query(ctx, q, args)
if err != nil {
return false, fmt.Errorf("cannot query document signed status: %w", err)
}
signed, err := pgx.CollectOneRow(rows, pgx.RowTo[bool])
if err != nil {
return false, fmt.Errorf("cannot collect signed status: %w", err)
}
return signed, nil
}

View File

@@ -22,6 +22,8 @@ type (
DocumentFilter struct {
query *string
trustCenterVisibilities []TrustCenterVisibility
published *bool
userEmail *string
}
)
@@ -40,7 +42,17 @@ func NewDocumentTrustCenterFilter() *DocumentFilter {
}
}
func (f *DocumentFilter) SQLArguments() pgx.StrictNamedArgs {
func (f *DocumentFilter) WithPublished(published *bool) *DocumentFilter {
f.published = published
return f
}
func (f *DocumentFilter) WithUserEmail(userEmail *string) *DocumentFilter {
f.userEmail = userEmail
return f
}
func (f *DocumentFilter) SQLArguments() pgx.NamedArgs {
var visibilities []string
if f.trustCenterVisibilities != nil {
visibilities = make([]string, len(f.trustCenterVisibilities))
@@ -48,9 +60,11 @@ func (f *DocumentFilter) SQLArguments() pgx.StrictNamedArgs {
visibilities[i] = v.String()
}
}
return pgx.StrictNamedArgs{
return pgx.NamedArgs{
"query": f.query,
"trust_center_visibilities": visibilities,
"published": f.published,
"user_email": f.userEmail,
}
}
@@ -71,5 +85,25 @@ func (f *DocumentFilter) SQLFragment() string {
trust_center_visibility = ANY(@trust_center_visibilities::trust_center_visibility[])
ELSE TRUE
END
AND
CASE
WHEN @published::boolean IS NULL THEN TRUE
WHEN @published::boolean IS TRUE THEN current_published_version IS NOT NULL
WHEN @published::boolean IS FALSE THEN current_published_version IS NULL
END
AND
CASE
WHEN @user_email::text IS NULL THEN TRUE
ELSE EXISTS (
SELECT 1
FROM document_versions dv
INNER JOIN document_version_signatures dvs ON dv.id = dvs.document_version_id
INNER JOIN peoples p ON dvs.signed_by = p.id
WHERE dv.document_id = documents.id
AND dv.status = 'PUBLISHED'
AND p.primary_email_address = @user_email::text
AND dvs.state IN ('REQUESTED', 'SIGNED')
)
END
)`
}

View File

@@ -78,6 +78,7 @@ func (p *DocumentVersions) LoadByDocumentID(
scope Scoper,
documentID gid.GID,
cursor *page.Cursor[DocumentVersionOrderField],
filter *DocumentVersionFilter,
) error {
q := `
SELECT
@@ -100,14 +101,16 @@ WHERE
%s
AND document_id = @document_id
AND %s
AND %s
`
q = fmt.Sprintf(q, scope.SQLFragment(), cursor.SQLFragment())
q = fmt.Sprintf(q, scope.SQLFragment(), filter.SQLFragment(), cursor.SQLFragment())
args := pgx.StrictNamedArgs{
"document_id": documentID,
}
maps.Copy(args, scope.SQLArguments())
maps.Copy(args, cursor.SQLArguments())
maps.Copy(args, filter.SQLArguments())
rows, err := conn.Query(ctx, q, args)
if err != nil {

View File

@@ -0,0 +1,55 @@
// Copyright (c) 2025 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 (
"github.com/jackc/pgx/v5"
)
type (
DocumentVersionFilter struct {
userEmail *string
}
)
func NewDocumentVersionFilter() *DocumentVersionFilter {
return &DocumentVersionFilter{}
}
func (f *DocumentVersionFilter) WithUserEmail(userEmail *string) *DocumentVersionFilter {
f.userEmail = userEmail
return f
}
func (f *DocumentVersionFilter) SQLArguments() pgx.StrictNamedArgs {
return pgx.StrictNamedArgs{
"user_email": f.userEmail,
}
}
func (f *DocumentVersionFilter) SQLFragment() string {
return `
(
@user_email::text IS NULL
OR EXISTS (
SELECT 1
FROM document_version_signatures dvs
INNER JOIN peoples p ON dvs.signed_by = p.id
WHERE dvs.document_version_id = document_versions.id
AND p.primary_email_address = @user_email::text
AND dvs.state IN ('REQUESTED', 'SIGNED')
)
)`
}

View File

@@ -20,7 +20,6 @@ type (
const (
DocumentVersionOrderFieldCreatedAt DocumentVersionOrderField = "CREATED_AT"
DocumentVersionOrderFieldVersion DocumentVersionOrderField = "VERSION"
)
func (p DocumentVersionOrderField) Column() string {

View File

@@ -57,6 +57,8 @@ type (
ErrDocumentVersionSignatureAlreadyExists struct {
message string
}
ErrDocumentVersionSignatureAlreadySigned struct{}
)
func (e ErrDocumentVersionSignatureNotFound) Error() string {
@@ -67,6 +69,10 @@ func (e ErrDocumentVersionSignatureAlreadyExists) Error() string {
return e.message
}
func (e ErrDocumentVersionSignatureAlreadySigned) Error() string {
return "document version already signed"
}
func (pvs DocumentVersionSignature) CursorKey(orderBy DocumentVersionSignatureOrderField) page.CursorKey {
switch orderBy {
case DocumentVersionSignatureOrderFieldCreatedAt:
@@ -412,3 +418,41 @@ WHERE
return nil
}
func (pvs *DocumentVersionSignature) IsSignedByUserEmail(
ctx context.Context,
conn pg.Conn,
scope Scoper,
documentVersionID gid.GID,
userEmail string,
) (bool, error) {
q := `
SELECT EXISTS (
SELECT 1
FROM document_version_signatures dvs
INNER JOIN peoples p ON dvs.signed_by = p.id
WHERE dvs.document_version_id = @document_version_id
AND p.primary_email_address = @user_email
AND dvs.state = 'SIGNED'
AND dvs.tenant_id = @tenant_id
) AS signed
`
args := pgx.StrictNamedArgs{
"document_version_id": documentVersionID,
"user_email": userEmail,
}
maps.Copy(args, scope.SQLArguments())
rows, err := conn.Query(ctx, q, args)
if err != nil {
return false, fmt.Errorf("cannot query document version signature: %w", err)
}
signed, err := pgx.CollectOneRow(rows, pgx.RowTo[bool])
if err != nil {
return false, fmt.Errorf("cannot collect signed status: %w", err)
}
return signed, nil
}

View File

@@ -22,9 +22,10 @@ import (
type MembershipRole string
const (
MembershipRoleOwner MembershipRole = "OWNER"
MembershipRoleAdmin MembershipRole = "ADMIN"
MembershipRoleViewer MembershipRole = "VIEWER"
MembershipRoleOwner MembershipRole = "OWNER"
MembershipRoleAdmin MembershipRole = "ADMIN"
MembershipRoleEmployee MembershipRole = "EMPLOYEE"
MembershipRoleViewer MembershipRole = "VIEWER"
)
func (r MembershipRole) String() string {
@@ -47,6 +48,8 @@ func (r *MembershipRole) Scan(value any) error {
*r = MembershipRoleOwner
case "ADMIN":
*r = MembershipRoleAdmin
case "EMPLOYEE":
*r = MembershipRoleEmployee
case "VIEWER":
*r = MembershipRoleViewer
default:

View File

@@ -0,0 +1 @@
ALTER TYPE authz_role RENAME VALUE 'MEMBER' TO 'EMPLOYEE';

View File

@@ -132,24 +132,24 @@ func (p *People) LoadByEmail(
primaryEmailAddress string,
) error {
q := `
SELECT
id,
organization_id,
kind,
full_name,
primary_email_address,
additional_email_addresses,
position,
contract_start_date,
contract_end_date,
created_at,
updated_at
FROM
peoples
WHERE
%s
AND primary_email_address = @primary_email_address
LIMIT 1;
SELECT
id,
organization_id,
kind,
full_name,
primary_email_address,
additional_email_addresses,
position,
contract_start_date,
contract_end_date,
created_at,
updated_at
FROM
peoples
WHERE
%s
AND primary_email_address = @primary_email_address
LIMIT 1;
`
q = fmt.Sprintf(q, scope.SQLFragment())
@@ -176,6 +176,62 @@ func (p *People) LoadByEmail(
return nil
}
func (p *People) LoadByEmailAndOrganizationID(
ctx context.Context,
conn pg.Conn,
scope Scoper,
primaryEmailAddress string,
organizationID gid.GID,
) error {
q := `
SELECT
id,
organization_id,
kind,
full_name,
primary_email_address,
additional_email_addresses,
position,
contract_start_date,
contract_end_date,
created_at,
updated_at
FROM
peoples
WHERE
%s
AND primary_email_address = @primary_email_address
AND organization_id = @organization_id
LIMIT 1;
`
q = fmt.Sprintf(q, scope.SQLFragment())
args := pgx.StrictNamedArgs{
"primary_email_address": primaryEmailAddress,
"organization_id": organizationID,
}
maps.Copy(args, scope.SQLArguments())
rows, err := conn.Query(ctx, q, args)
if err != nil {
return fmt.Errorf("cannot query people: %w", err)
}
people, err := pgx.CollectExactlyOneRow(rows, pgx.RowToStructByName[People])
if err != nil {
if errors.Is(err, pgx.ErrNoRows) {
return &ErrPeopleNotFound{Identifier: primaryEmailAddress}
}
return fmt.Errorf("cannot collect people: %w", err)
}
*p = people
return nil
}
func (p *Peoples) LoadByIDs(
ctx context.Context,
conn pg.Conn,