Move coredata outside probo service
Signed-off-by: gearnode <bryan@frimin.fr>
This commit is contained in:
268
pkg/coredata/control.go
Normal file
268
pkg/coredata/control.go
Normal file
@@ -0,0 +1,268 @@
|
||||
// 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 (
|
||||
"context"
|
||||
"fmt"
|
||||
"maps"
|
||||
"time"
|
||||
|
||||
"github.com/getprobo/probo/pkg/gid"
|
||||
"github.com/getprobo/probo/pkg/page"
|
||||
"github.com/jackc/pgx/v5"
|
||||
|
||||
"go.gearno.de/kit/pg"
|
||||
)
|
||||
|
||||
type (
|
||||
Control struct {
|
||||
ID gid.GID `db:"id"`
|
||||
FrameworkID gid.GID `db:"framework_id"`
|
||||
Category string `db:"category"`
|
||||
Name string `db:"name"`
|
||||
Description string `db:"description"`
|
||||
State ControlState `db:"state"`
|
||||
ContentRef string `db:"content_ref"`
|
||||
CreatedAt time.Time `db:"created_at"`
|
||||
UpdatedAt time.Time `db:"updated_at"`
|
||||
Version int `db:"version"`
|
||||
}
|
||||
|
||||
Controls []*Control
|
||||
|
||||
UpdateControlParams struct {
|
||||
ExpectedVersion int
|
||||
Name *string
|
||||
Description *string
|
||||
Category *string
|
||||
State *ControlState
|
||||
}
|
||||
)
|
||||
|
||||
func (c Control) CursorKey() page.CursorKey {
|
||||
return page.NewCursorKey(c.ID, c.CreatedAt)
|
||||
}
|
||||
|
||||
func (c *Control) LoadByID(
|
||||
ctx context.Context,
|
||||
conn pg.Conn,
|
||||
scope Scoper,
|
||||
controlID gid.GID,
|
||||
) error {
|
||||
q := `
|
||||
SELECT
|
||||
id,
|
||||
framework_id,
|
||||
category,
|
||||
name,
|
||||
description,
|
||||
state,
|
||||
content_ref,
|
||||
created_at,
|
||||
updated_at,
|
||||
version
|
||||
FROM
|
||||
controls
|
||||
WHERE
|
||||
%s
|
||||
AND id = @control_id
|
||||
LIMIT 1;
|
||||
`
|
||||
|
||||
q = fmt.Sprintf(q, scope.SQLFragment())
|
||||
|
||||
args := pgx.StrictNamedArgs{"control_id": controlID}
|
||||
maps.Copy(args, scope.SQLArguments())
|
||||
|
||||
rows, err := conn.Query(ctx, q, args)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot query controls: %w", err)
|
||||
}
|
||||
|
||||
control, err := pgx.CollectExactlyOneRow(rows, pgx.RowToStructByName[Control])
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot collect controls: %w", err)
|
||||
}
|
||||
|
||||
*c = control
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c Control) Insert(
|
||||
ctx context.Context,
|
||||
conn pg.Conn,
|
||||
scope Scoper,
|
||||
) error {
|
||||
q := `
|
||||
INSERT INTO
|
||||
controls (
|
||||
tenant_id,
|
||||
id,
|
||||
framework_id,
|
||||
category,
|
||||
name,
|
||||
state,
|
||||
description,
|
||||
content_ref,
|
||||
created_at,
|
||||
updated_at,
|
||||
version
|
||||
)
|
||||
VALUES (
|
||||
@tenant_id,
|
||||
@control_id,
|
||||
@framework_id,
|
||||
@category,
|
||||
@name,
|
||||
@state,
|
||||
@description,
|
||||
@content_ref,
|
||||
@created_at,
|
||||
@updated_at,
|
||||
@version
|
||||
);
|
||||
`
|
||||
|
||||
args := pgx.StrictNamedArgs{
|
||||
"tenant_id": scope.GetTenantID(),
|
||||
"control_id": c.ID,
|
||||
"framework_id": c.FrameworkID,
|
||||
"category": c.Category,
|
||||
"name": c.Name,
|
||||
"version": 0,
|
||||
"description": c.Description,
|
||||
"content_ref": c.ContentRef,
|
||||
"created_at": c.CreatedAt,
|
||||
"updated_at": c.UpdatedAt,
|
||||
"state": c.State,
|
||||
}
|
||||
_, err := conn.Exec(ctx, q, args)
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *Controls) LoadByFrameworkID(
|
||||
ctx context.Context,
|
||||
conn pg.Conn,
|
||||
scope Scoper,
|
||||
frameworkID gid.GID,
|
||||
cursor *page.Cursor,
|
||||
) error {
|
||||
q := `
|
||||
SELECT
|
||||
id,
|
||||
framework_id,
|
||||
category,
|
||||
name,
|
||||
description,
|
||||
state,
|
||||
content_ref,
|
||||
created_at,
|
||||
updated_at,
|
||||
version
|
||||
FROM
|
||||
controls
|
||||
WHERE
|
||||
%s
|
||||
AND framework_id = @framework_id
|
||||
AND %s
|
||||
`
|
||||
q = fmt.Sprintf(q, scope.SQLFragment(), cursor.SQLFragment())
|
||||
|
||||
args := pgx.StrictNamedArgs{"framework_id": frameworkID}
|
||||
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 controls: %w", err)
|
||||
}
|
||||
|
||||
controls, err := pgx.CollectRows(rows, pgx.RowToAddrOfStructByName[Control])
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot collect controls: %w", err)
|
||||
}
|
||||
|
||||
*c = controls
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Control) Update(
|
||||
ctx context.Context,
|
||||
conn pg.Conn,
|
||||
scope Scoper,
|
||||
params UpdateControlParams,
|
||||
) error {
|
||||
q := `
|
||||
UPDATE controls SET
|
||||
name = COALESCE(@name, name),
|
||||
description = COALESCE(@description, description),
|
||||
category = COALESCE(@category, category),
|
||||
state = COALESCE(@state, state),
|
||||
updated_at = @updated_at,
|
||||
version = version + 1
|
||||
WHERE %s
|
||||
AND id = @control_id
|
||||
AND version = @expected_version
|
||||
RETURNING
|
||||
id,
|
||||
framework_id,
|
||||
category,
|
||||
name,
|
||||
description,
|
||||
state,
|
||||
content_ref,
|
||||
created_at,
|
||||
updated_at,
|
||||
version
|
||||
`
|
||||
q = fmt.Sprintf(q, scope.SQLFragment())
|
||||
|
||||
args := pgx.NamedArgs{
|
||||
"control_id": c.ID,
|
||||
"expected_version": params.ExpectedVersion,
|
||||
"updated_at": time.Now(),
|
||||
}
|
||||
|
||||
maps.Copy(args, scope.SQLArguments())
|
||||
|
||||
if params.Name != nil {
|
||||
args["name"] = *params.Name
|
||||
}
|
||||
if params.Description != nil {
|
||||
args["description"] = *params.Description
|
||||
}
|
||||
if params.Category != nil {
|
||||
args["category"] = *params.Category
|
||||
}
|
||||
|
||||
maps.Copy(args, scope.SQLArguments())
|
||||
|
||||
rows, err := conn.Query(ctx, q, args)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot query controls: %w", err)
|
||||
}
|
||||
|
||||
control, err := pgx.CollectExactlyOneRow(rows, pgx.RowToStructByName[Control])
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot collect controls: %w", err)
|
||||
}
|
||||
|
||||
*c = control
|
||||
|
||||
return nil
|
||||
}
|
||||
84
pkg/coredata/control_state.go
Normal file
84
pkg/coredata/control_state.go
Normal file
@@ -0,0 +1,84 @@
|
||||
// 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 (
|
||||
"database/sql/driver"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
type (
|
||||
ControlState uint8
|
||||
)
|
||||
|
||||
const (
|
||||
ControlStateNotStarted ControlState = iota
|
||||
ControlStateInProgress
|
||||
ControlStateNotApplicable
|
||||
ControlStateImplemented
|
||||
)
|
||||
|
||||
func (cs ControlState) MarshalText() ([]byte, error) {
|
||||
return []byte(cs.String()), nil
|
||||
}
|
||||
|
||||
func (cs *ControlState) UnmarshalText(data []byte) error {
|
||||
val := string(data)
|
||||
|
||||
switch val {
|
||||
case ControlStateNotStarted.String():
|
||||
*cs = ControlStateNotStarted
|
||||
case ControlStateInProgress.String():
|
||||
*cs = ControlStateInProgress
|
||||
case ControlStateNotApplicable.String():
|
||||
*cs = ControlStateNotApplicable
|
||||
case ControlStateImplemented.String():
|
||||
*cs = ControlStateImplemented
|
||||
default:
|
||||
return fmt.Errorf("invalid ControlState value: %q", val)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cs ControlState) String() string {
|
||||
var val string
|
||||
|
||||
switch cs {
|
||||
case ControlStateNotStarted:
|
||||
val = "NOT_STARTED"
|
||||
case ControlStateInProgress:
|
||||
val = "IN_PROGRESS"
|
||||
case ControlStateNotApplicable:
|
||||
val = "NOT_APPLICABLE"
|
||||
case ControlStateImplemented:
|
||||
val = "IMPLEMENTED"
|
||||
}
|
||||
|
||||
return val
|
||||
}
|
||||
|
||||
func (cs *ControlState) Scan(value any) error {
|
||||
val, ok := value.(string)
|
||||
if !ok {
|
||||
return fmt.Errorf("invalid scan source for ControlState, expected string got %T", value)
|
||||
}
|
||||
|
||||
return cs.UnmarshalText([]byte(val))
|
||||
}
|
||||
|
||||
func (cs ControlState) Value() (driver.Value, error) {
|
||||
return cs.String(), nil
|
||||
}
|
||||
31
pkg/coredata/entity_type_reg.go
Normal file
31
pkg/coredata/entity_type_reg.go
Normal file
@@ -0,0 +1,31 @@
|
||||
// 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
|
||||
|
||||
const (
|
||||
OrganizationEntityType uint16 = iota
|
||||
FrameworkEntityType
|
||||
ControlEntityType
|
||||
TaskEntityType
|
||||
EvidenceEntityType
|
||||
ControlStateTransitionEntityType
|
||||
TaskStateTransitionEntityType
|
||||
VendorEntityType
|
||||
PeopleEntityType
|
||||
EvidenceStateTransitionEntityType
|
||||
PolicyEntityType
|
||||
UserEntityType
|
||||
SessionEntityType
|
||||
)
|
||||
224
pkg/coredata/evidence.go
Normal file
224
pkg/coredata/evidence.go
Normal file
@@ -0,0 +1,224 @@
|
||||
// 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 (
|
||||
"context"
|
||||
"fmt"
|
||||
"maps"
|
||||
"time"
|
||||
|
||||
"github.com/getprobo/probo/pkg/gid"
|
||||
"github.com/getprobo/probo/pkg/page"
|
||||
"github.com/jackc/pgx/v5"
|
||||
"go.gearno.de/kit/pg"
|
||||
)
|
||||
|
||||
type (
|
||||
Evidence struct {
|
||||
ID gid.GID `db:"id"`
|
||||
TaskID gid.GID `db:"task_id"`
|
||||
State EvidenceState `db:"state"`
|
||||
ObjectKey string `db:"object_key"`
|
||||
MimeType string `db:"mime_type"`
|
||||
Size uint64 `db:"size"`
|
||||
Filename string `db:"filename"`
|
||||
CreatedAt time.Time `db:"created_at"`
|
||||
UpdatedAt time.Time `db:"updated_at"`
|
||||
}
|
||||
|
||||
Evidences []*Evidence
|
||||
)
|
||||
|
||||
func (e Evidence) CursorKey() page.CursorKey {
|
||||
return page.NewCursorKey(e.ID, e.CreatedAt)
|
||||
}
|
||||
|
||||
func (e Evidence) Insert(
|
||||
ctx context.Context,
|
||||
conn pg.Conn,
|
||||
scope Scoper,
|
||||
) error {
|
||||
q := `
|
||||
INSERT INTO
|
||||
evidences (
|
||||
tenant_id,
|
||||
id,
|
||||
task_id,
|
||||
object_key,
|
||||
mime_type,
|
||||
size,
|
||||
filename,
|
||||
created_at,
|
||||
updated_at
|
||||
)
|
||||
VALUES (
|
||||
@tenant_id,
|
||||
@evidence_id,
|
||||
@task_id,
|
||||
@object_key,
|
||||
@mime_type,
|
||||
@size,
|
||||
@filename,
|
||||
@created_at,
|
||||
@updated_at
|
||||
)
|
||||
`
|
||||
|
||||
args := pgx.StrictNamedArgs{
|
||||
"tenant_id": scope.GetTenantID(),
|
||||
"evidence_id": e.ID,
|
||||
"task_id": e.TaskID,
|
||||
"object_key": e.ObjectKey,
|
||||
"mime_type": e.MimeType,
|
||||
"size": e.Size,
|
||||
"filename": e.Filename,
|
||||
"created_at": e.CreatedAt,
|
||||
"updated_at": e.UpdatedAt,
|
||||
}
|
||||
_, err := conn.Exec(ctx, q, args)
|
||||
return err
|
||||
}
|
||||
|
||||
func (e *Evidence) LoadByID(
|
||||
ctx context.Context,
|
||||
conn pg.Conn,
|
||||
scope Scoper,
|
||||
evidenceID gid.GID,
|
||||
) error {
|
||||
q := `
|
||||
WITH
|
||||
evidence_states AS (
|
||||
SELECT
|
||||
evidence_id,
|
||||
to_state AS state,
|
||||
reason,
|
||||
RANK() OVER w
|
||||
FROM
|
||||
evidence_state_transitions
|
||||
WHERE
|
||||
evidence_id = @evidence_id
|
||||
WINDOW
|
||||
w AS (PARTITION BY evidence_id ORDER BY created_at DESC)
|
||||
)
|
||||
SELECT
|
||||
id,
|
||||
task_id,
|
||||
es.state,
|
||||
object_key,
|
||||
mime_type,
|
||||
size,
|
||||
filename,
|
||||
created_at,
|
||||
updated_at
|
||||
FROM
|
||||
evidences
|
||||
INNER JOIN
|
||||
evidence_states es ON es.evidence_id = evidences.id
|
||||
WHERE
|
||||
%s
|
||||
AND id = @evidence_id
|
||||
AND es.rank = 1
|
||||
LIMIT 1;
|
||||
`
|
||||
|
||||
q = fmt.Sprintf(q, scope.SQLFragment())
|
||||
|
||||
args := pgx.StrictNamedArgs{"evidence_id": evidenceID}
|
||||
maps.Copy(args, scope.SQLArguments())
|
||||
|
||||
rows, err := conn.Query(ctx, q, args)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot query evidence: %w", err)
|
||||
}
|
||||
|
||||
evidence, err := pgx.CollectExactlyOneRow(rows, pgx.RowToStructByName[Evidence])
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot collect evidence: %w", err)
|
||||
}
|
||||
|
||||
*e = evidence
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *Evidences) LoadByTaskID(
|
||||
ctx context.Context,
|
||||
conn pg.Conn,
|
||||
scope Scoper,
|
||||
taskID gid.GID,
|
||||
cursor *page.Cursor,
|
||||
) error {
|
||||
q := `
|
||||
SELECT
|
||||
id,
|
||||
task_id,
|
||||
state,
|
||||
object_key,
|
||||
mime_type,
|
||||
size,
|
||||
filename,
|
||||
created_at,
|
||||
updated_at
|
||||
FROM
|
||||
evidences
|
||||
WHERE
|
||||
%s
|
||||
AND task_id = @task_id
|
||||
AND %s
|
||||
`
|
||||
|
||||
q = fmt.Sprintf(q, scope.SQLFragment(), cursor.SQLFragment())
|
||||
|
||||
args := pgx.StrictNamedArgs{"task_id": taskID}
|
||||
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 evidence: %w", err)
|
||||
}
|
||||
|
||||
evidences, err := pgx.CollectRows(rows, pgx.RowToAddrOfStructByName[Evidence])
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot collect evidence: %w", err)
|
||||
}
|
||||
|
||||
*e = evidences
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e Evidence) Delete(
|
||||
ctx context.Context,
|
||||
conn pg.Conn,
|
||||
scope Scoper,
|
||||
) error {
|
||||
q := `
|
||||
DELETE FROM
|
||||
evidences
|
||||
WHERE
|
||||
%s
|
||||
AND id = @evidence_id
|
||||
`
|
||||
|
||||
q = fmt.Sprintf(q, scope.SQLFragment())
|
||||
|
||||
args := pgx.StrictNamedArgs{"evidence_id": e.ID}
|
||||
maps.Copy(args, scope.SQLArguments())
|
||||
|
||||
_, err := conn.Exec(ctx, q, args)
|
||||
return err
|
||||
}
|
||||
79
pkg/coredata/evidence_state.go
Normal file
79
pkg/coredata/evidence_state.go
Normal file
@@ -0,0 +1,79 @@
|
||||
// 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 (
|
||||
"database/sql/driver"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
type (
|
||||
EvidenceState uint8
|
||||
)
|
||||
|
||||
const (
|
||||
EvidenceStateValid EvidenceState = iota
|
||||
EvidenceStateInvalid
|
||||
EvidenceStateExpired
|
||||
)
|
||||
|
||||
func (es EvidenceState) MarshalText() ([]byte, error) {
|
||||
return []byte(es.String()), nil
|
||||
}
|
||||
|
||||
func (es *EvidenceState) UnmarshalText(data []byte) error {
|
||||
val := string(data)
|
||||
|
||||
switch val {
|
||||
case EvidenceStateValid.String():
|
||||
*es = EvidenceStateValid
|
||||
case EvidenceStateInvalid.String():
|
||||
*es = EvidenceStateInvalid
|
||||
case EvidenceStateExpired.String():
|
||||
*es = EvidenceStateExpired
|
||||
default:
|
||||
return fmt.Errorf("invalid EvidenceState value: %q", val)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (es EvidenceState) String() string {
|
||||
var val string
|
||||
|
||||
switch es {
|
||||
case EvidenceStateValid:
|
||||
val = "VALID"
|
||||
case EvidenceStateInvalid:
|
||||
val = "INVALID"
|
||||
case EvidenceStateExpired:
|
||||
val = "EXPIRED"
|
||||
}
|
||||
|
||||
return val
|
||||
}
|
||||
|
||||
func (es *EvidenceState) Scan(value any) error {
|
||||
val, ok := value.(string)
|
||||
if !ok {
|
||||
return fmt.Errorf("invalid scan source for EvidenceState, expected string got %T", value)
|
||||
}
|
||||
|
||||
return es.UnmarshalText([]byte(val))
|
||||
}
|
||||
|
||||
func (es EvidenceState) Value() (driver.Value, error) {
|
||||
return es.String(), nil
|
||||
}
|
||||
266
pkg/coredata/framework.go
Normal file
266
pkg/coredata/framework.go
Normal file
@@ -0,0 +1,266 @@
|
||||
// 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 (
|
||||
"context"
|
||||
"fmt"
|
||||
"maps"
|
||||
"time"
|
||||
|
||||
"github.com/getprobo/probo/pkg/gid"
|
||||
"github.com/getprobo/probo/pkg/page"
|
||||
"github.com/jackc/pgx/v5"
|
||||
"go.gearno.de/kit/pg"
|
||||
)
|
||||
|
||||
type (
|
||||
Framework struct {
|
||||
ID gid.GID `db:"id"`
|
||||
OrganizationID gid.GID `db:"organization_id"`
|
||||
Name string `db:"name"`
|
||||
Description string `db:"description"`
|
||||
ContentRef string `db:"content_ref"`
|
||||
CreatedAt time.Time `db:"created_at"`
|
||||
UpdatedAt time.Time `db:"updated_at"`
|
||||
Version int `db:"version"`
|
||||
}
|
||||
|
||||
Frameworks []*Framework
|
||||
|
||||
UpdateFrameworkParams struct {
|
||||
ExpectedVersion int
|
||||
Name *string
|
||||
Description *string
|
||||
}
|
||||
)
|
||||
|
||||
func (f Framework) CursorKey() page.CursorKey {
|
||||
return page.NewCursorKey(f.ID, f.CreatedAt)
|
||||
}
|
||||
|
||||
func (f *Frameworks) LoadByOrganizationID(
|
||||
ctx context.Context,
|
||||
conn pg.Conn,
|
||||
scope Scoper,
|
||||
organizationID gid.GID,
|
||||
cursor *page.Cursor,
|
||||
) error {
|
||||
q := `
|
||||
SELECT
|
||||
id,
|
||||
organization_id,
|
||||
name,
|
||||
description,
|
||||
content_ref,
|
||||
created_at,
|
||||
updated_at,
|
||||
version
|
||||
FROM
|
||||
frameworks
|
||||
WHERE
|
||||
%s
|
||||
AND organization_id = @organization_id
|
||||
AND %s
|
||||
`
|
||||
|
||||
q = fmt.Sprintf(q, scope.SQLFragment(), cursor.SQLFragment())
|
||||
|
||||
args := pgx.StrictNamedArgs{"organization_id": organizationID}
|
||||
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 frameworks: %w", err)
|
||||
}
|
||||
|
||||
frameworks, err := pgx.CollectRows(rows, pgx.RowToAddrOfStructByName[Framework])
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot collect frameworks: %w", err)
|
||||
}
|
||||
|
||||
*f = frameworks
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *Framework) LoadByID(
|
||||
ctx context.Context,
|
||||
conn pg.Conn,
|
||||
scope Scoper,
|
||||
frameworkID gid.GID,
|
||||
) error {
|
||||
q := `
|
||||
SELECT
|
||||
id,
|
||||
organization_id,
|
||||
name,
|
||||
description,
|
||||
content_ref,
|
||||
created_at,
|
||||
updated_at,
|
||||
version
|
||||
FROM
|
||||
frameworks
|
||||
WHERE
|
||||
%s
|
||||
AND id = @framework_id
|
||||
LIMIT 1;
|
||||
`
|
||||
|
||||
q = fmt.Sprintf(q, scope.SQLFragment())
|
||||
|
||||
args := pgx.StrictNamedArgs{"framework_id": frameworkID}
|
||||
maps.Copy(args, scope.SQLArguments())
|
||||
rows, err := conn.Query(ctx, q, args)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot query frameworks: %w", err)
|
||||
}
|
||||
|
||||
framework, err := pgx.CollectExactlyOneRow(rows, pgx.RowToStructByName[Framework])
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot collect framework: %w", err)
|
||||
}
|
||||
|
||||
*f = framework
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f Framework) Insert(
|
||||
ctx context.Context,
|
||||
conn pg.Conn,
|
||||
scope Scoper,
|
||||
) error {
|
||||
q := `
|
||||
INSERT INTO
|
||||
frameworks (
|
||||
tenant_id,
|
||||
id,
|
||||
organization_id,
|
||||
name,
|
||||
description,
|
||||
content_ref,
|
||||
created_at,
|
||||
updated_at,
|
||||
version
|
||||
)
|
||||
VALUES (
|
||||
@tenant_id,
|
||||
@framework_id,
|
||||
@organization_id,
|
||||
@name,
|
||||
@description,
|
||||
@content_ref,
|
||||
@created_at,
|
||||
@updated_at,
|
||||
@version
|
||||
);
|
||||
`
|
||||
|
||||
args := pgx.StrictNamedArgs{
|
||||
"tenant_id": scope.GetTenantID(),
|
||||
"framework_id": f.ID,
|
||||
"organization_id": f.OrganizationID,
|
||||
"name": f.Name,
|
||||
"description": f.Description,
|
||||
"content_ref": f.ContentRef,
|
||||
"created_at": f.CreatedAt,
|
||||
"updated_at": f.UpdatedAt,
|
||||
"version": f.Version,
|
||||
}
|
||||
_, err := conn.Exec(ctx, q, args)
|
||||
return err
|
||||
}
|
||||
|
||||
func (f Framework) Delete(
|
||||
ctx context.Context,
|
||||
conn pg.Conn,
|
||||
scope Scoper,
|
||||
) error {
|
||||
q := `
|
||||
DELETE
|
||||
FROM
|
||||
frameworks
|
||||
WHERE
|
||||
%s
|
||||
AND id = @framework_id;
|
||||
`
|
||||
|
||||
args := pgx.StrictNamedArgs{"framework_id": f.ID}
|
||||
maps.Copy(args, scope.SQLArguments())
|
||||
q = fmt.Sprintf(q, scope.SQLFragment())
|
||||
|
||||
_, err := conn.Exec(ctx, q, args)
|
||||
return err
|
||||
}
|
||||
|
||||
func (f *Framework) Update(
|
||||
ctx context.Context,
|
||||
conn pg.Conn,
|
||||
scope Scoper,
|
||||
params UpdateFrameworkParams,
|
||||
) error {
|
||||
q := `
|
||||
UPDATE frameworks SET
|
||||
name = COALESCE(@name, name),
|
||||
description = COALESCE(@description, description),
|
||||
updated_at = @updated_at,
|
||||
version = version + 1
|
||||
WHERE %s
|
||||
AND id = @framework_id
|
||||
AND version = @expected_version
|
||||
RETURNING
|
||||
id,
|
||||
organization_id,
|
||||
name,
|
||||
description,
|
||||
content_ref,
|
||||
created_at,
|
||||
updated_at,
|
||||
version
|
||||
`
|
||||
q = fmt.Sprintf(q, scope.SQLFragment())
|
||||
|
||||
args := pgx.StrictNamedArgs{
|
||||
"framework_id": f.ID,
|
||||
"expected_version": params.ExpectedVersion,
|
||||
"updated_at": time.Now(),
|
||||
}
|
||||
|
||||
if params.Name != nil {
|
||||
args["name"] = *params.Name
|
||||
}
|
||||
if params.Description != nil {
|
||||
args["description"] = *params.Description
|
||||
}
|
||||
|
||||
maps.Copy(args, scope.SQLArguments())
|
||||
|
||||
rows, err := conn.Query(ctx, q, args)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot query frameworks: %w", err)
|
||||
}
|
||||
|
||||
framework, err := pgx.CollectExactlyOneRow(rows, pgx.RowToStructByName[Framework])
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot collect framework: %w", err)
|
||||
}
|
||||
|
||||
*f = framework
|
||||
|
||||
return nil
|
||||
}
|
||||
24
pkg/coredata/migrations.go
Normal file
24
pkg/coredata/migrations.go
Normal file
@@ -0,0 +1,24 @@
|
||||
// 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 (
|
||||
"embed"
|
||||
)
|
||||
|
||||
var (
|
||||
//go:embed migrations/*.sql
|
||||
Migrations embed.FS
|
||||
)
|
||||
37
pkg/coredata/migrations/20250124T113100Z.sql
Normal file
37
pkg/coredata/migrations/20250124T113100Z.sql
Normal file
@@ -0,0 +1,37 @@
|
||||
CREATE TABLE organizations (
|
||||
id UUID PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
description TEXT,
|
||||
created_at TIMESTAMP WITH TIME ZONE NOT NULL,
|
||||
updated_at TIMESTAMP WITH TIME ZONE NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE frameworks (
|
||||
id UUID PRIMARY KEY,
|
||||
organization_id UUID REFERENCES organizations(id) NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
description TEXT NOT NULL,
|
||||
content_ref TEXT NOT NULL,
|
||||
created_at TIMESTAMP WITH TIME ZONE NOT NULL,
|
||||
updated_at TIMESTAMP WITH TIME ZONE NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE controls (
|
||||
id UUID PRIMARY KEY,
|
||||
framework_id UUID REFERENCES frameworks(id) NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
description TEXT NOT NULL,
|
||||
content_ref TEXT NOT NULL,
|
||||
created_at TIMESTAMP WITH TIME ZONE NOT NULL,
|
||||
updated_at TIMESTAMP WITH TIME ZONE NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE tasks (
|
||||
id UUID PRIMARY KEY,
|
||||
control_id UUID REFERENCES controls(id) NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
description TEXT NOT NULL,
|
||||
content_ref TEXT NOT NULL,
|
||||
created_at TIMESTAMP WITH TIME ZONE NOT NULL,
|
||||
updated_at TIMESTAMP WITH TIME ZONE NOT NULL
|
||||
);
|
||||
1
pkg/coredata/migrations/20250125T212400Z.sql
Normal file
1
pkg/coredata/migrations/20250125T212400Z.sql
Normal file
@@ -0,0 +1 @@
|
||||
ALTER TABLE organizations DROP COLUMN description;
|
||||
31
pkg/coredata/migrations/20250125T214100Z.sql
Normal file
31
pkg/coredata/migrations/20250125T214100Z.sql
Normal file
@@ -0,0 +1,31 @@
|
||||
ALTER TABLE frameworks DROP CONSTRAINT IF EXISTS frameworks_organization_id_fkey;
|
||||
ALTER TABLE controls DROP CONSTRAINT IF EXISTS controls_framework_id_fkey;
|
||||
ALTER TABLE tasks DROP CONSTRAINT IF EXISTS tasks_control_id_fkey;
|
||||
|
||||
ALTER TABLE organizations DROP CONSTRAINT organizations_pkey;
|
||||
ALTER TABLE organizations ALTER COLUMN id TYPE TEXT USING id::text;
|
||||
ALTER TABLE organizations ADD PRIMARY KEY (id);
|
||||
|
||||
ALTER TABLE frameworks DROP CONSTRAINT frameworks_pkey;
|
||||
ALTER TABLE frameworks ALTER COLUMN id TYPE TEXT USING id::text;
|
||||
ALTER TABLE frameworks ADD PRIMARY KEY (id);
|
||||
ALTER TABLE frameworks ALTER COLUMN organization_id TYPE TEXT USING organization_id::text;
|
||||
|
||||
ALTER TABLE controls DROP CONSTRAINT controls_pkey;
|
||||
ALTER TABLE controls ALTER COLUMN id TYPE TEXT USING id::text;
|
||||
ALTER TABLE controls ADD PRIMARY KEY (id);
|
||||
ALTER TABLE controls ALTER COLUMN framework_id TYPE TEXT USING framework_id::text;
|
||||
|
||||
ALTER TABLE tasks DROP CONSTRAINT tasks_pkey;
|
||||
ALTER TABLE tasks ALTER COLUMN id TYPE TEXT USING id::text;
|
||||
ALTER TABLE tasks ADD PRIMARY KEY (id);
|
||||
ALTER TABLE tasks ALTER COLUMN control_id TYPE TEXT USING control_id::text;
|
||||
|
||||
ALTER TABLE frameworks ADD CONSTRAINT frameworks_organization_id_fkey
|
||||
FOREIGN KEY (organization_id) REFERENCES organizations(id);
|
||||
|
||||
ALTER TABLE controls ADD CONSTRAINT controls_framework_id_fkey
|
||||
FOREIGN KEY (framework_id) REFERENCES frameworks(id);
|
||||
|
||||
ALTER TABLE tasks ADD CONSTRAINT tasks_control_id_fkey
|
||||
FOREIGN KEY (control_id) REFERENCES controls(id);
|
||||
16
pkg/coredata/migrations/20250126T141600Z.sql
Normal file
16
pkg/coredata/migrations/20250126T141600Z.sql
Normal file
@@ -0,0 +1,16 @@
|
||||
CREATE TYPE control_state AS ENUM (
|
||||
'NOT_STARTED',
|
||||
'IN_PROGRESS',
|
||||
'NOT_APPLICABLE',
|
||||
'IMPLEMENTED'
|
||||
);
|
||||
|
||||
CREATE TABLE control_state_transitions (
|
||||
id TEXT PRIMARY KEY,
|
||||
control_id TEXT REFERENCES controls(id) NOT NULL,
|
||||
from_state control_state,
|
||||
to_state control_state NOT NULL,
|
||||
reason TEXT,
|
||||
created_at TIMESTAMP WITH TIME ZONE NOT NULL,
|
||||
updated_at TIMESTAMP WITH TIME ZONE NOT NULL
|
||||
);
|
||||
12
pkg/coredata/migrations/20250128T093900Z.sql
Normal file
12
pkg/coredata/migrations/20250128T093900Z.sql
Normal file
@@ -0,0 +1,12 @@
|
||||
ALTER TABLE tasks
|
||||
DROP CONSTRAINT tasks_control_id_fkey,
|
||||
DROP COLUMN control_id;
|
||||
|
||||
CREATE TABLE controls_tasks (
|
||||
task_id TEXT NOT NULL,
|
||||
control_id TEXT NOT NULL,
|
||||
created_at TIMESTAMP WITH TIME ZONE NOT NULL,
|
||||
PRIMARY KEY (task_id, control_id),
|
||||
FOREIGN KEY (task_id) REFERENCES tasks(id),
|
||||
FOREIGN KEY (control_id) REFERENCES controls(id)
|
||||
);
|
||||
7
pkg/coredata/migrations/20250128T151500Z.sql
Normal file
7
pkg/coredata/migrations/20250128T151500Z.sql
Normal file
@@ -0,0 +1,7 @@
|
||||
CREATE TABLE evidences (
|
||||
id TEXT PRIMARY KEY,
|
||||
task_id TEXT REFERENCES tasks(id) NOT NULL,
|
||||
object_key TEXT NOT NULL,
|
||||
created_at TIMESTAMP WITH TIME ZONE NOT NULL,
|
||||
updated_at TIMESTAMP WITH TIME ZONE NOT NULL
|
||||
);
|
||||
6
pkg/coredata/migrations/20250130T142900Z.sql
Normal file
6
pkg/coredata/migrations/20250130T142900Z.sql
Normal file
@@ -0,0 +1,6 @@
|
||||
CREATE TABLE vendors (
|
||||
id TEXT PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
created_at TIMESTAMP WITH TIME ZONE,
|
||||
updated_at TIMESTAMP WITH TIME ZONE
|
||||
);
|
||||
9
pkg/coredata/migrations/20250130T144500Z.sql
Normal file
9
pkg/coredata/migrations/20250130T144500Z.sql
Normal file
@@ -0,0 +1,9 @@
|
||||
CREATE TABLE peoples (
|
||||
id TEXT PRIMARY KEY,
|
||||
organization_id TEXT REFERENCES organizations(id) NOT NULL,
|
||||
full_name TEXT NOT NULL,
|
||||
primary_email_address TEXT NOT NULL,
|
||||
additional_email_addresses TEXT[] NOT NULL,
|
||||
created_at TIMESTAMP WITH TIME ZONE,
|
||||
updated_at TIMESTAMP WITH TIME ZONE
|
||||
);
|
||||
6
pkg/coredata/migrations/20250130T145600Z.sql
Normal file
6
pkg/coredata/migrations/20250130T145600Z.sql
Normal file
@@ -0,0 +1,6 @@
|
||||
CREATE TYPE task_state AS ENUM (
|
||||
'TODO',
|
||||
'DONE'
|
||||
);
|
||||
|
||||
ALTER TABLE tasks ADD COLUMN state task_state NOT NULL;
|
||||
11
pkg/coredata/migrations/20250130T154300Z.sql
Normal file
11
pkg/coredata/migrations/20250130T154300Z.sql
Normal file
@@ -0,0 +1,11 @@
|
||||
CREATE TABLE task_state_transitions (
|
||||
id TEXT PRIMARY KEY,
|
||||
task_id TEXT REFERENCES tasks(id) NOT NULL,
|
||||
from_state task_state,
|
||||
to_state task_state NOT NULL,
|
||||
reason TEXT,
|
||||
created_at TIMESTAMP WITH TIME ZONE,
|
||||
updated_at TIMESTAMP WITH TIME ZONE
|
||||
);
|
||||
|
||||
ALTER TABLE tasks DROP COLUMN state;
|
||||
3
pkg/coredata/migrations/20250130T210000Z.sql
Normal file
3
pkg/coredata/migrations/20250130T210000Z.sql
Normal file
@@ -0,0 +1,3 @@
|
||||
ALTER TABLE evidences
|
||||
ADD COLUMN mime_type TEXT NOT NULL,
|
||||
ADD COLUMN size NUMERIC NOT NULL;
|
||||
14
pkg/coredata/migrations/20250130T211400Z.sql
Normal file
14
pkg/coredata/migrations/20250130T211400Z.sql
Normal file
@@ -0,0 +1,14 @@
|
||||
CREATE TYPE evidence_state AS ENUM (
|
||||
'VALID',
|
||||
'INVALID',
|
||||
'EXPIRED'
|
||||
);
|
||||
|
||||
CREATE TABLE evidence_state_transitions (
|
||||
id TEXT PRIMARY KEY,
|
||||
from_state evidence_state,
|
||||
to_state evidence_state NOT NULL,
|
||||
reason TEXT,
|
||||
created_at TIMESTAMP WITH TIME ZONE,
|
||||
updated_at TIMESTAMP WITH TIME ZONE
|
||||
);
|
||||
2
pkg/coredata/migrations/20250130T212000Z.sql
Normal file
2
pkg/coredata/migrations/20250130T212000Z.sql
Normal file
@@ -0,0 +1,2 @@
|
||||
ALTER TABLE evidence_state_transitions ADD COLUMN evidence_id TEXT REFERENCES evidences(id) NOT NULL;
|
||||
ALTER TABLE evidences ALTER COLUMN size TYPE BIGINT USING size::BIGINT;
|
||||
1
pkg/coredata/migrations/20250139T143000Z.sql
Normal file
1
pkg/coredata/migrations/20250139T143000Z.sql
Normal file
@@ -0,0 +1 @@
|
||||
ALTER TABLE vendors ADD COLUMN organization_id TEXT REFERENCES organizations(id) NOT NULL;
|
||||
12
pkg/coredata/migrations/20250202T110500Z.sql
Normal file
12
pkg/coredata/migrations/20250202T110500Z.sql
Normal file
@@ -0,0 +1,12 @@
|
||||
CREATE TABLE usrmgr_users (
|
||||
id TEXT PRIMARY KEY,
|
||||
created_at TIMESTAMP NOT NULL,
|
||||
updated_at TIMESTAMP NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE usrmgr_sessions (
|
||||
id TEXT PRIMARY KEY,
|
||||
user_id TEXT NOT NULL REFERENCES usrmgr_users(id),
|
||||
created_at TIMESTAMP NOT NULL,
|
||||
updated_at TIMESTAMP NOT NULL
|
||||
);
|
||||
5
pkg/coredata/migrations/20250202T141800Z.sql
Normal file
5
pkg/coredata/migrations/20250202T141800Z.sql
Normal file
@@ -0,0 +1,5 @@
|
||||
CREATE EXTENSION citext;
|
||||
|
||||
ALTER TABLE usrmgr_users
|
||||
ADD COLUMN email_address CITEXT NOT NULL,
|
||||
ADD COLUMN hashed_password BYTEA NOT NULL;
|
||||
1
pkg/coredata/migrations/20250202T180400Z.sql
Normal file
1
pkg/coredata/migrations/20250202T180400Z.sql
Normal file
@@ -0,0 +1 @@
|
||||
ALTER TABLE usrmgr_users ADD UNIQUE (email_address);
|
||||
1
pkg/coredata/migrations/20250202T201400Z.sql
Normal file
1
pkg/coredata/migrations/20250202T201400Z.sql
Normal file
@@ -0,0 +1 @@
|
||||
ALTER TABLE usrmgr_sessions ADD COLUMN expired_at TIMESTAMP WITH TIME ZONE NOT NULL;
|
||||
10
pkg/coredata/migrations/20250206T121300Z.sql
Normal file
10
pkg/coredata/migrations/20250206T121300Z.sql
Normal file
@@ -0,0 +1,10 @@
|
||||
CREATE TYPE people_kind AS ENUM (
|
||||
'EMPLOYEE',
|
||||
'CONTRACTOR'
|
||||
);
|
||||
|
||||
ALTER TABLE peoples ADD COLUMN kind people_kind;
|
||||
|
||||
UPDATE peoples SET kind = 'EMPLOYEE';
|
||||
|
||||
ALTER TABLE peoples ALTER COLUMN kind SET NOT NULL;
|
||||
5
pkg/coredata/migrations/20250209T183000Z.sql
Normal file
5
pkg/coredata/migrations/20250209T183000Z.sql
Normal file
@@ -0,0 +1,5 @@
|
||||
ALTER TABLE organizations ADD COLUMN logo_url TEXT;
|
||||
|
||||
UPDATE organizations SET logo_url = 'https://fastly.picsum.photos/id/411/100/100.jpg?grayscale&hmac=lGH1KqiTvm1TFkjJ6kimsgMJIL0S7zVS7EHFi0qOgCk';
|
||||
|
||||
ALTER TABLE organizations ALTER COLUMN logo_url SET NOT NULL;
|
||||
5
pkg/coredata/migrations/20250212T174900Z.sql
Normal file
5
pkg/coredata/migrations/20250212T174900Z.sql
Normal file
@@ -0,0 +1,5 @@
|
||||
ALTER TABLE controls ADD COLUMN category TEXT;
|
||||
|
||||
UPDATE controls SET category = 'security';
|
||||
|
||||
ALTER TABLE controls ALTER COLUMN category SET NOT NULL;
|
||||
15
pkg/coredata/migrations/20250218T101900Z.sql
Normal file
15
pkg/coredata/migrations/20250218T101900Z.sql
Normal file
@@ -0,0 +1,15 @@
|
||||
CREATE TYPE service_criticality AS ENUM ('LOW', 'MEDIUM', 'HIGH');
|
||||
CREATE TYPE risk_tier AS ENUM ('CRITICAL', 'SIGNIFICANT', 'GENERAL');
|
||||
|
||||
ALTER TABLE vendors
|
||||
ADD COLUMN service_start_date TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
|
||||
ADD COLUMN service_termination_date TIMESTAMP WITH TIME ZONE,
|
||||
ADD COLUMN service_criticality service_criticality NOT NULL DEFAULT 'LOW',
|
||||
ADD COLUMN risk_tier risk_tier NOT NULL DEFAULT 'GENERAL',
|
||||
ADD COLUMN status_page_url TEXT;
|
||||
|
||||
UPDATE vendors SET service_start_date = created_at;
|
||||
|
||||
ALTER TABLE vendors ALTER COLUMN service_start_date DROP DEFAULT;
|
||||
ALTER TABLE vendors ALTER COLUMN service_criticality DROP DEFAULT;
|
||||
ALTER TABLE vendors ALTER COLUMN risk_tier DROP DEFAULT;
|
||||
2
pkg/coredata/migrations/20250218T112800Z.sql
Normal file
2
pkg/coredata/migrations/20250218T112800Z.sql
Normal file
@@ -0,0 +1,2 @@
|
||||
ALTER TABLE vendors ADD COLUMN description TEXT NOT NULL DEFAULT '';
|
||||
ALTER TABLE vendors ALTER COLUMN description DROP DEFAULT;
|
||||
1
pkg/coredata/migrations/20250218T113200Z.sql
Normal file
1
pkg/coredata/migrations/20250218T113200Z.sql
Normal file
@@ -0,0 +1 @@
|
||||
ALTER TABLE vendors ADD COLUMN terms_of_service_url TEXT;
|
||||
1
pkg/coredata/migrations/20250218T113800Z.sql
Normal file
1
pkg/coredata/migrations/20250218T113800Z.sql
Normal file
@@ -0,0 +1 @@
|
||||
ALTER TABLE vendors ADD COLUMN privacy_policy_url TEXT;
|
||||
2
pkg/coredata/migrations/20250218T120000Z.sql
Normal file
2
pkg/coredata/migrations/20250218T120000Z.sql
Normal file
@@ -0,0 +1,2 @@
|
||||
ALTER TABLE vendors RENAME COLUMN service_start_date TO service_start_at;
|
||||
ALTER TABLE vendors RENAME COLUMN service_termination_date TO service_termination_at;
|
||||
1
pkg/coredata/migrations/20250218T121700Z.sql
Normal file
1
pkg/coredata/migrations/20250218T121700Z.sql
Normal file
@@ -0,0 +1 @@
|
||||
ALTER TABLE vendors ADD COLUMN version INTEGER NOT NULL DEFAULT 1;
|
||||
2
pkg/coredata/migrations/20250218T125600Z.sql
Normal file
2
pkg/coredata/migrations/20250218T125600Z.sql
Normal file
@@ -0,0 +1,2 @@
|
||||
ALTER TABLE evidences ADD COLUMN filename TEXT NOT NULL DEFAULT '';
|
||||
ALTER TABLE evidences ALTER COLUMN filename DROP DEFAULT;
|
||||
1
pkg/coredata/migrations/20250218T171000Z.sql
Normal file
1
pkg/coredata/migrations/20250218T171000Z.sql
Normal file
@@ -0,0 +1 @@
|
||||
ALTER TABLE peoples ADD COLUMN version INTEGER NOT NULL DEFAULT 1;
|
||||
3
pkg/coredata/migrations/20250225T153220Z.sql
Normal file
3
pkg/coredata/migrations/20250225T153220Z.sql
Normal file
@@ -0,0 +1,3 @@
|
||||
ALTER TABLE usrmgr_users ADD COLUMN organization_id TEXT;
|
||||
|
||||
CREATE INDEX usrmgr_users_organization_id_idx ON usrmgr_users(organization_id);
|
||||
11
pkg/coredata/migrations/20250225T201106Z.sql
Normal file
11
pkg/coredata/migrations/20250225T201106Z.sql
Normal file
@@ -0,0 +1,11 @@
|
||||
CREATE TABLE usrmgr_user_organizations (
|
||||
user_id TEXT REFERENCES usrmgr_users(id) NOT NULL,
|
||||
organization_id TEXT NOT NULL,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
||||
PRIMARY KEY (user_id, organization_id)
|
||||
);
|
||||
|
||||
INSERT INTO usrmgr_user_organizations (user_id, organization_id, created_at)
|
||||
SELECT id, organization_id, NOW()
|
||||
FROM usrmgr_users
|
||||
WHERE organization_id IS NOT NULL;
|
||||
1
pkg/coredata/migrations/20250225T211652Z.sql
Normal file
1
pkg/coredata/migrations/20250225T211652Z.sql
Normal file
@@ -0,0 +1 @@
|
||||
ALTER TABLE usrmgr_users ADD COLUMN fullname TEXT;
|
||||
1
pkg/coredata/migrations/20250225T222000Z.sql
Normal file
1
pkg/coredata/migrations/20250225T222000Z.sql
Normal file
@@ -0,0 +1 @@
|
||||
ALTER TABLE usrmgr_users ALTER COLUMN fullname SET NOT NULL;
|
||||
2
pkg/coredata/migrations/20250227T144100Z.sql
Normal file
2
pkg/coredata/migrations/20250227T144100Z.sql
Normal file
@@ -0,0 +1,2 @@
|
||||
ALTER TABLE frameworks ADD COLUMN version INTEGER NOT NULL DEFAULT 0;
|
||||
ALTER TABLE frameworks ALTER COLUMN version DROP DEFAULT;
|
||||
2
pkg/coredata/migrations/20250227T152900Z.sql
Normal file
2
pkg/coredata/migrations/20250227T152900Z.sql
Normal file
@@ -0,0 +1,2 @@
|
||||
ALTER TABLE controls ADD COLUMN version INTEGER NOT NULL DEFAULT 0;
|
||||
ALTER TABLE controls ALTER COLUMN version DROP DEFAULT;
|
||||
12
pkg/coredata/migrations/20250228T000000Z.sql
Normal file
12
pkg/coredata/migrations/20250228T000000Z.sql
Normal file
@@ -0,0 +1,12 @@
|
||||
CREATE TYPE policy_status AS ENUM ('DRAFT', 'ACTIVE');
|
||||
|
||||
CREATE TABLE policies (
|
||||
id TEXT PRIMARY KEY,
|
||||
organization_id TEXT REFERENCES organizations(id) NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
content TEXT NOT NULL,
|
||||
status policy_status NOT NULL,
|
||||
created_at TIMESTAMP WITH TIME ZONE NOT NULL,
|
||||
updated_at TIMESTAMP WITH TIME ZONE NOT NULL,
|
||||
version INTEGER NOT NULL
|
||||
);
|
||||
1
pkg/coredata/migrations/20250305T110005Z.sql
Normal file
1
pkg/coredata/migrations/20250305T110005Z.sql
Normal file
@@ -0,0 +1 @@
|
||||
ALTER TABLE policies ADD COLUMN review_date TIMESTAMP WITH TIME ZONE;
|
||||
1
pkg/coredata/migrations/20250305T111053Z.sql
Normal file
1
pkg/coredata/migrations/20250305T111053Z.sql
Normal file
@@ -0,0 +1 @@
|
||||
ALTER TABLE policies ADD COLUMN owner_id TEXT REFERENCES peoples(id) NOT NULL;
|
||||
47
pkg/coredata/migrations/20250305T155800Z.sql
Normal file
47
pkg/coredata/migrations/20250305T155800Z.sql
Normal file
@@ -0,0 +1,47 @@
|
||||
ALTER TABLE organizations ADD COLUMN tenant_id TEXT;
|
||||
UPDATE organizations SET tenant_id = id;
|
||||
ALTER TABLE organizations ALTER COLUMN tenant_id SET NOT NULL;
|
||||
|
||||
ALTER TABLE frameworks ADD COLUMN tenant_id TEXT;
|
||||
UPDATE frameworks f SET tenant_id = f.organization_id;
|
||||
ALTER TABLE frameworks ALTER COLUMN tenant_id SET NOT NULL;
|
||||
|
||||
ALTER TABLE controls ADD COLUMN tenant_id TEXT;
|
||||
UPDATE controls c SET tenant_id = (SELECT organization_id FROM frameworks f WHERE f.id = c.framework_id);
|
||||
ALTER TABLE controls ALTER COLUMN tenant_id SET NOT NULL;
|
||||
|
||||
ALTER TABLE tasks ADD COLUMN tenant_id TEXT;
|
||||
UPDATE tasks t SET tenant_id = (SELECT c.tenant_id FROM controls_tasks ct JOIN controls c ON ct.control_id = c.id WHERE ct.task_id = t.id LIMIT 1);
|
||||
ALTER TABLE tasks ALTER COLUMN tenant_id SET NOT NULL;
|
||||
|
||||
ALTER TABLE evidences ADD COLUMN tenant_id TEXT;
|
||||
UPDATE evidences e SET tenant_id = (SELECT t.tenant_id FROM tasks t WHERE t.id = e.task_id);
|
||||
ALTER TABLE evidences ALTER COLUMN tenant_id SET NOT NULL;
|
||||
|
||||
ALTER TABLE controls_tasks ADD COLUMN tenant_id TEXT;
|
||||
UPDATE controls_tasks ct SET tenant_id = (SELECT f.tenant_id FROM frameworks f JOIN controls c ON c.framework_id = f.id WHERE c.id = ct.control_id);
|
||||
ALTER TABLE controls_tasks ALTER COLUMN tenant_id SET NOT NULL;
|
||||
|
||||
ALTER TABLE control_state_transitions ADD COLUMN tenant_id TEXT;
|
||||
UPDATE control_state_transitions cst SET tenant_id = (SELECT c.tenant_id FROM controls c WHERE c.id = cst.control_id);
|
||||
ALTER TABLE control_state_transitions ALTER COLUMN tenant_id SET NOT NULL;
|
||||
|
||||
ALTER TABLE task_state_transitions ADD COLUMN tenant_id TEXT;
|
||||
UPDATE task_state_transitions tst SET tenant_id = (SELECT t.tenant_id FROM tasks t WHERE t.id = tst.task_id);
|
||||
ALTER TABLE task_state_transitions ALTER COLUMN tenant_id SET NOT NULL;
|
||||
|
||||
ALTER TABLE evidence_state_transitions ADD COLUMN tenant_id TEXT;
|
||||
UPDATE evidence_state_transitions est SET tenant_id = (SELECT e.tenant_id FROM evidences e WHERE e.id = est.evidence_id);
|
||||
ALTER TABLE evidence_state_transitions ALTER COLUMN tenant_id SET NOT NULL;
|
||||
|
||||
ALTER TABLE peoples ADD COLUMN tenant_id TEXT;
|
||||
UPDATE peoples p SET tenant_id = p.organization_id;
|
||||
ALTER TABLE peoples ALTER COLUMN tenant_id SET NOT NULL;
|
||||
|
||||
ALTER TABLE vendors ADD COLUMN tenant_id TEXT;
|
||||
UPDATE vendors v SET tenant_id = v.organization_id;
|
||||
ALTER TABLE vendors ALTER COLUMN tenant_id SET NOT NULL;
|
||||
|
||||
ALTER TABLE policies ADD COLUMN tenant_id TEXT;
|
||||
UPDATE policies p SET tenant_id = p.organization_id;
|
||||
ALTER TABLE policies ALTER COLUMN tenant_id SET NOT NULL;
|
||||
1
pkg/coredata/migrations/20250305T171200Z.sql
Normal file
1
pkg/coredata/migrations/20250305T171200Z.sql
Normal file
@@ -0,0 +1 @@
|
||||
ALTER TABLE usrmgr_sessions ADD COLUMN data jsonb NOT NULL DEFAULT '{}';
|
||||
17
pkg/coredata/migrations/20250310T161900Z.sql
Normal file
17
pkg/coredata/migrations/20250310T161900Z.sql
Normal file
@@ -0,0 +1,17 @@
|
||||
DROP TABLE control_state_transitions;
|
||||
DROP TABLE evidence_state_transitions;
|
||||
DROP TABLE task_state_transitions;
|
||||
|
||||
ALTER TABLE tasks ADD COLUMN control_id TEXT;
|
||||
|
||||
UPDATE tasks
|
||||
SET control_id = controls_tasks.control_id
|
||||
FROM controls_tasks
|
||||
WHERE tasks.id = controls_tasks.task_id;
|
||||
|
||||
ALTER TABLE tasks ALTER COLUMN control_id SET NOT NULL;
|
||||
|
||||
ALTER TABLE tasks ADD CONSTRAINT fk_tasks_control_id
|
||||
FOREIGN KEY (control_id) REFERENCES controls(id) ON DELETE CASCADE;
|
||||
|
||||
DROP TABLE controls_tasks;
|
||||
3
pkg/coredata/migrations/20250310T164900Z.sql
Normal file
3
pkg/coredata/migrations/20250310T164900Z.sql
Normal file
@@ -0,0 +1,3 @@
|
||||
ALTER TABLE controls ADD COLUMN state control_state;
|
||||
ALTER TABLE evidences ADD COLUMN state evidence_state;
|
||||
ALTER TABLE tasks ADD COLUMN state task_state;
|
||||
1
pkg/coredata/migrations/20250310T181100Z.sql
Normal file
1
pkg/coredata/migrations/20250310T181100Z.sql
Normal file
@@ -0,0 +1 @@
|
||||
ALTER TABLE tasks ADD COLUMN version INTEGER NOT NULL DEFAULT 1;
|
||||
111
pkg/coredata/organization.go
Normal file
111
pkg/coredata/organization.go
Normal file
@@ -0,0 +1,111 @@
|
||||
// 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 (
|
||||
"context"
|
||||
"fmt"
|
||||
"maps"
|
||||
"time"
|
||||
|
||||
"github.com/getprobo/probo/pkg/gid"
|
||||
"github.com/jackc/pgx/v5"
|
||||
"go.gearno.de/kit/pg"
|
||||
)
|
||||
|
||||
type (
|
||||
Organization struct {
|
||||
ID gid.GID `db:"id"`
|
||||
TenantID gid.TenantID `db:"tenant_id"`
|
||||
Name string `db:"name"`
|
||||
LogoURL string `db:"logo_url"`
|
||||
CreatedAt time.Time `db:"created_at"`
|
||||
UpdatedAt time.Time `db:"updated_at"`
|
||||
}
|
||||
)
|
||||
|
||||
func (o *Organization) LoadByID(
|
||||
ctx context.Context,
|
||||
conn pg.Conn,
|
||||
scope Scoper,
|
||||
organizationID gid.GID,
|
||||
) error {
|
||||
q := `
|
||||
SELECT
|
||||
tenant_id,
|
||||
id,
|
||||
name,
|
||||
logo_url,
|
||||
created_at,
|
||||
updated_at
|
||||
FROM
|
||||
organizations
|
||||
WHERE
|
||||
%s
|
||||
AND id = @organization_id
|
||||
LIMIT 1;
|
||||
`
|
||||
|
||||
q = fmt.Sprintf(q, scope.SQLFragment())
|
||||
|
||||
args := pgx.StrictNamedArgs{"organization_id": organizationID}
|
||||
maps.Copy(args, scope.SQLArguments())
|
||||
|
||||
rows, err := conn.Query(ctx, q, args)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot query organizations: %w", err)
|
||||
}
|
||||
|
||||
organization, err := pgx.CollectExactlyOneRow(rows, pgx.RowToStructByName[Organization])
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot collect organization: %w", err)
|
||||
}
|
||||
|
||||
*o = organization
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (o *Organization) Insert(
|
||||
ctx context.Context,
|
||||
conn pg.Conn,
|
||||
) error {
|
||||
q := `
|
||||
INSERT INTO organizations (
|
||||
tenant_id,
|
||||
id,
|
||||
name,
|
||||
logo_url,
|
||||
created_at,
|
||||
updated_at
|
||||
) VALUES (@tenant_id, @id, @name, @logo_url, @created_at, @updated_at)
|
||||
`
|
||||
|
||||
args := pgx.StrictNamedArgs{
|
||||
"tenant_id": o.TenantID,
|
||||
"id": o.ID,
|
||||
"name": o.Name,
|
||||
"logo_url": o.LogoURL,
|
||||
"created_at": o.CreatedAt,
|
||||
"updated_at": o.UpdatedAt,
|
||||
}
|
||||
|
||||
_, err := conn.Exec(ctx, q, args)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
280
pkg/coredata/people.go
Normal file
280
pkg/coredata/people.go
Normal file
@@ -0,0 +1,280 @@
|
||||
// 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 (
|
||||
"context"
|
||||
"fmt"
|
||||
"maps"
|
||||
"time"
|
||||
|
||||
"github.com/getprobo/probo/pkg/gid"
|
||||
"github.com/getprobo/probo/pkg/page"
|
||||
"github.com/jackc/pgx/v5"
|
||||
"go.gearno.de/kit/pg"
|
||||
)
|
||||
|
||||
type (
|
||||
People struct {
|
||||
ID gid.GID `db:"id"`
|
||||
OrganizationID gid.GID `db:"organization_id"`
|
||||
Kind PeopleKind `db:"kind"`
|
||||
FullName string `db:"full_name"`
|
||||
PrimaryEmailAddress string `db:"primary_email_address"`
|
||||
AdditionalEmailAddresses []string `db:"additional_email_addresses"`
|
||||
CreatedAt time.Time `db:"created_at"`
|
||||
UpdatedAt time.Time `db:"updated_at"`
|
||||
Version int `db:"version"`
|
||||
}
|
||||
|
||||
Peoples []*People
|
||||
|
||||
UpdatePeopleParams struct {
|
||||
ExpectedVersion int
|
||||
FullName *string
|
||||
PrimaryEmailAddress *string
|
||||
AdditionalEmailAddresses *[]string
|
||||
Kind *PeopleKind
|
||||
}
|
||||
)
|
||||
|
||||
func (p People) CursorKey() page.CursorKey {
|
||||
return page.NewCursorKey(p.ID, p.CreatedAt)
|
||||
}
|
||||
|
||||
func (p *People) LoadByID(
|
||||
ctx context.Context,
|
||||
conn pg.Conn,
|
||||
scope Scoper,
|
||||
peopleID gid.GID,
|
||||
) error {
|
||||
q := `
|
||||
SELECT
|
||||
id,
|
||||
organization_id,
|
||||
kind,
|
||||
full_name,
|
||||
primary_email_address,
|
||||
additional_email_addresses,
|
||||
created_at,
|
||||
updated_at,
|
||||
version
|
||||
FROM
|
||||
peoples
|
||||
WHERE
|
||||
%s
|
||||
AND id = @people_id
|
||||
LIMIT 1;
|
||||
`
|
||||
|
||||
q = fmt.Sprintf(q, scope.SQLFragment())
|
||||
|
||||
args := pgx.StrictNamedArgs{"people_id": peopleID}
|
||||
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 {
|
||||
return fmt.Errorf("cannot collect people: %w", err)
|
||||
}
|
||||
|
||||
*p = people
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p People) Insert(
|
||||
ctx context.Context,
|
||||
conn pg.Conn,
|
||||
scope Scoper,
|
||||
) error {
|
||||
q := `
|
||||
INSERT INTO
|
||||
peoples (
|
||||
tenant_id,
|
||||
id,
|
||||
organization_id,
|
||||
kind,
|
||||
full_name,
|
||||
primary_email_address,
|
||||
additional_email_addresses,
|
||||
created_at,
|
||||
updated_at,
|
||||
version
|
||||
)
|
||||
VALUES (
|
||||
@tenant_id,
|
||||
@people_id,
|
||||
@organization_id,
|
||||
@kind,
|
||||
@full_name,
|
||||
@primary_email_address,
|
||||
@additional_email_addresses,
|
||||
@created_at,
|
||||
@updated_at,
|
||||
@version
|
||||
)
|
||||
`
|
||||
|
||||
args := pgx.StrictNamedArgs{
|
||||
"tenant_id": scope.GetTenantID(),
|
||||
"people_id": p.ID,
|
||||
"organization_id": p.OrganizationID,
|
||||
"kind": p.Kind,
|
||||
"full_name": p.FullName,
|
||||
"primary_email_address": p.PrimaryEmailAddress,
|
||||
"additional_email_addresses": p.AdditionalEmailAddresses,
|
||||
"created_at": p.CreatedAt,
|
||||
"updated_at": p.UpdatedAt,
|
||||
"version": p.Version,
|
||||
}
|
||||
_, err := conn.Exec(ctx, q, args)
|
||||
return err
|
||||
}
|
||||
|
||||
func (p People) Delete(
|
||||
ctx context.Context,
|
||||
conn pg.Conn,
|
||||
scope Scoper,
|
||||
) error {
|
||||
q := `
|
||||
DELETE FROM peoples WHERE %s AND id = @people_id
|
||||
`
|
||||
|
||||
q = fmt.Sprintf(q, scope.SQLFragment())
|
||||
|
||||
args := pgx.StrictNamedArgs{"people_id": p.ID}
|
||||
maps.Copy(args, scope.SQLArguments())
|
||||
|
||||
_, err := conn.Exec(ctx, q, args)
|
||||
return err
|
||||
}
|
||||
|
||||
func (p *Peoples) LoadByOrganizationID(
|
||||
ctx context.Context,
|
||||
conn pg.Conn,
|
||||
scope Scoper,
|
||||
organizationID gid.GID,
|
||||
cursor *page.Cursor,
|
||||
) error {
|
||||
q := `
|
||||
SELECT
|
||||
id,
|
||||
organization_id,
|
||||
kind,
|
||||
full_name,
|
||||
primary_email_address,
|
||||
additional_email_addresses,
|
||||
created_at,
|
||||
updated_at,
|
||||
version
|
||||
FROM
|
||||
peoples
|
||||
WHERE
|
||||
%s
|
||||
AND organization_id = @organization_id
|
||||
AND %s
|
||||
`
|
||||
|
||||
q = fmt.Sprintf(q, scope.SQLFragment(), cursor.SQLFragment())
|
||||
|
||||
args := pgx.StrictNamedArgs{"organization_id": organizationID}
|
||||
maps.Copy(args, cursor.SQLArguments())
|
||||
maps.Copy(args, scope.SQLArguments())
|
||||
|
||||
rows, err := conn.Query(ctx, q, args)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot query people: %w", err)
|
||||
}
|
||||
|
||||
peoples, err := pgx.CollectRows(rows, pgx.RowToAddrOfStructByName[People])
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot collect people: %w", err)
|
||||
}
|
||||
|
||||
*p = peoples
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *People) Update(
|
||||
ctx context.Context,
|
||||
conn pg.Conn,
|
||||
scope Scoper,
|
||||
params UpdatePeopleParams,
|
||||
) error {
|
||||
q := `
|
||||
UPDATE peoples SET
|
||||
full_name = COALESCE(@full_name, full_name),
|
||||
primary_email_address = COALESCE(@primary_email_address, primary_email_address),
|
||||
additional_email_addresses = COALESCE(@additional_email_addresses, additional_email_addresses),
|
||||
kind = COALESCE(@kind, kind),
|
||||
updated_at = @updated_at,
|
||||
version = version + 1
|
||||
WHERE %s
|
||||
AND id = @people_id
|
||||
AND version = @expected_version
|
||||
RETURNING
|
||||
id,
|
||||
organization_id,
|
||||
kind,
|
||||
full_name,
|
||||
primary_email_address,
|
||||
additional_email_addresses,
|
||||
created_at,
|
||||
updated_at,
|
||||
version
|
||||
`
|
||||
q = fmt.Sprintf(q, scope.SQLFragment())
|
||||
|
||||
args := pgx.StrictNamedArgs{
|
||||
"people_id": p.ID,
|
||||
"expected_version": params.ExpectedVersion,
|
||||
"updated_at": time.Now(),
|
||||
}
|
||||
|
||||
if params.FullName != nil {
|
||||
args["full_name"] = *params.FullName
|
||||
}
|
||||
if params.PrimaryEmailAddress != nil {
|
||||
args["primary_email_address"] = *params.PrimaryEmailAddress
|
||||
}
|
||||
if params.AdditionalEmailAddresses != nil {
|
||||
args["additional_email_addresses"] = *params.AdditionalEmailAddresses
|
||||
}
|
||||
if params.Kind != nil {
|
||||
args["kind"] = *params.Kind
|
||||
}
|
||||
|
||||
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 {
|
||||
return fmt.Errorf("cannot collect people: %w", err)
|
||||
}
|
||||
|
||||
*p = people
|
||||
|
||||
return nil
|
||||
}
|
||||
74
pkg/coredata/people_kind.go
Normal file
74
pkg/coredata/people_kind.go
Normal file
@@ -0,0 +1,74 @@
|
||||
// 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 (
|
||||
"database/sql/driver"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
type (
|
||||
PeopleKind uint8
|
||||
)
|
||||
|
||||
const (
|
||||
PeopleKindEmployee PeopleKind = iota
|
||||
PeopleKindContractor
|
||||
)
|
||||
|
||||
func (ps PeopleKind) MarshalText() ([]byte, error) {
|
||||
return []byte(ps.String()), nil
|
||||
}
|
||||
|
||||
func (ps *PeopleKind) UnmarshalText(data []byte) error {
|
||||
val := string(data)
|
||||
|
||||
switch val {
|
||||
case PeopleKindEmployee.String():
|
||||
*ps = PeopleKindEmployee
|
||||
case PeopleKindContractor.String():
|
||||
*ps = PeopleKindContractor
|
||||
default:
|
||||
return fmt.Errorf("invalid PeopleKind value: %q", val)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ts PeopleKind) String() string {
|
||||
var val string
|
||||
|
||||
switch ts {
|
||||
case PeopleKindEmployee:
|
||||
val = "EMPLOYEE"
|
||||
case PeopleKindContractor:
|
||||
val = "CONTRACTOR"
|
||||
}
|
||||
|
||||
return val
|
||||
}
|
||||
|
||||
func (pk *PeopleKind) Scan(value any) error {
|
||||
val, ok := value.(string)
|
||||
if !ok {
|
||||
return fmt.Errorf("invalid scan source for PeopleKind, expected string got %T", value)
|
||||
}
|
||||
|
||||
return pk.UnmarshalText([]byte(val))
|
||||
}
|
||||
|
||||
func (pk PeopleKind) Value() (driver.Value, error) {
|
||||
return pk.String(), nil
|
||||
}
|
||||
278
pkg/coredata/policy.go
Normal file
278
pkg/coredata/policy.go
Normal file
@@ -0,0 +1,278 @@
|
||||
package coredata
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"maps"
|
||||
"time"
|
||||
|
||||
"github.com/getprobo/probo/pkg/gid"
|
||||
"github.com/getprobo/probo/pkg/page"
|
||||
"github.com/jackc/pgx/v5"
|
||||
"go.gearno.de/kit/pg"
|
||||
)
|
||||
|
||||
type (
|
||||
Policy struct {
|
||||
ID gid.GID `db:"id"`
|
||||
OrganizationID gid.GID `db:"organization_id"`
|
||||
OwnerID gid.GID `db:"owner_id"`
|
||||
Status PolicyStatus `db:"status"`
|
||||
Name string `db:"name"`
|
||||
Content string `db:"content"`
|
||||
ReviewDate *time.Time `db:"review_date"`
|
||||
CreatedAt time.Time `db:"created_at"`
|
||||
UpdatedAt time.Time `db:"updated_at"`
|
||||
Version int `db:"version"`
|
||||
}
|
||||
|
||||
Policies []*Policy
|
||||
|
||||
UpdatePolicyParams struct {
|
||||
ExpectedVersion int
|
||||
Name *string
|
||||
Content *string
|
||||
Status *PolicyStatus
|
||||
ReviewDate **time.Time
|
||||
OwnerID *gid.GID
|
||||
}
|
||||
)
|
||||
|
||||
func (p Policy) CursorKey() page.CursorKey {
|
||||
return page.NewCursorKey(p.ID, p.CreatedAt)
|
||||
}
|
||||
|
||||
func (p *Policy) LoadByID(
|
||||
ctx context.Context,
|
||||
conn pg.Conn,
|
||||
scope Scoper,
|
||||
policyID gid.GID,
|
||||
) error {
|
||||
q := `
|
||||
SELECT
|
||||
id,
|
||||
organization_id,
|
||||
owner_id,
|
||||
name,
|
||||
status,
|
||||
content,
|
||||
review_date,
|
||||
created_at,
|
||||
updated_at,
|
||||
version
|
||||
FROM
|
||||
policies
|
||||
WHERE
|
||||
%s
|
||||
AND id = @policy_id
|
||||
LIMIT 1;
|
||||
`
|
||||
|
||||
q = fmt.Sprintf(q, scope.SQLFragment())
|
||||
|
||||
args := pgx.StrictNamedArgs{"policy_id": policyID}
|
||||
maps.Copy(args, scope.SQLArguments())
|
||||
|
||||
rows, err := conn.Query(ctx, q, args)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot query policies: %w", err)
|
||||
}
|
||||
|
||||
policy, err := pgx.CollectExactlyOneRow(rows, pgx.RowToStructByName[Policy])
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot collect policy: %w", err)
|
||||
}
|
||||
|
||||
*p = policy
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Policies) LoadByOrganizationID(
|
||||
ctx context.Context,
|
||||
conn pg.Conn,
|
||||
scope Scoper,
|
||||
organizationID gid.GID,
|
||||
cursor *page.Cursor,
|
||||
) error {
|
||||
q := `
|
||||
SELECT
|
||||
id,
|
||||
organization_id,
|
||||
owner_id,
|
||||
name,
|
||||
status,
|
||||
content,
|
||||
review_date,
|
||||
created_at,
|
||||
updated_at,
|
||||
version
|
||||
FROM
|
||||
policies
|
||||
WHERE
|
||||
%s
|
||||
AND organization_id = @organization_id
|
||||
AND %s
|
||||
`
|
||||
|
||||
q = fmt.Sprintf(q, scope.SQLFragment(), cursor.SQLFragment())
|
||||
|
||||
args := pgx.StrictNamedArgs{"organization_id": organizationID}
|
||||
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 policies: %w", err)
|
||||
}
|
||||
|
||||
policies, err := pgx.CollectRows(rows, pgx.RowToAddrOfStructByName[Policy])
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot collect policies: %w", err)
|
||||
}
|
||||
|
||||
*p = policies
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p Policy) Insert(
|
||||
ctx context.Context,
|
||||
conn pg.Conn,
|
||||
scope Scoper,
|
||||
) error {
|
||||
q := `
|
||||
INSERT INTO
|
||||
policies (
|
||||
tenant_id,
|
||||
id,
|
||||
organization_id,
|
||||
owner_id,
|
||||
name,
|
||||
status,
|
||||
content,
|
||||
review_date,
|
||||
created_at,
|
||||
updated_at,
|
||||
version
|
||||
)
|
||||
VALUES (
|
||||
@tenant_id,
|
||||
@policy_id,
|
||||
@organization_id,
|
||||
@owner_id,
|
||||
@name,
|
||||
@status,
|
||||
@content,
|
||||
@review_date,
|
||||
@created_at,
|
||||
@updated_at,
|
||||
@version
|
||||
);
|
||||
`
|
||||
|
||||
args := pgx.StrictNamedArgs{
|
||||
"tenant_id": scope.GetTenantID(),
|
||||
"policy_id": p.ID,
|
||||
"organization_id": p.OrganizationID,
|
||||
"owner_id": p.OwnerID,
|
||||
"name": p.Name,
|
||||
"status": p.Status,
|
||||
"content": p.Content,
|
||||
"review_date": p.ReviewDate,
|
||||
"created_at": p.CreatedAt,
|
||||
"updated_at": p.UpdatedAt,
|
||||
"version": p.Version,
|
||||
}
|
||||
_, err := conn.Exec(ctx, q, args)
|
||||
return err
|
||||
}
|
||||
|
||||
func (p Policy) Delete(
|
||||
ctx context.Context,
|
||||
conn pg.Conn,
|
||||
scope Scoper,
|
||||
) error {
|
||||
q := `
|
||||
DELETE FROM policies WHERE %s AND id = @policy_id
|
||||
`
|
||||
|
||||
q = fmt.Sprintf(q, scope.SQLFragment())
|
||||
|
||||
args := pgx.StrictNamedArgs{"policy_id": p.ID}
|
||||
maps.Copy(args, scope.SQLArguments())
|
||||
|
||||
_, err := conn.Exec(ctx, q, args)
|
||||
return err
|
||||
}
|
||||
|
||||
func (p *Policy) Update(
|
||||
ctx context.Context,
|
||||
conn pg.Conn,
|
||||
scope Scoper,
|
||||
params UpdatePolicyParams,
|
||||
) error {
|
||||
q := `
|
||||
UPDATE policies SET
|
||||
name = COALESCE(@name, name),
|
||||
status = COALESCE(@status, status),
|
||||
content = COALESCE(@content, content),
|
||||
review_date = COALESCE(@review_date, review_date),
|
||||
owner_id = COALESCE(@owner_id, owner_id),
|
||||
updated_at = @updated_at,
|
||||
version = version + 1
|
||||
WHERE %s
|
||||
AND id = @policy_id
|
||||
AND version = @expected_version
|
||||
RETURNING
|
||||
id,
|
||||
organization_id,
|
||||
owner_id,
|
||||
name,
|
||||
content,
|
||||
review_date,
|
||||
created_at,
|
||||
updated_at,
|
||||
status,
|
||||
version
|
||||
`
|
||||
q = fmt.Sprintf(q, scope.SQLFragment())
|
||||
|
||||
args := pgx.StrictNamedArgs{
|
||||
"policy_id": p.ID,
|
||||
"expected_version": params.ExpectedVersion,
|
||||
"updated_at": time.Now(),
|
||||
}
|
||||
|
||||
if params.Name != nil {
|
||||
args["name"] = *params.Name
|
||||
}
|
||||
if params.Content != nil {
|
||||
args["content"] = *params.Content
|
||||
}
|
||||
if params.Status != nil {
|
||||
args["status"] = *params.Status
|
||||
}
|
||||
if params.ReviewDate != nil {
|
||||
args["review_date"] = *params.ReviewDate
|
||||
}
|
||||
if params.OwnerID != nil {
|
||||
args["owner_id"] = *params.OwnerID
|
||||
}
|
||||
|
||||
maps.Copy(args, scope.SQLArguments())
|
||||
|
||||
rows, err := conn.Query(ctx, q, args)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot query policies: %w", err)
|
||||
}
|
||||
|
||||
policy, err := pgx.CollectExactlyOneRow(rows, pgx.RowToStructByName[Policy])
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot collect policy: %w", err)
|
||||
}
|
||||
|
||||
*p = policy
|
||||
|
||||
return nil
|
||||
}
|
||||
74
pkg/coredata/policy_status.go
Normal file
74
pkg/coredata/policy_status.go
Normal file
@@ -0,0 +1,74 @@
|
||||
// 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 (
|
||||
"database/sql/driver"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
type (
|
||||
PolicyStatus uint8
|
||||
)
|
||||
|
||||
const (
|
||||
PolicyStatusDraft PolicyStatus = iota
|
||||
PolicyStatusActive
|
||||
)
|
||||
|
||||
func (ps PolicyStatus) MarshalText() ([]byte, error) {
|
||||
return []byte(ps.String()), nil
|
||||
}
|
||||
|
||||
func (ps *PolicyStatus) UnmarshalText(data []byte) error {
|
||||
val := string(data)
|
||||
|
||||
switch val {
|
||||
case PolicyStatusDraft.String():
|
||||
*ps = PolicyStatusDraft
|
||||
case PolicyStatusActive.String():
|
||||
*ps = PolicyStatusActive
|
||||
default:
|
||||
return fmt.Errorf("invalid PolicyStatus value: %q", val)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ps PolicyStatus) String() string {
|
||||
var val string
|
||||
|
||||
switch ps {
|
||||
case PolicyStatusDraft:
|
||||
val = "DRAFT"
|
||||
case PolicyStatusActive:
|
||||
val = "ACTIVE"
|
||||
}
|
||||
|
||||
return val
|
||||
}
|
||||
|
||||
func (ps *PolicyStatus) Scan(value any) error {
|
||||
val, ok := value.(string)
|
||||
if !ok {
|
||||
return fmt.Errorf("invalid scan source for PolicyStatus, expected string got %T", value)
|
||||
}
|
||||
|
||||
return ps.UnmarshalText([]byte(val))
|
||||
}
|
||||
|
||||
func (ps PolicyStatus) Value() (driver.Value, error) {
|
||||
return ps.String(), nil
|
||||
}
|
||||
66
pkg/coredata/risk_tier.go
Normal file
66
pkg/coredata/risk_tier.go
Normal file
@@ -0,0 +1,66 @@
|
||||
// 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 (
|
||||
"database/sql/driver"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
type RiskTier string
|
||||
|
||||
const (
|
||||
RiskTierCritical RiskTier = "CRITICAL" // Handles sensitive data, critical for platform operation
|
||||
RiskTierSignificant RiskTier = "SIGNIFICANT" // No user data access, but important for platform management
|
||||
RiskTierGeneral RiskTier = "GENERAL" // General vendor with minimal risk
|
||||
)
|
||||
|
||||
func (rt RiskTier) MarshalText() ([]byte, error) {
|
||||
return []byte(rt.String()), nil
|
||||
}
|
||||
|
||||
func (rt *RiskTier) UnmarshalText(data []byte) error {
|
||||
val := string(data)
|
||||
|
||||
switch val {
|
||||
case RiskTierCritical.String():
|
||||
*rt = RiskTierCritical
|
||||
case RiskTierSignificant.String():
|
||||
*rt = RiskTierSignificant
|
||||
case RiskTierGeneral.String():
|
||||
*rt = RiskTierGeneral
|
||||
default:
|
||||
return fmt.Errorf("invalid RiskTier value: %q", val)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (rt RiskTier) String() string {
|
||||
return string(rt)
|
||||
}
|
||||
|
||||
func (rt *RiskTier) Scan(value any) error {
|
||||
val, ok := value.(string)
|
||||
if !ok {
|
||||
return fmt.Errorf("invalid scan source for RiskTier, expected string got %T", value)
|
||||
}
|
||||
|
||||
return rt.UnmarshalText([]byte(val))
|
||||
}
|
||||
|
||||
func (rt RiskTier) Value() (driver.Value, error) {
|
||||
return rt.String(), nil
|
||||
}
|
||||
77
pkg/coredata/scope.go
Normal file
77
pkg/coredata/scope.go
Normal file
@@ -0,0 +1,77 @@
|
||||
// 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 (
|
||||
"fmt"
|
||||
|
||||
"github.com/getprobo/probo/pkg/gid"
|
||||
"github.com/jackc/pgx/v5"
|
||||
)
|
||||
|
||||
type (
|
||||
Scoper interface {
|
||||
SQLArguments() pgx.StrictNamedArgs
|
||||
SQLFragment() string
|
||||
GetTenantID() gid.TenantID
|
||||
}
|
||||
|
||||
NoScope struct{}
|
||||
|
||||
Scope struct {
|
||||
tenantID gid.TenantID
|
||||
}
|
||||
)
|
||||
|
||||
var (
|
||||
_ Scoper = (*NoScope)(nil)
|
||||
_ Scoper = (*Scope)(nil)
|
||||
)
|
||||
|
||||
func NewNoScope() *NoScope {
|
||||
return &NoScope{}
|
||||
}
|
||||
|
||||
func (*NoScope) SQLArguments() pgx.StrictNamedArgs {
|
||||
return pgx.StrictNamedArgs{}
|
||||
}
|
||||
|
||||
func (*NoScope) SQLFragment() string {
|
||||
return "TRUE"
|
||||
}
|
||||
|
||||
func (*NoScope) GetTenantID() gid.TenantID {
|
||||
panic(fmt.Errorf("cannot get tenant id from no scope"))
|
||||
}
|
||||
|
||||
func NewScope(tenantID gid.TenantID) *Scope {
|
||||
return &Scope{
|
||||
tenantID: tenantID,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Scope) SQLArguments() pgx.StrictNamedArgs {
|
||||
return pgx.StrictNamedArgs{
|
||||
"tenant_id": s.tenantID,
|
||||
}
|
||||
}
|
||||
|
||||
func (*Scope) SQLFragment() string {
|
||||
return "tenant_id = @tenant_id"
|
||||
}
|
||||
|
||||
func (s *Scope) GetTenantID() gid.TenantID {
|
||||
return s.tenantID
|
||||
}
|
||||
66
pkg/coredata/service_criticality.go
Normal file
66
pkg/coredata/service_criticality.go
Normal file
@@ -0,0 +1,66 @@
|
||||
// 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 (
|
||||
"database/sql/driver"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
type ServiceCriticality string
|
||||
|
||||
const (
|
||||
ServiceCriticalityLow ServiceCriticality = "LOW"
|
||||
ServiceCriticalityMedium ServiceCriticality = "MEDIUM"
|
||||
ServiceCriticalityHigh ServiceCriticality = "HIGH"
|
||||
)
|
||||
|
||||
func (sc ServiceCriticality) MarshalText() ([]byte, error) {
|
||||
return []byte(sc.String()), nil
|
||||
}
|
||||
|
||||
func (sc *ServiceCriticality) UnmarshalText(data []byte) error {
|
||||
val := string(data)
|
||||
|
||||
switch val {
|
||||
case ServiceCriticalityLow.String():
|
||||
*sc = ServiceCriticalityLow
|
||||
case ServiceCriticalityMedium.String():
|
||||
*sc = ServiceCriticalityMedium
|
||||
case ServiceCriticalityHigh.String():
|
||||
*sc = ServiceCriticalityHigh
|
||||
default:
|
||||
return fmt.Errorf("invalid ServiceCriticality value: %q", val)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (sc ServiceCriticality) String() string {
|
||||
return string(sc)
|
||||
}
|
||||
|
||||
func (sc *ServiceCriticality) Scan(value any) error {
|
||||
val, ok := value.(string)
|
||||
if !ok {
|
||||
return fmt.Errorf("invalid scan source for ServiceCriticality, expected string got %T", value)
|
||||
}
|
||||
|
||||
return sc.UnmarshalText([]byte(val))
|
||||
}
|
||||
|
||||
func (sc ServiceCriticality) Value() (driver.Value, error) {
|
||||
return sc.String(), nil
|
||||
}
|
||||
152
pkg/coredata/session.go
Normal file
152
pkg/coredata/session.go
Normal file
@@ -0,0 +1,152 @@
|
||||
// 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 (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/getprobo/probo/pkg/gid"
|
||||
"github.com/getprobo/probo/pkg/page"
|
||||
"github.com/jackc/pgx/v5"
|
||||
"go.gearno.de/kit/pg"
|
||||
)
|
||||
|
||||
type (
|
||||
Session struct {
|
||||
ID gid.GID `db:"id"`
|
||||
UserID gid.GID `db:"user_id"`
|
||||
Data SessionData `db:"data"`
|
||||
ExpiredAt time.Time `db:"expired_at"`
|
||||
CreatedAt time.Time `db:"created_at"`
|
||||
UpdatedAt time.Time `db:"updated_at"`
|
||||
}
|
||||
|
||||
SessionData struct{}
|
||||
)
|
||||
|
||||
func (s Session) CursorKey() page.CursorKey {
|
||||
return page.NewCursorKey(s.ID, s.CreatedAt)
|
||||
}
|
||||
|
||||
func (s *Session) LoadByID(
|
||||
ctx context.Context,
|
||||
conn pg.Conn,
|
||||
sessionID gid.GID,
|
||||
) error {
|
||||
q := `
|
||||
SELECT
|
||||
id,
|
||||
user_id,
|
||||
data,
|
||||
expired_at,
|
||||
created_at,
|
||||
updated_at
|
||||
FROM
|
||||
usrmgr_sessions
|
||||
WHERE
|
||||
id = @session_id
|
||||
LIMIT 1;
|
||||
`
|
||||
|
||||
args := pgx.StrictNamedArgs{"session_id": sessionID}
|
||||
|
||||
rows, err := conn.Query(ctx, q, args)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot query session: %w", err)
|
||||
}
|
||||
|
||||
session, err := pgx.CollectExactlyOneRow(rows, pgx.RowToStructByName[Session])
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot collect session: %w", err)
|
||||
}
|
||||
*s = session
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Session) Insert(
|
||||
ctx context.Context,
|
||||
conn pg.Conn,
|
||||
) error {
|
||||
q := `
|
||||
INSERT INTO
|
||||
usrmgr_sessions (id, user_id, data, expired_at, created_at, updated_at)
|
||||
VALUES (
|
||||
@session_id,
|
||||
@user_id,
|
||||
@data,
|
||||
@expired_at,
|
||||
@created_at,
|
||||
@updated_at
|
||||
)
|
||||
`
|
||||
|
||||
args := pgx.StrictNamedArgs{
|
||||
"session_id": s.ID,
|
||||
"user_id": s.UserID,
|
||||
"data": s.Data,
|
||||
"expired_at": s.ExpiredAt,
|
||||
"created_at": s.CreatedAt,
|
||||
"updated_at": s.UpdatedAt,
|
||||
}
|
||||
|
||||
_, err := conn.Exec(ctx, q, args)
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *Session) Update(
|
||||
ctx context.Context,
|
||||
conn pg.Conn,
|
||||
) error {
|
||||
q := `
|
||||
UPDATE usrmgr_sessions
|
||||
SET
|
||||
expired_at = @expired_at,
|
||||
updated_at = @updated_at,
|
||||
data = @data
|
||||
WHERE
|
||||
id = @session_id
|
||||
`
|
||||
|
||||
args := pgx.StrictNamedArgs{
|
||||
"session_id": s.ID,
|
||||
"user_id": s.UserID,
|
||||
"expired_at": s.ExpiredAt,
|
||||
"updated_at": s.UpdatedAt,
|
||||
}
|
||||
|
||||
_, err := conn.Exec(ctx, q, args)
|
||||
return err
|
||||
}
|
||||
|
||||
func DeleteSession(
|
||||
ctx context.Context,
|
||||
conn pg.Conn,
|
||||
sessionID gid.GID,
|
||||
) error {
|
||||
q := `
|
||||
DELETE FROM
|
||||
usrmgr_sessions
|
||||
WHERE
|
||||
id = @session_id
|
||||
`
|
||||
|
||||
args := pgx.StrictNamedArgs{"session_id": sessionID}
|
||||
|
||||
_, err := conn.Exec(ctx, q, args)
|
||||
return err
|
||||
}
|
||||
303
pkg/coredata/task.go
Normal file
303
pkg/coredata/task.go
Normal file
@@ -0,0 +1,303 @@
|
||||
// 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 (
|
||||
"context"
|
||||
"fmt"
|
||||
"maps"
|
||||
"time"
|
||||
|
||||
"github.com/getprobo/probo/pkg/gid"
|
||||
"github.com/getprobo/probo/pkg/page"
|
||||
"github.com/jackc/pgx/v5"
|
||||
|
||||
"go.gearno.de/kit/pg"
|
||||
)
|
||||
|
||||
type (
|
||||
Task struct {
|
||||
ID gid.GID `db:"id"`
|
||||
ControlID gid.GID `db:"control_id"`
|
||||
Name string `db:"name"`
|
||||
Description string `db:"description"`
|
||||
State TaskState `db:"state"`
|
||||
ContentRef string `db:"content_ref"`
|
||||
CreatedAt time.Time `db:"created_at"`
|
||||
UpdatedAt time.Time `db:"updated_at"`
|
||||
Version int `db:"version"`
|
||||
}
|
||||
|
||||
Tasks []*Task
|
||||
|
||||
UpdateTaskParams struct {
|
||||
ExpectedVersion int
|
||||
Name *string
|
||||
Description *string
|
||||
State *TaskState
|
||||
}
|
||||
)
|
||||
|
||||
func (t Task) CursorKey() page.CursorKey {
|
||||
return page.NewCursorKey(t.ID, t.CreatedAt)
|
||||
}
|
||||
|
||||
func (t *Task) LoadByID(
|
||||
ctx context.Context,
|
||||
conn pg.Conn,
|
||||
scope Scoper,
|
||||
taskID gid.GID,
|
||||
) error {
|
||||
q := `
|
||||
SELECT
|
||||
id,
|
||||
control_id,
|
||||
name,
|
||||
description,
|
||||
state,
|
||||
content_ref,
|
||||
created_at,
|
||||
updated_at,
|
||||
version
|
||||
FROM
|
||||
tasks
|
||||
WHERE
|
||||
%s
|
||||
AND task_id = @task_id
|
||||
LIMIT 1;
|
||||
`
|
||||
|
||||
q = fmt.Sprintf(q, scope.SQLFragment())
|
||||
|
||||
args := pgx.StrictNamedArgs{"task_id": taskID}
|
||||
maps.Copy(args, scope.SQLArguments())
|
||||
|
||||
rows, err := conn.Query(ctx, q, args)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot query task: %w", err)
|
||||
}
|
||||
|
||||
task, err := pgx.CollectExactlyOneRow(rows, pgx.RowToStructByName[Task])
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot collect task: %w", err)
|
||||
}
|
||||
|
||||
*t = task
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t Task) Insert(
|
||||
ctx context.Context,
|
||||
conn pg.Conn,
|
||||
scope Scoper,
|
||||
) error {
|
||||
q := `
|
||||
INSERT INTO tasks (
|
||||
tenant_id,
|
||||
id,
|
||||
name,
|
||||
control_id,
|
||||
description,
|
||||
content_ref,
|
||||
created_at,
|
||||
updated_at,
|
||||
version,
|
||||
state
|
||||
)
|
||||
VALUES (
|
||||
@tenant_id,
|
||||
@task_id,
|
||||
@name,
|
||||
@control_id,
|
||||
@description,
|
||||
@content_ref,
|
||||
@created_at,
|
||||
@updated_at,
|
||||
@version,
|
||||
@state
|
||||
);
|
||||
`
|
||||
|
||||
args := pgx.StrictNamedArgs{
|
||||
"tenant_id": scope.GetTenantID(),
|
||||
"task_id": t.ID,
|
||||
"control_id": t.ControlID,
|
||||
"name": t.Name,
|
||||
"description": t.Description,
|
||||
"content_ref": t.ContentRef,
|
||||
"created_at": t.CreatedAt,
|
||||
"updated_at": t.UpdatedAt,
|
||||
"version": t.Version,
|
||||
"state": t.State,
|
||||
}
|
||||
_, err := conn.Exec(ctx, q, args)
|
||||
return err
|
||||
}
|
||||
|
||||
func (t *Tasks) LoadByControlID(
|
||||
ctx context.Context,
|
||||
conn pg.Conn,
|
||||
scope Scoper,
|
||||
controlID gid.GID,
|
||||
cursor *page.Cursor,
|
||||
) error {
|
||||
q := `
|
||||
SELECT
|
||||
id,
|
||||
control_id,
|
||||
name,
|
||||
description,
|
||||
state,
|
||||
content_ref,
|
||||
created_at,
|
||||
updated_at,
|
||||
version
|
||||
FROM
|
||||
tasks
|
||||
WHERE
|
||||
%s
|
||||
AND control_id = @control_id
|
||||
AND %s
|
||||
`
|
||||
|
||||
q = fmt.Sprintf(q, scope.SQLFragment(), cursor.SQLFragment())
|
||||
|
||||
args := pgx.StrictNamedArgs{"control_id": controlID}
|
||||
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 tasks: %w", err)
|
||||
}
|
||||
|
||||
tasks, err := pgx.CollectRows(rows, pgx.RowToAddrOfStructByName[Task])
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot collect tasks: %w", err)
|
||||
}
|
||||
|
||||
*t = tasks
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *Task) Update(
|
||||
ctx context.Context,
|
||||
conn pg.Conn,
|
||||
scope Scoper,
|
||||
params UpdateTaskParams,
|
||||
) error {
|
||||
q := `
|
||||
UPDATE tasks
|
||||
SET
|
||||
name = COALESCE(@name, name),
|
||||
description = COALESCE(@description, description),
|
||||
state = COALESCE(@state, state),
|
||||
updated_at = @updated_at,
|
||||
version = version + 1
|
||||
WHERE
|
||||
%s
|
||||
AND id = @task_id
|
||||
AND version = @expected_version
|
||||
RETURNING
|
||||
version;
|
||||
`
|
||||
q = fmt.Sprintf(q, scope.SQLFragment())
|
||||
|
||||
args := pgx.StrictNamedArgs{
|
||||
"task_id": t.ID,
|
||||
"expected_version": params.ExpectedVersion,
|
||||
"name": params.Name,
|
||||
"description": params.Description,
|
||||
"state": params.State,
|
||||
"updated_at": time.Now(),
|
||||
}
|
||||
maps.Copy(args, scope.SQLArguments())
|
||||
|
||||
err := conn.QueryRow(ctx, q, args).Scan(&t.Version)
|
||||
return err
|
||||
}
|
||||
|
||||
func (t *Task) Delete(
|
||||
ctx context.Context,
|
||||
conn pg.Conn,
|
||||
scope Scoper,
|
||||
) error {
|
||||
q := `
|
||||
WITH control_count AS (
|
||||
SELECT COUNT(*) AS count FROM controls_tasks WHERE task_id = @task_id
|
||||
),
|
||||
delete_link AS (
|
||||
DELETE FROM controls_tasks
|
||||
WHERE task_id = @task_id AND control_id = @control_id
|
||||
RETURNING task_id
|
||||
),
|
||||
delete_transitions AS (
|
||||
DELETE FROM task_state_transitions
|
||||
WHERE %s AND task_id = @task_id AND (SELECT count FROM control_count) <= 1
|
||||
RETURNING task_id
|
||||
),
|
||||
delete_all_links AS (
|
||||
DELETE FROM controls_tasks
|
||||
WHERE task_id = @task_id AND (SELECT count FROM control_count) <= 1
|
||||
RETURNING task_id
|
||||
),
|
||||
delete_task AS (
|
||||
DELETE FROM tasks
|
||||
WHERE %s AND id = @task_id AND (SELECT count FROM control_count) <= 1
|
||||
RETURNING id
|
||||
)
|
||||
SELECT
|
||||
(SELECT count FROM control_count) AS control_count,
|
||||
(SELECT COUNT(*) FROM delete_link) AS deleted_links,
|
||||
(SELECT COUNT(*) FROM delete_transitions) AS deleted_transitions,
|
||||
(SELECT COUNT(*) FROM delete_all_links) AS deleted_all_links,
|
||||
(SELECT COUNT(*) FROM delete_task) AS deleted_tasks;
|
||||
`
|
||||
q = fmt.Sprintf(q, scope.SQLFragment(), scope.SQLFragment())
|
||||
|
||||
args := pgx.StrictNamedArgs{
|
||||
"task_id": t.ID,
|
||||
"control_id": t.ControlID,
|
||||
}
|
||||
maps.Copy(args, scope.SQLArguments())
|
||||
|
||||
var controlCount, deletedLinks, deletedTransitions, deletedAllLinks, deletedTasks int
|
||||
err := conn.QueryRow(ctx, q, args).Scan(
|
||||
&controlCount,
|
||||
&deletedLinks,
|
||||
&deletedTransitions,
|
||||
&deletedAllLinks,
|
||||
&deletedTasks,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot execute delete operation: %w", err)
|
||||
}
|
||||
|
||||
if controlCount <= 1 {
|
||||
if deletedTransitions == 0 || deletedAllLinks == 0 || deletedTasks == 0 {
|
||||
return fmt.Errorf("failed to delete task completely: transitions=%d, links=%d, tasks=%d",
|
||||
deletedTransitions, deletedAllLinks, deletedTasks)
|
||||
}
|
||||
} else {
|
||||
if deletedLinks == 0 {
|
||||
return fmt.Errorf("failed to delete control-task link")
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
74
pkg/coredata/task_state.go
Normal file
74
pkg/coredata/task_state.go
Normal file
@@ -0,0 +1,74 @@
|
||||
// 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 (
|
||||
"database/sql/driver"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
type (
|
||||
TaskState uint8
|
||||
)
|
||||
|
||||
const (
|
||||
TaskStateTodo TaskState = iota
|
||||
TaskStateDone
|
||||
)
|
||||
|
||||
func (ts TaskState) MarshalText() ([]byte, error) {
|
||||
return []byte(ts.String()), nil
|
||||
}
|
||||
|
||||
func (ts *TaskState) UnmarshalText(data []byte) error {
|
||||
val := string(data)
|
||||
|
||||
switch val {
|
||||
case TaskStateTodo.String():
|
||||
*ts = TaskStateTodo
|
||||
case TaskStateDone.String():
|
||||
*ts = TaskStateDone
|
||||
default:
|
||||
return fmt.Errorf("invalid TaskState value: %q", val)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ts TaskState) String() string {
|
||||
var val string
|
||||
|
||||
switch ts {
|
||||
case TaskStateTodo:
|
||||
val = "TODO"
|
||||
case TaskStateDone:
|
||||
val = "DONE"
|
||||
}
|
||||
|
||||
return val
|
||||
}
|
||||
|
||||
func (ts *TaskState) Scan(value any) error {
|
||||
val, ok := value.(string)
|
||||
if !ok {
|
||||
return fmt.Errorf("invalid scan source for TaskState, expected string got %T", value)
|
||||
}
|
||||
|
||||
return ts.UnmarshalText([]byte(val))
|
||||
}
|
||||
|
||||
func (ts TaskState) Value() (driver.Value, error) {
|
||||
return ts.String(), nil
|
||||
}
|
||||
145
pkg/coredata/user.go
Normal file
145
pkg/coredata/user.go
Normal file
@@ -0,0 +1,145 @@
|
||||
// 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 (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/getprobo/probo/pkg/gid"
|
||||
"github.com/getprobo/probo/pkg/page"
|
||||
"github.com/jackc/pgx/v5"
|
||||
"go.gearno.de/kit/pg"
|
||||
)
|
||||
|
||||
type (
|
||||
User struct {
|
||||
ID gid.GID `db:"id"`
|
||||
EmailAddress string `db:"email_address"`
|
||||
HashedPassword []byte `db:"hashed_password"`
|
||||
FullName string `db:"fullname"`
|
||||
CreatedAt time.Time `db:"created_at"`
|
||||
UpdatedAt time.Time `db:"updated_at"`
|
||||
}
|
||||
)
|
||||
|
||||
func (u User) CursorKey() page.CursorKey {
|
||||
return page.NewCursorKey(u.ID, u.CreatedAt)
|
||||
}
|
||||
|
||||
func (u *User) LoadByEmail(
|
||||
ctx context.Context,
|
||||
conn pg.Conn,
|
||||
email string,
|
||||
) error {
|
||||
q := `
|
||||
SELECT
|
||||
id,
|
||||
email_address,
|
||||
hashed_password,
|
||||
fullname,
|
||||
created_at,
|
||||
updated_at
|
||||
FROM
|
||||
usrmgr_users
|
||||
WHERE
|
||||
email_address = @user_email
|
||||
LIMIT 1;
|
||||
`
|
||||
|
||||
args := pgx.StrictNamedArgs{"user_email": email}
|
||||
|
||||
rows, err := conn.Query(ctx, q, args)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot query user: %w", err)
|
||||
}
|
||||
|
||||
user, err := pgx.CollectExactlyOneRow(rows, pgx.RowToStructByName[User])
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot collect user: %w", err)
|
||||
}
|
||||
|
||||
*u = user
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *User) LoadByID(
|
||||
ctx context.Context,
|
||||
conn pg.Conn,
|
||||
userID gid.GID,
|
||||
) error {
|
||||
q := `
|
||||
SELECT
|
||||
id,
|
||||
email_address,
|
||||
hashed_password,
|
||||
fullname,
|
||||
created_at,
|
||||
updated_at
|
||||
FROM
|
||||
usrmgr_users
|
||||
WHERE
|
||||
id = @user_id
|
||||
LIMIT 1;
|
||||
`
|
||||
|
||||
args := pgx.StrictNamedArgs{"user_id": userID}
|
||||
|
||||
rows, err := conn.Query(ctx, q, args)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot query user: %w", err)
|
||||
}
|
||||
|
||||
user, err := pgx.CollectExactlyOneRow(rows, pgx.RowToStructByName[User])
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot collect user: %w", err)
|
||||
}
|
||||
|
||||
*u = user
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *User) Insert(
|
||||
ctx context.Context,
|
||||
conn pg.Conn,
|
||||
) error {
|
||||
q := `
|
||||
INSERT INTO
|
||||
usrmgr_users (id, email_address, hashed_password, fullname, created_at, updated_at)
|
||||
VALUES (
|
||||
@user_id,
|
||||
@email_address,
|
||||
@hashed_password,
|
||||
@fullname,
|
||||
@created_at,
|
||||
@updated_at
|
||||
)
|
||||
`
|
||||
|
||||
args := pgx.StrictNamedArgs{
|
||||
"user_id": u.ID,
|
||||
"email_address": u.EmailAddress,
|
||||
"hashed_password": u.HashedPassword,
|
||||
"fullname": u.FullName,
|
||||
"created_at": u.CreatedAt,
|
||||
"updated_at": u.UpdatedAt,
|
||||
}
|
||||
|
||||
_, err := conn.Exec(ctx, q, args)
|
||||
return err
|
||||
}
|
||||
343
pkg/coredata/vendor.go
Normal file
343
pkg/coredata/vendor.go
Normal file
@@ -0,0 +1,343 @@
|
||||
// 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 (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"maps"
|
||||
"time"
|
||||
|
||||
"github.com/getprobo/probo/pkg/gid"
|
||||
"github.com/getprobo/probo/pkg/page"
|
||||
"github.com/jackc/pgx/v5"
|
||||
"go.gearno.de/kit/pg"
|
||||
)
|
||||
|
||||
var ErrConcurrentModification = errors.New("concurrent modification")
|
||||
|
||||
type (
|
||||
Vendor struct {
|
||||
ID gid.GID `db:"id"`
|
||||
OrganizationID gid.GID `db:"organization_id"`
|
||||
Name string `db:"name"`
|
||||
Description string `db:"description"`
|
||||
ServiceStartAt time.Time `db:"service_start_at"`
|
||||
ServiceTerminationAt *time.Time `db:"service_termination_at"`
|
||||
ServiceCriticality ServiceCriticality `db:"service_criticality"`
|
||||
RiskTier RiskTier `db:"risk_tier"`
|
||||
StatusPageURL *string `db:"status_page_url"`
|
||||
TermsOfServiceURL *string `db:"terms_of_service_url"`
|
||||
PrivacyPolicyURL *string `db:"privacy_policy_url"`
|
||||
CreatedAt time.Time `db:"created_at"`
|
||||
UpdatedAt time.Time `db:"updated_at"`
|
||||
Version int `db:"version"`
|
||||
}
|
||||
|
||||
Vendors []*Vendor
|
||||
|
||||
UpdateVendorParams struct {
|
||||
ExpectedVersion int
|
||||
Name *string
|
||||
Description *string
|
||||
ServiceStartAt *time.Time
|
||||
ServiceTerminationAt *time.Time
|
||||
ServiceCriticality *ServiceCriticality
|
||||
RiskTier *RiskTier
|
||||
StatusPageURL *string
|
||||
TermsOfServiceURL *string
|
||||
PrivacyPolicyURL *string
|
||||
}
|
||||
)
|
||||
|
||||
func (v Vendor) CursorKey() page.CursorKey {
|
||||
return page.NewCursorKey(v.ID, v.CreatedAt)
|
||||
}
|
||||
|
||||
func (v *Vendor) LoadByID(
|
||||
ctx context.Context,
|
||||
conn pg.Conn,
|
||||
scope Scoper,
|
||||
vendorID gid.GID,
|
||||
) error {
|
||||
q := `
|
||||
SELECT
|
||||
id,
|
||||
organization_id,
|
||||
name,
|
||||
description,
|
||||
service_start_at,
|
||||
service_termination_at,
|
||||
service_criticality,
|
||||
risk_tier,
|
||||
status_page_url,
|
||||
terms_of_service_url,
|
||||
privacy_policy_url,
|
||||
created_at,
|
||||
updated_at,
|
||||
version
|
||||
FROM
|
||||
vendors
|
||||
WHERE
|
||||
%s
|
||||
AND id = @vendor_id
|
||||
LIMIT 1;
|
||||
`
|
||||
|
||||
q = fmt.Sprintf(q, scope.SQLFragment())
|
||||
|
||||
args := pgx.StrictNamedArgs{"vendor_id": vendorID}
|
||||
maps.Copy(args, scope.SQLArguments())
|
||||
|
||||
rows, err := conn.Query(ctx, q, args)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot query vendor: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
vendor, err := pgx.CollectExactlyOneRow(rows, pgx.RowToStructByName[Vendor])
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot collect vendor: %w", err)
|
||||
}
|
||||
|
||||
*v = vendor
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v Vendor) Insert(
|
||||
ctx context.Context,
|
||||
conn pg.Conn,
|
||||
scope Scoper,
|
||||
) error {
|
||||
q := `
|
||||
INSERT INTO
|
||||
vendors (
|
||||
tenant_id,
|
||||
id,
|
||||
organization_id,
|
||||
name,
|
||||
description,
|
||||
service_start_at,
|
||||
service_termination_at,
|
||||
service_criticality,
|
||||
risk_tier,
|
||||
status_page_url,
|
||||
terms_of_service_url,
|
||||
privacy_policy_url,
|
||||
created_at,
|
||||
updated_at,
|
||||
version
|
||||
)
|
||||
VALUES (
|
||||
@tenant_id,
|
||||
@vendor_id,
|
||||
@organization_id,
|
||||
@name,
|
||||
@description,
|
||||
@service_start_at,
|
||||
@service_termination_at,
|
||||
@service_criticality,
|
||||
@risk_tier,
|
||||
@status_page_url,
|
||||
@terms_of_service_url,
|
||||
@privacy_policy_url,
|
||||
@created_at,
|
||||
@updated_at,
|
||||
1
|
||||
)
|
||||
`
|
||||
|
||||
args := pgx.StrictNamedArgs{
|
||||
"tenant_id": scope.GetTenantID(),
|
||||
"vendor_id": v.ID,
|
||||
"organization_id": v.OrganizationID,
|
||||
"name": v.Name,
|
||||
"description": v.Description,
|
||||
"service_start_at": v.ServiceStartAt,
|
||||
"service_termination_at": v.ServiceTerminationAt,
|
||||
"service_criticality": v.ServiceCriticality,
|
||||
"risk_tier": v.RiskTier,
|
||||
"status_page_url": v.StatusPageURL,
|
||||
"terms_of_service_url": v.TermsOfServiceURL,
|
||||
"privacy_policy_url": v.PrivacyPolicyURL,
|
||||
"created_at": v.CreatedAt,
|
||||
"updated_at": v.UpdatedAt,
|
||||
}
|
||||
_, err := conn.Exec(ctx, q, args)
|
||||
return err
|
||||
}
|
||||
|
||||
func (v Vendor) Delete(
|
||||
ctx context.Context,
|
||||
conn pg.Conn,
|
||||
scope Scoper,
|
||||
) error {
|
||||
q := `
|
||||
DELETE FROM vendors WHERE %s AND id = @vendor_id
|
||||
`
|
||||
|
||||
q = fmt.Sprintf(q, scope.SQLFragment())
|
||||
|
||||
args := pgx.StrictNamedArgs{"vendor_id": v.ID}
|
||||
maps.Copy(args, scope.SQLArguments())
|
||||
|
||||
_, err := conn.Exec(ctx, q, args)
|
||||
return err
|
||||
}
|
||||
|
||||
func (v *Vendors) LoadByOrganizationID(
|
||||
ctx context.Context,
|
||||
conn pg.Conn,
|
||||
scope Scoper,
|
||||
organizationID gid.GID,
|
||||
cursor *page.Cursor,
|
||||
) error {
|
||||
q := `
|
||||
SELECT
|
||||
id,
|
||||
organization_id,
|
||||
name,
|
||||
description,
|
||||
service_start_at,
|
||||
service_termination_at,
|
||||
service_criticality,
|
||||
risk_tier,
|
||||
status_page_url,
|
||||
terms_of_service_url,
|
||||
privacy_policy_url,
|
||||
created_at,
|
||||
updated_at,
|
||||
version
|
||||
FROM
|
||||
vendors
|
||||
WHERE
|
||||
%s
|
||||
AND organization_id = @organization_id
|
||||
AND %s
|
||||
`
|
||||
|
||||
q = fmt.Sprintf(q, scope.SQLFragment(), cursor.SQLFragment())
|
||||
|
||||
args := pgx.StrictNamedArgs{"organization_id": organizationID}
|
||||
maps.Copy(args, cursor.SQLArguments())
|
||||
maps.Copy(args, scope.SQLArguments())
|
||||
|
||||
rows, err := conn.Query(ctx, q, args)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot query vendors: %w", err)
|
||||
}
|
||||
|
||||
vendors, err := pgx.CollectRows(rows, pgx.RowToAddrOfStructByName[Vendor])
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot collect vendors: %w", err)
|
||||
}
|
||||
|
||||
*v = vendors
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v *Vendor) Update(
|
||||
ctx context.Context,
|
||||
conn pg.Conn,
|
||||
scope Scoper,
|
||||
params UpdateVendorParams,
|
||||
) error {
|
||||
q := `
|
||||
UPDATE vendors SET
|
||||
name = COALESCE(@name, name),
|
||||
description = COALESCE(@description, description),
|
||||
service_start_at = COALESCE(@service_start_at, service_start_at),
|
||||
service_termination_at = COALESCE(@service_termination_at, service_termination_at),
|
||||
service_criticality = COALESCE(@service_criticality, service_criticality),
|
||||
risk_tier = COALESCE(@risk_tier, risk_tier),
|
||||
status_page_url = COALESCE(@status_page_url, status_page_url),
|
||||
terms_of_service_url = COALESCE(@terms_of_service_url, terms_of_service_url),
|
||||
privacy_policy_url = COALESCE(@privacy_policy_url, privacy_policy_url),
|
||||
updated_at = @updated_at,
|
||||
version = version + 1
|
||||
WHERE %s
|
||||
AND id = @vendor_id
|
||||
AND version = @expected_version
|
||||
RETURNING
|
||||
id,
|
||||
organization_id,
|
||||
name,
|
||||
description,
|
||||
service_start_at,
|
||||
service_termination_at,
|
||||
service_criticality,
|
||||
risk_tier,
|
||||
status_page_url,
|
||||
terms_of_service_url,
|
||||
privacy_policy_url,
|
||||
created_at,
|
||||
updated_at,
|
||||
version
|
||||
`
|
||||
q = fmt.Sprintf(q, scope.SQLFragment())
|
||||
|
||||
args := pgx.StrictNamedArgs{
|
||||
"vendor_id": v.ID,
|
||||
"expected_version": params.ExpectedVersion,
|
||||
"updated_at": time.Now(),
|
||||
}
|
||||
|
||||
if params.Name != nil {
|
||||
args["name"] = *params.Name
|
||||
}
|
||||
if params.Description != nil {
|
||||
args["description"] = *params.Description
|
||||
}
|
||||
if params.ServiceStartAt != nil {
|
||||
args["service_start_at"] = *params.ServiceStartAt
|
||||
}
|
||||
if params.ServiceTerminationAt != nil {
|
||||
args["service_termination_at"] = *params.ServiceTerminationAt
|
||||
}
|
||||
if params.ServiceCriticality != nil {
|
||||
args["service_criticality"] = *params.ServiceCriticality
|
||||
}
|
||||
if params.RiskTier != nil {
|
||||
args["risk_tier"] = *params.RiskTier
|
||||
}
|
||||
if params.StatusPageURL != nil {
|
||||
args["status_page_url"] = *params.StatusPageURL
|
||||
}
|
||||
if params.TermsOfServiceURL != nil {
|
||||
args["terms_of_service_url"] = *params.TermsOfServiceURL
|
||||
}
|
||||
if params.PrivacyPolicyURL != nil {
|
||||
args["privacy_policy_url"] = *params.PrivacyPolicyURL
|
||||
}
|
||||
|
||||
maps.Copy(args, scope.SQLArguments())
|
||||
|
||||
rows, err := conn.Query(ctx, q, args)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot query vendor: %w", err)
|
||||
}
|
||||
|
||||
vendor, err := pgx.CollectExactlyOneRow(rows, pgx.RowToStructByName[Vendor])
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot collect vendor: %w", err)
|
||||
}
|
||||
|
||||
*v = vendor
|
||||
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user