Move coredata outside probo service

Signed-off-by: gearnode <bryan@frimin.fr>
This commit is contained in:
gearnode
2025-03-11 09:38:36 +01:00
parent 3c1a0c9d6d
commit 5bdc474aef
113 changed files with 119 additions and 166 deletions

268
pkg/coredata/control.go Normal file
View 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
}

View 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
}

View 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
View 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
}

View 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
View 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
}

View 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
)

View 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
);

View File

@@ -0,0 +1 @@
ALTER TABLE organizations DROP COLUMN description;

View 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);

View 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
);

View 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)
);

View 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
);

View 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
);

View 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
);

View File

@@ -0,0 +1,6 @@
CREATE TYPE task_state AS ENUM (
'TODO',
'DONE'
);
ALTER TABLE tasks ADD COLUMN state task_state NOT NULL;

View 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;

View File

@@ -0,0 +1,3 @@
ALTER TABLE evidences
ADD COLUMN mime_type TEXT NOT NULL,
ADD COLUMN size NUMERIC NOT NULL;

View 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
);

View 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;

View File

@@ -0,0 +1 @@
ALTER TABLE vendors ADD COLUMN organization_id TEXT REFERENCES organizations(id) NOT NULL;

View 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
);

View 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;

View File

@@ -0,0 +1 @@
ALTER TABLE usrmgr_users ADD UNIQUE (email_address);

View File

@@ -0,0 +1 @@
ALTER TABLE usrmgr_sessions ADD COLUMN expired_at TIMESTAMP WITH TIME ZONE NOT NULL;

View 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;

View 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;

View 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;

View 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;

View File

@@ -0,0 +1,2 @@
ALTER TABLE vendors ADD COLUMN description TEXT NOT NULL DEFAULT '';
ALTER TABLE vendors ALTER COLUMN description DROP DEFAULT;

View File

@@ -0,0 +1 @@
ALTER TABLE vendors ADD COLUMN terms_of_service_url TEXT;

View File

@@ -0,0 +1 @@
ALTER TABLE vendors ADD COLUMN privacy_policy_url TEXT;

View 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;

View File

@@ -0,0 +1 @@
ALTER TABLE vendors ADD COLUMN version INTEGER NOT NULL DEFAULT 1;

View File

@@ -0,0 +1,2 @@
ALTER TABLE evidences ADD COLUMN filename TEXT NOT NULL DEFAULT '';
ALTER TABLE evidences ALTER COLUMN filename DROP DEFAULT;

View File

@@ -0,0 +1 @@
ALTER TABLE peoples ADD COLUMN version INTEGER NOT NULL DEFAULT 1;

View 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);

View 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;

View File

@@ -0,0 +1 @@
ALTER TABLE usrmgr_users ADD COLUMN fullname TEXT;

View File

@@ -0,0 +1 @@
ALTER TABLE usrmgr_users ALTER COLUMN fullname SET NOT NULL;

View File

@@ -0,0 +1,2 @@
ALTER TABLE frameworks ADD COLUMN version INTEGER NOT NULL DEFAULT 0;
ALTER TABLE frameworks ALTER COLUMN version DROP DEFAULT;

View File

@@ -0,0 +1,2 @@
ALTER TABLE controls ADD COLUMN version INTEGER NOT NULL DEFAULT 0;
ALTER TABLE controls ALTER COLUMN version DROP DEFAULT;

View 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
);

View File

@@ -0,0 +1 @@
ALTER TABLE policies ADD COLUMN review_date TIMESTAMP WITH TIME ZONE;

View File

@@ -0,0 +1 @@
ALTER TABLE policies ADD COLUMN owner_id TEXT REFERENCES peoples(id) NOT NULL;

View 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;

View File

@@ -0,0 +1 @@
ALTER TABLE usrmgr_sessions ADD COLUMN data jsonb NOT NULL DEFAULT '{}';

View 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;

View 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;

View File

@@ -0,0 +1 @@
ALTER TABLE tasks ADD COLUMN version INTEGER NOT NULL DEFAULT 1;

View 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
View 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
}

View 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
View 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
}

View 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
View 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
View 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
}

View 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
View 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
View 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
}

View 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
View 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
View 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
}