@managemint-solutions/sdk 0.42.0 → 0.43.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +3 -1
- package/dist/tasks/dto.d.ts +10 -1
- package/dist/tasks/dto.js +29 -1
- package/dist/tasks/index.d.ts +15 -1
- package/dist/tasks/index.js +152 -12
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -202,10 +202,13 @@ await supabase.tasks.archive(task.mms_id);
|
|
|
202
202
|
await supabase.tasks.unarchive(task.mms_id);
|
|
203
203
|
await supabase.tasks.remove(task.mms_id); // soft delete
|
|
204
204
|
await supabase.tasks.restore(task.mms_id);
|
|
205
|
+
await supabase.tasks.linkTask(task.mms_id, { linked_task_id: otherId, link_type: TaskLinkType.CHILD });
|
|
206
|
+
await supabase.tasks.unlinkTask(task.mms_id, otherId);
|
|
205
207
|
```
|
|
206
208
|
|
|
207
209
|
A task's `task_id` (its per-organization number) is assigned by the database on insert, and each
|
|
208
|
-
task carries its embedded `status`, `assigned_to`, `project` and `client`.
|
|
210
|
+
task carries its embedded `status`, `assigned_to`, `project` and `client`. The detail read also carries its `parent`,
|
|
211
|
+
`subtasks` and `related` tasks, read from `task_links` in both directions.
|
|
209
212
|
|
|
210
213
|
The client decodes `{ mms_id, organization_id }` from the JWT itself and scopes every query
|
|
211
214
|
by `organization_id`. `supabase.supabase` is the raw `@supabase/supabase-js` client
|
package/dist/index.d.ts
CHANGED
|
@@ -36,7 +36,7 @@ export type { ClaimOutcome, WebhookEventRow, WebhookEventsTable } from './webhoo
|
|
|
36
36
|
export { GetSupportingFilesDto, SupportingFileIdDto } from './supporting-files';
|
|
37
37
|
export { ClientIdDto, CreateClientDto, GetClientsDto, UpdateClientDto } from './clients';
|
|
38
38
|
export { CreateProjectDto, GetProjectsDto, ProjectIdDto, UpdateProjectDto } from './projects';
|
|
39
|
-
export { CreateTaskDto, GetTasksDto, TaskIdDto, UpdateTaskDto } from './tasks';
|
|
39
|
+
export { AddTaskLinkDto, CreateTaskDto, GetTasksDto, TaskIdDto, TaskLinkIdDto, UpdateTaskDto } from './tasks';
|
|
40
40
|
export type { CreateLeaveEntitlementInput, CreateLeaveRequestRow, LeaveEntitlementCover, ReviewLeaveRequestRow, UpdateLeaveRequestRow, } from './leave';
|
|
41
41
|
export { CreateLeaveEntitlementDto, CreateLeaveRequestDto, CreateLeaveTypeDto, GetLeaveEntitlementsDto, GetLeaveRequestsDto, GetLeaveTypesDto, LeaveEntitlementIdDto, LeaveRequestIdDto, LeaveTypeIdDto, ReviewLeaveRequestDto, UpdateLeaveEntitlementDto, UpdateLeaveRequestDto, UpdateLeaveTypeDto, } from './leave';
|
|
42
42
|
export { GetPublicHolidaysDto } from './public-holidays';
|
package/dist/index.js
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
// to keep. The row and input types they produce and accept are exported for annotations.
|
|
9
9
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
10
|
exports.AddUserDto = exports.UpdatePermissionsDto = exports.PermissionsResource = exports.UpdateOrganizationDto = exports.CreateOrganizationDto = exports.NotificationIdDto = exports.GetNotificationsDto = exports.NoteIdDto = exports.GetNotesDto = exports.AddNoteDto = exports.UpdateAdditionalDetailsDto = exports.IsAdditionalDetailsValueByType = exports.GetAdditionalDetailsDto = exports.AdditionalDetailsIdDto = exports.AddAdditionalDetailsDto = exports.UpdateLabelDto = exports.LabelIdDto = exports.GetLabelsDto = exports.AddLabelDto = exports.UpdateStatusDto = exports.StatusIdDto = exports.GetStatusesDto = exports.AddStatusDto = exports.CreateSupportRequestDto = exports.UpdateConfigDto = exports.CreateConfigDto = exports.CreateFeatureRequestDto = exports.InvoiceIdDto = exports.GetInvoicesQueryDto = exports.ReinstateSubscriptionDto = exports.PurchaseModuleDto = exports.PaymentIdDto = exports.InitialPaymentDto = exports.GetPaymentsQueryDto = exports.CancelSubscriptionDto = exports.GetAuditTrailDto = exports.IsNullable = exports.EmptyToNull = exports.SupabaseClientError = exports.createSupabaseServiceClient = exports.SupabaseServiceClient = exports.SignInDto = exports.SendPasswordResetEmailDto = exports.ResetPasswordDto = exports.ExchangeTokenHashDto = exports.ExchangeRefreshTokenDto = exports.createSupabaseAuthClient = exports.SupabaseAuthClient = exports.createSupabaseClient = exports.SupabaseClient = void 0;
|
|
11
|
-
exports.monthlyTotalMinor = exports.weekStartOf = exports.minutesBetween = exports.UpdateTimesheetEntryDto = exports.TimesheetIdDto = exports.TimesheetEntryIdDto = exports.ReviewTimesheetDto = exports.GetTimesheetsDto = exports.GetTimesheetReportDto = exports.CreateTimesheetEntryDto = exports.todayInJohannesburg = exports.shiftDays = exports.leavePeriodFor = exports.isWithinPeriod = exports.countWorkingDays = exports.addOneYearMinusDay = exports.GetPublicHolidaysDto = exports.UpdateLeaveTypeDto = exports.UpdateLeaveRequestDto = exports.UpdateLeaveEntitlementDto = exports.ReviewLeaveRequestDto = exports.LeaveTypeIdDto = exports.LeaveRequestIdDto = exports.LeaveEntitlementIdDto = exports.GetLeaveTypesDto = exports.GetLeaveRequestsDto = exports.GetLeaveEntitlementsDto = exports.CreateLeaveTypeDto = exports.CreateLeaveRequestDto = exports.CreateLeaveEntitlementDto = exports.UpdateTaskDto = exports.TaskIdDto = exports.GetTasksDto = exports.CreateTaskDto = exports.UpdateProjectDto = exports.ProjectIdDto = exports.GetProjectsDto = exports.CreateProjectDto = exports.UpdateClientDto = exports.GetClientsDto = exports.CreateClientDto = exports.ClientIdDto = exports.SupportingFileIdDto = exports.GetSupportingFilesDto = exports.UserIdDto = exports.UpdateUserDto = exports.UpdateMyProfileDto = exports.GetUsersQueryDto = void 0;
|
|
11
|
+
exports.monthlyTotalMinor = exports.weekStartOf = exports.minutesBetween = exports.UpdateTimesheetEntryDto = exports.TimesheetIdDto = exports.TimesheetEntryIdDto = exports.ReviewTimesheetDto = exports.GetTimesheetsDto = exports.GetTimesheetReportDto = exports.CreateTimesheetEntryDto = exports.todayInJohannesburg = exports.shiftDays = exports.leavePeriodFor = exports.isWithinPeriod = exports.countWorkingDays = exports.addOneYearMinusDay = exports.GetPublicHolidaysDto = exports.UpdateLeaveTypeDto = exports.UpdateLeaveRequestDto = exports.UpdateLeaveEntitlementDto = exports.ReviewLeaveRequestDto = exports.LeaveTypeIdDto = exports.LeaveRequestIdDto = exports.LeaveEntitlementIdDto = exports.GetLeaveTypesDto = exports.GetLeaveRequestsDto = exports.GetLeaveEntitlementsDto = exports.CreateLeaveTypeDto = exports.CreateLeaveRequestDto = exports.CreateLeaveEntitlementDto = exports.UpdateTaskDto = exports.TaskLinkIdDto = exports.TaskIdDto = exports.GetTasksDto = exports.CreateTaskDto = exports.AddTaskLinkDto = exports.UpdateProjectDto = exports.ProjectIdDto = exports.GetProjectsDto = exports.CreateProjectDto = exports.UpdateClientDto = exports.GetClientsDto = exports.CreateClientDto = exports.ClientIdDto = exports.SupportingFileIdDto = exports.GetSupportingFilesDto = exports.UserIdDto = exports.UpdateUserDto = exports.UpdateMyProfileDto = exports.GetUsersQueryDto = void 0;
|
|
12
12
|
var client_1 = require("./client");
|
|
13
13
|
Object.defineProperty(exports, "SupabaseClient", { enumerable: true, get: function () { return client_1.SupabaseClient; } });
|
|
14
14
|
Object.defineProperty(exports, "createSupabaseClient", { enumerable: true, get: function () { return client_1.createSupabaseClient; } });
|
|
@@ -100,9 +100,11 @@ Object.defineProperty(exports, "GetProjectsDto", { enumerable: true, get: functi
|
|
|
100
100
|
Object.defineProperty(exports, "ProjectIdDto", { enumerable: true, get: function () { return projects_1.ProjectIdDto; } });
|
|
101
101
|
Object.defineProperty(exports, "UpdateProjectDto", { enumerable: true, get: function () { return projects_1.UpdateProjectDto; } });
|
|
102
102
|
var tasks_1 = require("./tasks");
|
|
103
|
+
Object.defineProperty(exports, "AddTaskLinkDto", { enumerable: true, get: function () { return tasks_1.AddTaskLinkDto; } });
|
|
103
104
|
Object.defineProperty(exports, "CreateTaskDto", { enumerable: true, get: function () { return tasks_1.CreateTaskDto; } });
|
|
104
105
|
Object.defineProperty(exports, "GetTasksDto", { enumerable: true, get: function () { return tasks_1.GetTasksDto; } });
|
|
105
106
|
Object.defineProperty(exports, "TaskIdDto", { enumerable: true, get: function () { return tasks_1.TaskIdDto; } });
|
|
107
|
+
Object.defineProperty(exports, "TaskLinkIdDto", { enumerable: true, get: function () { return tasks_1.TaskLinkIdDto; } });
|
|
106
108
|
Object.defineProperty(exports, "UpdateTaskDto", { enumerable: true, get: function () { return tasks_1.UpdateTaskDto; } });
|
|
107
109
|
var leave_1 = require("./leave");
|
|
108
110
|
Object.defineProperty(exports, "CreateLeaveEntitlementDto", { enumerable: true, get: function () { return leave_1.CreateLeaveEntitlementDto; } });
|
package/dist/tasks/dto.d.ts
CHANGED
|
@@ -1,7 +1,16 @@
|
|
|
1
|
-
import type { CreateTaskDto as CreateTaskDtoType, GetTasksDto as GetTasksDtoType, TaskIdDto as TaskIdDtoType, UpdateTaskDto as UpdateTaskDtoType } from '@managemint-solutions/entities/tasks/dto';
|
|
1
|
+
import type { AddTaskLinkDto as AddTaskLinkDtoType, CreateTaskDto as CreateTaskDtoType, GetTasksDto as GetTasksDtoType, TaskIdDto as TaskIdDtoType, TaskLinkIdDto as TaskLinkIdDtoType, UpdateTaskDto as UpdateTaskDtoType } from '@managemint-solutions/entities/tasks/dto';
|
|
2
|
+
import { TaskLinkType } from '@managemint-solutions/entities/tasks/enum';
|
|
2
3
|
export declare class TaskIdDto implements TaskIdDtoType {
|
|
3
4
|
readonly taskId: string;
|
|
4
5
|
}
|
|
6
|
+
export declare class TaskLinkIdDto implements TaskLinkIdDtoType {
|
|
7
|
+
readonly taskId: string;
|
|
8
|
+
readonly linkedTaskId: string;
|
|
9
|
+
}
|
|
10
|
+
export declare class AddTaskLinkDto implements AddTaskLinkDtoType {
|
|
11
|
+
readonly linked_task_id: string;
|
|
12
|
+
readonly link_type: TaskLinkType;
|
|
13
|
+
}
|
|
5
14
|
export declare class GetTasksDto implements Omit<GetTasksDtoType, 'status_id_in' | 'status_id_not_in' | 'label_id_in' | 'priority_in'> {
|
|
6
15
|
readonly search?: string | null;
|
|
7
16
|
readonly page: number;
|
package/dist/tasks/dto.js
CHANGED
|
@@ -9,9 +9,10 @@ var __metadata = (this && this.__metadata) || function (k, v) {
|
|
|
9
9
|
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
10
10
|
};
|
|
11
11
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
-
exports.UpdateTaskDto = exports.CreateTaskDto = exports.GetTasksDto = exports.TaskIdDto = void 0;
|
|
12
|
+
exports.UpdateTaskDto = exports.CreateTaskDto = exports.GetTasksDto = exports.AddTaskLinkDto = exports.TaskLinkIdDto = exports.TaskIdDto = void 0;
|
|
13
13
|
const class_validator_1 = require("class-validator");
|
|
14
14
|
const class_transformer_1 = require("class-transformer");
|
|
15
|
+
const enum_1 = require("@managemint-solutions/entities/tasks/enum");
|
|
15
16
|
const transforms_1 = require("../transforms");
|
|
16
17
|
const validators_1 = require("../validators");
|
|
17
18
|
// class-validator versions of the shared DTO types from @managemint-solutions/entities, so the
|
|
@@ -25,6 +26,33 @@ __decorate([
|
|
|
25
26
|
,
|
|
26
27
|
__metadata("design:type", String)
|
|
27
28
|
], TaskIdDto.prototype, "taskId", void 0);
|
|
29
|
+
class TaskLinkIdDto {
|
|
30
|
+
taskId;
|
|
31
|
+
linkedTaskId;
|
|
32
|
+
}
|
|
33
|
+
exports.TaskLinkIdDto = TaskLinkIdDto;
|
|
34
|
+
__decorate([
|
|
35
|
+
(0, class_validator_1.IsUUID)('7', { message: 'Invalid task ID format' }),
|
|
36
|
+
__metadata("design:type", String)
|
|
37
|
+
], TaskLinkIdDto.prototype, "taskId", void 0);
|
|
38
|
+
__decorate([
|
|
39
|
+
(0, class_validator_1.IsUUID)('7', { message: 'Invalid linked task ID format' }),
|
|
40
|
+
__metadata("design:type", String)
|
|
41
|
+
], TaskLinkIdDto.prototype, "linkedTaskId", void 0);
|
|
42
|
+
class AddTaskLinkDto {
|
|
43
|
+
linked_task_id;
|
|
44
|
+
// The only check on the value: `task_links.link_type` is plain text.
|
|
45
|
+
link_type;
|
|
46
|
+
}
|
|
47
|
+
exports.AddTaskLinkDto = AddTaskLinkDto;
|
|
48
|
+
__decorate([
|
|
49
|
+
(0, class_validator_1.IsUUID)('7', { message: 'Invalid linked task ID format' }),
|
|
50
|
+
__metadata("design:type", String)
|
|
51
|
+
], AddTaskLinkDto.prototype, "linked_task_id", void 0);
|
|
52
|
+
__decorate([
|
|
53
|
+
(0, class_validator_1.IsEnum)(enum_1.TaskLinkType, { message: 'Link type must be child or related' }),
|
|
54
|
+
__metadata("design:type", String)
|
|
55
|
+
], AddTaskLinkDto.prototype, "link_type", void 0);
|
|
28
56
|
// `status_id_in`, `status_id_not_in`, `label_id_in` and `priority_in` are omitted from the
|
|
29
57
|
// implemented type on purpose: the entities type carries the CSV wire values, while this class
|
|
30
58
|
// carries the `@Transform`-parsed arrays the query builder feeds to `.in('status_id', ...)` /
|
package/dist/tasks/index.d.ts
CHANGED
|
@@ -3,7 +3,7 @@ import type { TaskEntity, TaskSummary } from '@managemint-solutions/entities/tas
|
|
|
3
3
|
import type { AuthContext } from '../auth';
|
|
4
4
|
import { type AuditResource } from '../audit';
|
|
5
5
|
import type { TypedSupabaseClient } from '../client';
|
|
6
|
-
import type { CreateTaskDto, GetTasksDto, UpdateTaskDto } from './dto';
|
|
6
|
+
import type { AddTaskLinkDto, CreateTaskDto, GetTasksDto, UpdateTaskDto } from './dto';
|
|
7
7
|
export * from './dto';
|
|
8
8
|
export declare class TasksResource {
|
|
9
9
|
private readonly supabase;
|
|
@@ -20,6 +20,20 @@ export declare class TasksResource {
|
|
|
20
20
|
restore(taskId: string): Promise<TaskEntity>;
|
|
21
21
|
/** Soft delete - the row stays, flagged `deleted`, and `restore()` brings it back. */
|
|
22
22
|
remove(taskId: string): Promise<void>;
|
|
23
|
+
/**
|
|
24
|
+
* Links `input.linked_task_id` to the task as a subtask or as related. The rules live here,
|
|
25
|
+
* checked before the insert: not the task itself, both tasks this organization's and not
|
|
26
|
+
* deleted, and no loop of subtasks. The two a concurrent write could still break - one link
|
|
27
|
+
* per pair, one parent per task - are unique indexes, and their violations are turned into
|
|
28
|
+
* sentences. The link has no audit of its own: it is one UPDATE entry on the task it was
|
|
29
|
+
* made from.
|
|
30
|
+
*/
|
|
31
|
+
linkTask(taskId: string, input: AddTaskLinkDto): Promise<TaskEntity>;
|
|
32
|
+
/**
|
|
33
|
+
* Removes the link between the two tasks, whichever of them it was made from - a pair carries
|
|
34
|
+
* one link at most. Unlinked from the subtask's side, the entry names the other task as parent.
|
|
35
|
+
*/
|
|
36
|
+
unlinkTask(taskId: string, linkedTaskId: string): Promise<TaskEntity>;
|
|
23
37
|
/**
|
|
24
38
|
* Refuses, before anything is written, a label id that is not one of this organization's task
|
|
25
39
|
* labels - one deleted while the form was open, or another organization's. The `task_labels`
|
package/dist/tasks/index.js
CHANGED
|
@@ -15,8 +15,9 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
|
15
15
|
};
|
|
16
16
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
17
|
exports.TasksResource = void 0;
|
|
18
|
-
const enum_1 = require("@managemint-solutions/entities/
|
|
19
|
-
const enum_2 = require("@managemint-solutions/entities/
|
|
18
|
+
const enum_1 = require("@managemint-solutions/entities/tasks/enum");
|
|
19
|
+
const enum_2 = require("@managemint-solutions/entities/labels/enum");
|
|
20
|
+
const enum_3 = require("@managemint-solutions/entities/audit-trail/enum");
|
|
20
21
|
const audit_1 = require("../audit");
|
|
21
22
|
const errors_1 = require("../errors");
|
|
22
23
|
const transforms_1 = require("../transforms");
|
|
@@ -49,6 +50,15 @@ const TASK_LIST_SELECT = `
|
|
|
49
50
|
project:project_profiles!project_id (*),
|
|
50
51
|
client:client_profiles!client_id (*)
|
|
51
52
|
`;
|
|
53
|
+
// A linked task as the detail read shows it. `deleted` is read only so a soft-deleted task can be
|
|
54
|
+
// left out of the links; it is not part of LinkedTask.
|
|
55
|
+
const LINKED_TASK_FIELDS = `
|
|
56
|
+
mms_id,
|
|
57
|
+
task_number:task_id,
|
|
58
|
+
name,
|
|
59
|
+
priority,
|
|
60
|
+
deleted,
|
|
61
|
+
status:statuses!status_id ( mms_id, colour, label, is_complete, is_fail, is_triage )`;
|
|
52
62
|
const TASK_DETAIL_SELECT = `
|
|
53
63
|
mms_id,
|
|
54
64
|
created_at,
|
|
@@ -87,7 +97,15 @@ const TASK_DETAIL_SELECT = `
|
|
|
87
97
|
deleted_at,
|
|
88
98
|
deleted_by:user_profiles!deleted_by (*),
|
|
89
99
|
restored_at,
|
|
90
|
-
restored_by:user_profiles!restored_by (*)
|
|
100
|
+
restored_by:user_profiles!restored_by (*),
|
|
101
|
+
links_out:task_links!task_id (
|
|
102
|
+
link_type,
|
|
103
|
+
task:tasks!linked_task_id ( ${LINKED_TASK_FIELDS} )
|
|
104
|
+
),
|
|
105
|
+
links_in:task_links!linked_task_id (
|
|
106
|
+
link_type,
|
|
107
|
+
task:tasks!task_id ( ${LINKED_TASK_FIELDS} )
|
|
108
|
+
)
|
|
91
109
|
`;
|
|
92
110
|
// Added to the list select only when `label_id_in` is set: an inner join on the join table is how
|
|
93
111
|
// PostgREST filters a task by the labels it carries.
|
|
@@ -95,6 +113,12 @@ const LABEL_FILTER_SELECT = 'task_labels!inner ( label_id )';
|
|
|
95
113
|
// A `task_labels` row as `setLabels` reads and writes it: the label id, and the name the audit
|
|
96
114
|
// entry records.
|
|
97
115
|
const PINNED_LABEL_SELECT = 'label_id, label:labels ( name )';
|
|
116
|
+
const linkedTasks = (rows, linkType) => rows
|
|
117
|
+
.filter((row) => row.link_type === linkType && row.task && !row.task.deleted)
|
|
118
|
+
.map((row) => {
|
|
119
|
+
const { deleted: _deleted, ...task } = row.task;
|
|
120
|
+
return task;
|
|
121
|
+
});
|
|
98
122
|
class TasksResource {
|
|
99
123
|
supabase;
|
|
100
124
|
auth;
|
|
@@ -106,7 +130,7 @@ class TasksResource {
|
|
|
106
130
|
this.audit = audit;
|
|
107
131
|
this.table = (0, audit_1.auditedTable)(supabase, auth, audit, {
|
|
108
132
|
table: 'tasks',
|
|
109
|
-
entityType:
|
|
133
|
+
entityType: enum_3.AuditTrailEntityType.TASKS,
|
|
110
134
|
});
|
|
111
135
|
}
|
|
112
136
|
async list(query) {
|
|
@@ -196,10 +220,20 @@ class TasksResource {
|
|
|
196
220
|
.select(TASK_DETAIL_SELECT)
|
|
197
221
|
.eq('mms_id', taskId)
|
|
198
222
|
.eq('organization_id', this.auth.organization_id)
|
|
223
|
+
.order('created_at', { referencedTable: 'links_out' })
|
|
224
|
+
.order('created_at', { referencedTable: 'links_in' })
|
|
199
225
|
.single();
|
|
200
226
|
if (error)
|
|
201
227
|
throw (0, errors_1.mapPostgrestError)(error);
|
|
202
|
-
|
|
228
|
+
// Related is stored once, so it is read from both directions; a child link is the parent
|
|
229
|
+
// from the child's side and a subtask from the parent's.
|
|
230
|
+
const { links_out: linksOut, links_in: linksIn, ...task } = data;
|
|
231
|
+
return {
|
|
232
|
+
...task,
|
|
233
|
+
parent: linkedTasks(linksIn, enum_1.TaskLinkType.CHILD)[0] ?? null,
|
|
234
|
+
subtasks: linkedTasks(linksOut, enum_1.TaskLinkType.CHILD),
|
|
235
|
+
related: [...linkedTasks(linksOut, enum_1.TaskLinkType.RELATED), ...linkedTasks(linksIn, enum_1.TaskLinkType.RELATED)],
|
|
236
|
+
};
|
|
203
237
|
}
|
|
204
238
|
async create(input) {
|
|
205
239
|
// `label_ids` is not a tasks column: the labels are join rows, written after the task.
|
|
@@ -237,20 +271,126 @@ class TasksResource {
|
|
|
237
271
|
return this.getById(taskId);
|
|
238
272
|
}
|
|
239
273
|
async archive(taskId) {
|
|
240
|
-
await this.table.transition(taskId,
|
|
274
|
+
await this.table.transition(taskId, enum_3.AuditTrailAction.ARCHIVE, (0, transforms_1.archivedPatch)(this.auth.mms_id));
|
|
241
275
|
return this.getById(taskId);
|
|
242
276
|
}
|
|
243
277
|
async unarchive(taskId) {
|
|
244
|
-
await this.table.transition(taskId,
|
|
278
|
+
await this.table.transition(taskId, enum_3.AuditTrailAction.UNARCHIVE, (0, transforms_1.unarchivedPatch)(this.auth.mms_id));
|
|
245
279
|
return this.getById(taskId);
|
|
246
280
|
}
|
|
247
281
|
async restore(taskId) {
|
|
248
|
-
await this.table.transition(taskId,
|
|
282
|
+
await this.table.transition(taskId, enum_3.AuditTrailAction.RESTORE, (0, transforms_1.restoredPatch)(this.auth.mms_id));
|
|
249
283
|
return this.getById(taskId);
|
|
250
284
|
}
|
|
251
285
|
/** Soft delete - the row stays, flagged `deleted`, and `restore()` brings it back. */
|
|
252
286
|
async remove(taskId) {
|
|
253
|
-
await this.table.transition(taskId,
|
|
287
|
+
await this.table.transition(taskId, enum_3.AuditTrailAction.DELETE, (0, transforms_1.deletedPatch)(this.auth.mms_id));
|
|
288
|
+
}
|
|
289
|
+
/**
|
|
290
|
+
* Links `input.linked_task_id` to the task as a subtask or as related. The rules live here,
|
|
291
|
+
* checked before the insert: not the task itself, both tasks this organization's and not
|
|
292
|
+
* deleted, and no loop of subtasks. The two a concurrent write could still break - one link
|
|
293
|
+
* per pair, one parent per task - are unique indexes, and their violations are turned into
|
|
294
|
+
* sentences. The link has no audit of its own: it is one UPDATE entry on the task it was
|
|
295
|
+
* made from.
|
|
296
|
+
*/
|
|
297
|
+
async linkTask(taskId, input) {
|
|
298
|
+
const refuse = (status, message, cause) => new errors_1.SupabaseClientError({ status, error: 'Database Error', message }, { cause });
|
|
299
|
+
if (input.linked_task_id === taskId) {
|
|
300
|
+
throw refuse(400, 'A task cannot be linked to itself');
|
|
301
|
+
}
|
|
302
|
+
const { data: tasks, error: tasksError } = await this.supabase
|
|
303
|
+
.from('tasks')
|
|
304
|
+
.select('mms_id, task_number:task_id')
|
|
305
|
+
.eq('organization_id', this.auth.organization_id)
|
|
306
|
+
.eq('deleted', false)
|
|
307
|
+
.in('mms_id', [taskId, input.linked_task_id]);
|
|
308
|
+
if (tasksError)
|
|
309
|
+
throw (0, errors_1.mapPostgrestError)(tasksError);
|
|
310
|
+
const linked = tasks.find((task) => task.mms_id === input.linked_task_id);
|
|
311
|
+
if (tasks.length !== 2 || !linked) {
|
|
312
|
+
throw refuse(400, 'Invalid task for this organization');
|
|
313
|
+
}
|
|
314
|
+
if (input.link_type === enum_1.TaskLinkType.CHILD) {
|
|
315
|
+
// Walk up from the new parent: were the new subtask among its ancestors, the link would
|
|
316
|
+
// close a loop. The chain is a few links deep. `seen` stops the walk should two concurrent
|
|
317
|
+
// links ever have made a loop after all, rather than reading forever.
|
|
318
|
+
const seen = new Set();
|
|
319
|
+
let ancestor = taskId;
|
|
320
|
+
while (ancestor && !seen.has(ancestor)) {
|
|
321
|
+
const child = ancestor;
|
|
322
|
+
seen.add(child);
|
|
323
|
+
const { data: parentLink, error: parentError } = await this.supabase
|
|
324
|
+
.from('task_links')
|
|
325
|
+
.select('task_id')
|
|
326
|
+
.eq('organization_id', this.auth.organization_id)
|
|
327
|
+
.eq('linked_task_id', child)
|
|
328
|
+
.eq('link_type', enum_1.TaskLinkType.CHILD)
|
|
329
|
+
.maybeSingle();
|
|
330
|
+
if (parentError)
|
|
331
|
+
throw (0, errors_1.mapPostgrestError)(parentError);
|
|
332
|
+
ancestor = parentLink?.task_id ?? null;
|
|
333
|
+
if (ancestor === input.linked_task_id) {
|
|
334
|
+
throw refuse(400, 'A task cannot be a subtask of its own subtask');
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
const { error } = await this.supabase.from('task_links').insert({
|
|
339
|
+
task_id: taskId,
|
|
340
|
+
linked_task_id: input.linked_task_id,
|
|
341
|
+
link_type: input.link_type,
|
|
342
|
+
organization_id: this.auth.organization_id,
|
|
343
|
+
created_by: this.auth.mms_id,
|
|
344
|
+
});
|
|
345
|
+
if (error) {
|
|
346
|
+
// Postgres names the violated index: the primary key and the pair index both mean the two
|
|
347
|
+
// tasks are linked already.
|
|
348
|
+
if (error.code === '23505' && error.message.includes('uq_task_links_one_parent')) {
|
|
349
|
+
throw refuse(409, 'That task already has a parent', error);
|
|
350
|
+
}
|
|
351
|
+
if (error.code === '23505') {
|
|
352
|
+
throw refuse(409, 'These tasks are already linked', error);
|
|
353
|
+
}
|
|
354
|
+
throw (0, errors_1.mapPostgrestError)(error);
|
|
355
|
+
}
|
|
356
|
+
const role = input.link_type === enum_1.TaskLinkType.CHILD ? 'subtask' : 'related';
|
|
357
|
+
await this.audit.record({
|
|
358
|
+
action: enum_3.AuditTrailAction.UPDATE,
|
|
359
|
+
entityType: enum_3.AuditTrailEntityType.TASKS,
|
|
360
|
+
entityId: taskId,
|
|
361
|
+
changes: { linked_task: { old: null, new: `TASK-${linked.task_number} (${role})` } },
|
|
362
|
+
});
|
|
363
|
+
return this.getById(taskId);
|
|
364
|
+
}
|
|
365
|
+
/**
|
|
366
|
+
* Removes the link between the two tasks, whichever of them it was made from - a pair carries
|
|
367
|
+
* one link at most. Unlinked from the subtask's side, the entry names the other task as parent.
|
|
368
|
+
*/
|
|
369
|
+
async unlinkTask(taskId, linkedTaskId) {
|
|
370
|
+
// Both ids are `@IsUUID('7')`-validated by TaskLinkIdDto, so interpolating them is safe.
|
|
371
|
+
const { data, error } = await this.supabase
|
|
372
|
+
.from('task_links')
|
|
373
|
+
.delete()
|
|
374
|
+
.eq('organization_id', this.auth.organization_id)
|
|
375
|
+
.or(`and(task_id.eq.${taskId},linked_task_id.eq.${linkedTaskId}),and(task_id.eq.${linkedTaskId},linked_task_id.eq.${taskId})`)
|
|
376
|
+
.select('task_id, link_type, from:tasks!task_id ( task_number:task_id ), to:tasks!linked_task_id ( task_number:task_id )');
|
|
377
|
+
if (error)
|
|
378
|
+
throw (0, errors_1.mapPostgrestError)(error);
|
|
379
|
+
const [row] = data;
|
|
380
|
+
if (!row) {
|
|
381
|
+
throw new errors_1.SupabaseClientError({ status: 404, error: 'Database Error', message: 'Link not found' });
|
|
382
|
+
}
|
|
383
|
+
const madeHere = row.task_id === taskId;
|
|
384
|
+
const role = row.link_type === enum_1.TaskLinkType.RELATED ? 'related' : madeHere ? 'subtask' : 'parent';
|
|
385
|
+
await this.audit.record({
|
|
386
|
+
action: enum_3.AuditTrailAction.UPDATE,
|
|
387
|
+
entityType: enum_3.AuditTrailEntityType.TASKS,
|
|
388
|
+
entityId: taskId,
|
|
389
|
+
changes: {
|
|
390
|
+
linked_task: { old: `TASK-${(madeHere ? row.to : row.from).task_number} (${role})`, new: null },
|
|
391
|
+
},
|
|
392
|
+
});
|
|
393
|
+
return this.getById(taskId);
|
|
254
394
|
}
|
|
255
395
|
/**
|
|
256
396
|
* Refuses, before anything is written, a label id that is not one of this organization's task
|
|
@@ -265,7 +405,7 @@ class TasksResource {
|
|
|
265
405
|
.from('labels')
|
|
266
406
|
.select('mms_id', { count: 'exact', head: true })
|
|
267
407
|
.eq('organization_id', this.auth.organization_id)
|
|
268
|
-
.eq('entity_type',
|
|
408
|
+
.eq('entity_type', enum_2.LabelEntityTypes.TASK)
|
|
269
409
|
.in('mms_id', labelIds);
|
|
270
410
|
if (error)
|
|
271
411
|
throw (0, errors_1.mapPostgrestError)(error);
|
|
@@ -326,8 +466,8 @@ class TasksResource {
|
|
|
326
466
|
throw (0, errors_1.mapPostgrestError)(error);
|
|
327
467
|
}
|
|
328
468
|
await this.audit.record({
|
|
329
|
-
action:
|
|
330
|
-
entityType:
|
|
469
|
+
action: enum_3.AuditTrailAction.UPDATE,
|
|
470
|
+
entityType: enum_3.AuditTrailEntityType.TASKS,
|
|
331
471
|
entityId: taskId,
|
|
332
472
|
changes: {
|
|
333
473
|
labels: {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@managemint-solutions/sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.43.0",
|
|
4
4
|
"description": "Typed Supabase data-access SDK for ManageMint Solutions",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"author": "Scott Bebington <scottbebington@gmail.com>",
|
|
@@ -51,7 +51,7 @@
|
|
|
51
51
|
"testEnvironment": "node"
|
|
52
52
|
},
|
|
53
53
|
"dependencies": {
|
|
54
|
-
"@managemint-solutions/entities": "^1.
|
|
54
|
+
"@managemint-solutions/entities": "^1.26.0"
|
|
55
55
|
},
|
|
56
56
|
"peerDependencies": {
|
|
57
57
|
"@nestjs/common": "^11.0.0",
|