Skip to content

Commit dffd52a

Browse files
committed
Add task priority field
Introduce a rank-style priority on tasks, scoped by (organization_id, state). New tasks auto-assign the next priority. Reordering uses the same CTE-based algorithm as trust center references and compliance external URLs. Exposed through GraphQL, MCP, and the PRIORITY order field. Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
1 parent 992c4ab commit dffd52a

File tree

10 files changed

+130
-4
lines changed

10 files changed

+130
-4
lines changed
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
ALTER TABLE tasks ADD COLUMN priority INTEGER;
2+
3+
WITH ranked_tasks AS (
4+
SELECT
5+
id,
6+
ROW_NUMBER() OVER (PARTITION BY organization_id, state ORDER BY created_at DESC, id DESC) AS rn
7+
FROM tasks
8+
)
9+
UPDATE tasks t
10+
SET priority = rt.rn
11+
FROM ranked_tasks rt
12+
WHERE t.id = rt.id;
13+
14+
ALTER TABLE tasks ALTER COLUMN priority SET NOT NULL;
15+
16+
ALTER TABLE tasks
17+
ADD CONSTRAINT tasks_organization_id_state_priority_key
18+
UNIQUE (organization_id, state, priority)
19+
DEFERRABLE INITIALLY DEFERRED;

pkg/coredata/task.go

Lines changed: 79 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ type (
4141
TimeEstimate *time.Duration `db:"time_estimate"`
4242
AssignedToID *gid.GID `db:"assigned_to_profile_id"`
4343
Deadline *time.Time `db:"deadline"`
44+
Priority int `db:"priority"`
4445
CreatedAt time.Time `db:"created_at"`
4546
UpdatedAt time.Time `db:"updated_at"`
4647
}
@@ -50,6 +51,8 @@ type (
5051

5152
func (c Task) CursorKey(orderBy TaskOrderField) page.CursorKey {
5253
switch orderBy {
54+
case TaskOrderFieldPriority:
55+
return page.NewCursorKey(c.ID, c.Priority)
5356
case TaskOrderFieldCreatedAt:
5457
return page.NewCursorKey(c.ID, c.CreatedAt)
5558
}
@@ -89,6 +92,7 @@ SELECT
8992
time_estimate,
9093
assigned_to_profile_id,
9194
deadline,
95+
priority,
9296
created_at,
9397
updated_at
9498
FROM
@@ -141,6 +145,7 @@ SELECT
141145
time_estimate,
142146
assigned_to_profile_id,
143147
deadline,
148+
priority,
144149
created_at,
145150
updated_at
146151
FROM
@@ -170,12 +175,17 @@ WHERE
170175
return nil
171176
}
172177

173-
func (c Task) Insert(
178+
func (c *Task) Insert(
174179
ctx context.Context,
175180
conn pg.Conn,
176181
scope Scoper,
177182
) error {
178183
q := `
184+
WITH next_priority AS (
185+
SELECT COALESCE(MAX(priority), 0) + 1 AS value
186+
FROM tasks
187+
WHERE organization_id = @organization_id AND state = @state
188+
)
179189
INSERT INTO
180190
tasks (
181191
tenant_id,
@@ -189,6 +199,7 @@ INSERT INTO
189199
time_estimate,
190200
assigned_to_profile_id,
191201
deadline,
202+
priority,
192203
created_at,
193204
updated_at
194205
)
@@ -204,9 +215,11 @@ VALUES (
204215
@time_estimate,
205216
@assigned_to_profile_id,
206217
@deadline,
218+
(SELECT value FROM next_priority),
207219
@created_at,
208220
@updated_at
209-
);
221+
)
222+
RETURNING priority;
210223
`
211224

212225
args := pgx.StrictNamedArgs{
@@ -224,8 +237,8 @@ VALUES (
224237
"created_at": c.CreatedAt,
225238
"updated_at": c.UpdatedAt,
226239
}
227-
_, err := conn.Exec(ctx, q, args)
228240

241+
err := conn.QueryRow(ctx, q, args).Scan(&c.Priority)
229242
if err != nil {
230243
var pgErr *pgconn.PgError
231244
if errors.As(err, &pgErr) {
@@ -245,6 +258,11 @@ func (c *Task) Upsert(
245258
scope Scoper,
246259
) error {
247260
q := `
261+
WITH next_priority AS (
262+
SELECT COALESCE(MAX(priority), 0) + 1 AS value
263+
FROM tasks
264+
WHERE organization_id = @organization_id AND state = @state
265+
)
248266
INSERT INTO
249267
tasks (
250268
tenant_id,
@@ -258,6 +276,7 @@ INSERT INTO
258276
time_estimate,
259277
assigned_to_profile_id,
260278
deadline,
279+
priority,
261280
created_at,
262281
updated_at
263282
)
@@ -273,6 +292,7 @@ VALUES (
273292
@time_estimate,
274293
@assigned_to_profile_id,
275294
@deadline,
295+
(SELECT value FROM next_priority),
276296
@created_at,
277297
@updated_at
278298
)
@@ -292,6 +312,7 @@ RETURNING
292312
time_estimate,
293313
assigned_to_profile_id,
294314
deadline,
315+
priority,
295316
created_at,
296317
updated_at
297318
`
@@ -377,6 +398,7 @@ func (c *Tasks) LoadByOrganizationID(
377398
time_estimate,
378399
assigned_to_profile_id,
379400
deadline,
401+
priority,
380402
created_at,
381403
updated_at
382404
FROM
@@ -458,6 +480,7 @@ SELECT
458480
time_estimate,
459481
assigned_to_profile_id,
460482
deadline,
483+
priority,
461484
created_at,
462485
updated_at
463486
FROM
@@ -525,6 +548,59 @@ WHERE %s
525548
return err
526549
}
527550

551+
func (c *Task) UpdatePriority(
552+
ctx context.Context,
553+
conn pg.Conn,
554+
scope Scoper,
555+
) error {
556+
q := `
557+
WITH old AS (
558+
SELECT
559+
priority AS old_priority
560+
FROM tasks
561+
WHERE %s AND id = @id AND organization_id = @organization_id AND state = @state
562+
)
563+
564+
UPDATE tasks
565+
SET
566+
priority = CASE
567+
WHEN id = @id THEN @new_priority
568+
ELSE priority + CASE
569+
WHEN @new_priority < old.old_priority THEN 1
570+
WHEN @new_priority > old.old_priority THEN -1
571+
END
572+
END,
573+
updated_at = @updated_at
574+
FROM old
575+
WHERE %s
576+
AND organization_id = @organization_id
577+
AND state = @state
578+
AND (
579+
id = @id
580+
OR (priority BETWEEN LEAST(old.old_priority, @new_priority) AND GREATEST(old.old_priority, @new_priority))
581+
);
582+
`
583+
584+
scopeFragment := scope.SQLFragment()
585+
q = fmt.Sprintf(q, scopeFragment, scopeFragment)
586+
587+
args := pgx.StrictNamedArgs{
588+
"id": c.ID,
589+
"new_priority": c.Priority,
590+
"organization_id": c.OrganizationID,
591+
"state": c.State,
592+
"updated_at": c.UpdatedAt,
593+
}
594+
maps.Copy(args, scope.SQLArguments())
595+
596+
_, err := conn.Exec(ctx, q, args)
597+
if err != nil {
598+
return fmt.Errorf("cannot update task priority: %w", err)
599+
}
600+
601+
return nil
602+
}
603+
528604
func (c *Task) Delete(
529605
ctx context.Context,
530606
conn pg.Conn,

pkg/coredata/task_order_field.go

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,19 @@ type (
1919
)
2020

2121
const (
22+
TaskOrderFieldPriority TaskOrderField = "PRIORITY"
2223
TaskOrderFieldCreatedAt TaskOrderField = "CREATED_AT"
2324
)
2425

2526
func (p TaskOrderField) Column() string {
26-
return string(p)
27+
switch p {
28+
case TaskOrderFieldPriority:
29+
return "priority"
30+
case TaskOrderFieldCreatedAt:
31+
return "created_at"
32+
default:
33+
return string(p)
34+
}
2735
}
2836

2937
func (p TaskOrderField) String() string {

pkg/probo/task_service.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ type (
5151
Deadline **time.Time
5252
AssignedToID **gid.GID
5353
MeasureID **gid.GID
54+
Priority *int
5455
}
5556
)
5657

@@ -319,6 +320,13 @@ func (s TaskService) Update(
319320

320321
task.UpdatedAt = time.Now()
321322

323+
if req.Priority != nil {
324+
task.Priority = *req.Priority
325+
if err := task.UpdatePriority(ctx, conn, s.svc.scope); err != nil {
326+
return fmt.Errorf("cannot update task priority: %w", err)
327+
}
328+
}
329+
322330
if err := task.Update(ctx, conn, s.svc.scope); err != nil {
323331
return fmt.Errorf("cannot update task: %w", err)
324332
}

pkg/server/api/console/v1/schema.graphql

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -499,6 +499,7 @@ enum MeasureOrderField
499499

500500
enum TaskOrderField
501501
@goModel(model: "go.probo.inc/probo/pkg/coredata.TaskOrderField") {
502+
PRIORITY
502503
CREATED_AT
503504
}
504505

@@ -2392,6 +2393,7 @@ type Task implements Node {
23922393
name: String!
23932394
description: String
23942395
state: TaskState!
2396+
priority: Int!
23952397
timeEstimate: Duration
23962398
deadline: Datetime
23972399
assignedTo: Profile @goField(forceResolver: true)
@@ -4211,6 +4213,7 @@ input UpdateTaskInput {
42114213
name: String
42124214
description: String @goField(omittable: true)
42134215
state: TaskState
4216+
priority: Int
42144217
timeEstimate: Duration @goField(omittable: true)
42154218
deadline: Datetime @goField(omittable: true)
42164219
assignedToId: ID @goField(omittable: true)

pkg/server/api/console/v1/types/task.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ func NewTask(t *coredata.Task) *Task {
7070
Name: t.Name,
7171
Description: t.Description,
7272
State: t.State,
73+
Priority: t.Priority,
7374
TimeEstimate: t.TimeEstimate,
7475
Deadline: t.Deadline,
7576
CreatedAt: t.CreatedAt,

pkg/server/api/console/v1/v1_resolver.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3766,6 +3766,7 @@ func (r *mutationResolver) UpdateTask(ctx context.Context, input types.UpdateTas
37663766
Name: input.Name,
37673767
Description: gqlutils.UnwrapOmittable(input.Description),
37683768
State: input.State,
3769+
Priority: input.Priority,
37693770
TimeEstimate: gqlutils.UnwrapOmittable(input.TimeEstimate),
37703771
Deadline: gqlutils.UnwrapOmittable(input.Deadline),
37713772
AssignedToID: gqlutils.UnwrapOmittable(input.AssignedToID),

pkg/server/api/mcp/v1/schema.resolvers.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1904,6 +1904,7 @@ func (r *Resolver) UpdateTaskTool(ctx context.Context, req *mcp.CallToolRequest,
19041904
Name: input.Name,
19051905
Description: UnwrapOmittable(input.Description),
19061906
State: input.State,
1907+
Priority: input.Priority,
19071908
TimeEstimate: UnwrapOmittable(input.TimeEstimate),
19081909
Deadline: UnwrapOmittable(input.Deadline),
19091910
AssignedToID: UnwrapOmittable(input.AssignedToID),

pkg/server/api/mcp/v1/specification.yaml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4619,6 +4619,7 @@ components:
46194619
TaskOrderField:
46204620
type: string
46214621
enum:
4622+
- PRIORITY
46224623
- CREATED_AT
46234624
go.probo.inc/mcpgen/type: go.probo.inc/probo/pkg/coredata.TaskOrderField
46244625

@@ -4642,6 +4643,7 @@ components:
46424643
- organization_id
46434644
- name
46444645
- state
4646+
- priority
46454647
- created_at
46464648
- updated_at
46474649
properties:
@@ -4671,6 +4673,9 @@ components:
46714673
state:
46724674
$ref: "#/components/schemas/TaskState"
46734675
description: Task state
4676+
priority:
4677+
type: integer
4678+
description: Task priority within state
46744679
time_estimate:
46754680
anyOf:
46764681
- $ref: "#/components/schemas/Duration"
@@ -4822,6 +4827,9 @@ components:
48224827
- type: "null"
48234828
description: No state
48244829
description: Task state
4830+
priority:
4831+
type: integer
4832+
description: Task priority within state
48254833
time_estimate:
48264834
anyOf:
48274835
- $ref: "#/components/schemas/Duration"

pkg/server/api/mcp/v1/types/task.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ func NewTask(t *coredata.Task) *Task {
2525
Name: t.Name,
2626
Description: t.Description,
2727
State: t.State,
28+
Priority: t.Priority,
2829
TimeEstimate: t.TimeEstimate,
2930
CreatedAt: t.CreatedAt,
3031
UpdatedAt: t.UpdatedAt,

0 commit comments

Comments
 (0)