runline 0.14.0 → 0.15.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/dist/plugins/_shared/shiftCloud.js +65 -0
- package/dist/plugins/shiftCrm/src/access.js +45 -0
- package/dist/plugins/shiftCrm/src/accounts.js +81 -0
- package/dist/plugins/shiftCrm/src/activities.js +61 -0
- package/dist/plugins/shiftCrm/src/imports.js +101 -0
- package/dist/plugins/shiftCrm/src/index.js +28 -0
- package/dist/plugins/shiftCrm/src/opportunities.js +145 -0
- package/dist/plugins/shiftCrm/src/people.js +117 -0
- package/dist/plugins/shiftCrm/src/records.js +80 -0
- package/dist/plugins/shiftCrm/src/shared.js +122 -0
- package/dist/plugins/shiftCrm/src/tasks.js +79 -0
- package/dist/plugins/shiftLabs/src/issue-views.js +52 -31
- package/dist/plugins/shiftLabs/src/issues.js +61 -51
- package/dist/plugins/shiftLabs/src/pages.js +60 -34
- package/dist/plugins/shiftLabs/src/projects.js +28 -19
- package/dist/plugins/shiftLabs/src/shared.js +2 -35
- package/package.json +1 -1
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import * as t from "typebox";
|
|
2
|
+
import { CRM_BASE, CRM_PROPERTY_DATA_TYPE, enumSchema, idSchema, listParams, pathSegment, recordTypeSchema, request, STRICT_OBJECT, withQuery, } from "./shared.js";
|
|
3
|
+
export function registerRecordActions(rl) {
|
|
4
|
+
rl.registerAction("record.changeEvents", {
|
|
5
|
+
access: "read",
|
|
6
|
+
description: "List the immutable change-event history (created/updated snapshots) for a CRM record.",
|
|
7
|
+
inputSchema: t.Object({ recordId: idSchema("CRM record ID") }, STRICT_OBJECT),
|
|
8
|
+
async execute(input, ctx) {
|
|
9
|
+
const { recordId } = input;
|
|
10
|
+
const body = await request(ctx, `${CRM_BASE}/records/${pathSegment(recordId)}/change-events`);
|
|
11
|
+
return body.changeEvents;
|
|
12
|
+
},
|
|
13
|
+
});
|
|
14
|
+
rl.registerAction("propertyDefinition.list", {
|
|
15
|
+
access: "read",
|
|
16
|
+
description: "List governed custom-property definitions, optionally for one record type.",
|
|
17
|
+
inputSchema: t.Object({ recordType: t.Optional(recordTypeSchema()) }, STRICT_OBJECT),
|
|
18
|
+
async execute(input, ctx) {
|
|
19
|
+
const body = await request(ctx, withQuery(`${CRM_BASE}/property-definitions`, listParams(input)));
|
|
20
|
+
return body.propertyDefinitions;
|
|
21
|
+
},
|
|
22
|
+
});
|
|
23
|
+
rl.registerAction("propertyDefinition.create", {
|
|
24
|
+
access: "write",
|
|
25
|
+
description: "Define a governed custom property for a record type. Requires a CRM admin grant. Enum types require options.",
|
|
26
|
+
inputSchema: t.Object({
|
|
27
|
+
recordType: recordTypeSchema(),
|
|
28
|
+
key: t.String({
|
|
29
|
+
maxLength: 100,
|
|
30
|
+
pattern: "^[a-z][a-z0-9_]*$",
|
|
31
|
+
description: "snake_case property key",
|
|
32
|
+
}),
|
|
33
|
+
label: t.String({
|
|
34
|
+
minLength: 1,
|
|
35
|
+
maxLength: 200,
|
|
36
|
+
pattern: "\\S",
|
|
37
|
+
description: "Human-readable label",
|
|
38
|
+
}),
|
|
39
|
+
dataType: enumSchema("Property data type", CRM_PROPERTY_DATA_TYPE),
|
|
40
|
+
options: t.Optional(t.Array(t.String({ minLength: 1, maxLength: 500, pattern: "\\S" }), {
|
|
41
|
+
description: "Allowed values for enum and multi_enum types",
|
|
42
|
+
})),
|
|
43
|
+
validation: t.Optional(t.Object({
|
|
44
|
+
minLength: t.Optional(t.Integer({ minimum: 0 })),
|
|
45
|
+
maxLength: t.Optional(t.Integer({ minimum: 0 })),
|
|
46
|
+
minimum: t.Optional(t.Number()),
|
|
47
|
+
maximum: t.Optional(t.Number()),
|
|
48
|
+
pattern: t.Optional(t.String({ maxLength: 1_000 })),
|
|
49
|
+
}, { additionalProperties: false })),
|
|
50
|
+
filterable: t.Optional(t.Boolean()),
|
|
51
|
+
}, STRICT_OBJECT),
|
|
52
|
+
async execute(input, ctx) {
|
|
53
|
+
const fields = input;
|
|
54
|
+
const isEnum = fields.dataType === "enum" || fields.dataType === "multi_enum";
|
|
55
|
+
const options = fields.options ?? [];
|
|
56
|
+
if (isEnum && options.length === 0) {
|
|
57
|
+
throw new Error("Enum property definitions require options");
|
|
58
|
+
}
|
|
59
|
+
if (!isEnum && options.length > 0) {
|
|
60
|
+
throw new Error("Options are only allowed on enum property types");
|
|
61
|
+
}
|
|
62
|
+
if (new Set(options).size !== options.length) {
|
|
63
|
+
throw new Error("Duplicate property option");
|
|
64
|
+
}
|
|
65
|
+
if (fields.validation?.pattern) {
|
|
66
|
+
try {
|
|
67
|
+
new RegExp(fields.validation.pattern);
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
throw new Error("Property validation pattern is not a valid regex");
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
const body = await request(ctx, `${CRM_BASE}/property-definitions`, {
|
|
74
|
+
method: "POST",
|
|
75
|
+
body: JSON.stringify(input),
|
|
76
|
+
});
|
|
77
|
+
return body.propertyDefinition;
|
|
78
|
+
},
|
|
79
|
+
});
|
|
80
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import * as t from "typebox";
|
|
2
|
+
import { cursorSchema, enumSchema, idSchema, timestampSchema, } from "../../_shared/shiftCloud.js";
|
|
3
|
+
export { enumSchema, idSchema, listParams, pathSegment, request, STRICT_OBJECT, STRICT_UPDATE_OBJECT, timestampSchema, withQuery, } from "../../_shared/shiftCloud.js";
|
|
4
|
+
export const CRM_BASE = "/v1/crm";
|
|
5
|
+
export const CRM_ACCESS_ROLE = ["user", "admin"];
|
|
6
|
+
export const CRM_RECORD_TYPE = [
|
|
7
|
+
"account",
|
|
8
|
+
"person",
|
|
9
|
+
"opportunity",
|
|
10
|
+
"activity",
|
|
11
|
+
"task",
|
|
12
|
+
];
|
|
13
|
+
export const CRM_IDENTITY_KIND = [
|
|
14
|
+
"email",
|
|
15
|
+
"phone",
|
|
16
|
+
"whatsapp",
|
|
17
|
+
"linkedin",
|
|
18
|
+
"other",
|
|
19
|
+
];
|
|
20
|
+
export const CRM_PIPELINE_OUTCOME = ["open", "won", "lost"];
|
|
21
|
+
export const CRM_TASK_STATUS = [
|
|
22
|
+
"todo",
|
|
23
|
+
"in_progress",
|
|
24
|
+
"blocked",
|
|
25
|
+
"done",
|
|
26
|
+
"cancelled",
|
|
27
|
+
];
|
|
28
|
+
export const CRM_TASK_PRIORITY = ["low", "medium", "high", "urgent"];
|
|
29
|
+
export const CRM_ACTIVITY_DIRECTION = [
|
|
30
|
+
"inbound",
|
|
31
|
+
"outbound",
|
|
32
|
+
"internal",
|
|
33
|
+
];
|
|
34
|
+
export const CRM_ACTIVITY_STATUS = [
|
|
35
|
+
"planned",
|
|
36
|
+
"completed",
|
|
37
|
+
"cancelled",
|
|
38
|
+
];
|
|
39
|
+
export const CRM_PROPERTY_DATA_TYPE = [
|
|
40
|
+
"string",
|
|
41
|
+
"number",
|
|
42
|
+
"boolean",
|
|
43
|
+
"date",
|
|
44
|
+
"datetime",
|
|
45
|
+
"enum",
|
|
46
|
+
"multi_enum",
|
|
47
|
+
"json",
|
|
48
|
+
];
|
|
49
|
+
function propertiesSchema(description) {
|
|
50
|
+
return t.Record(t.String(), t.Unknown(), { description });
|
|
51
|
+
}
|
|
52
|
+
function externalRefSchema() {
|
|
53
|
+
return t.Object({
|
|
54
|
+
sourceSystem: t.String({
|
|
55
|
+
minLength: 1,
|
|
56
|
+
maxLength: 100,
|
|
57
|
+
pattern: "\\S",
|
|
58
|
+
description: "Upstream system name, e.g. hubspot",
|
|
59
|
+
}),
|
|
60
|
+
sourceId: t.String({
|
|
61
|
+
minLength: 1,
|
|
62
|
+
maxLength: 500,
|
|
63
|
+
pattern: "\\S",
|
|
64
|
+
description: "Stable ID in the upstream system",
|
|
65
|
+
}),
|
|
66
|
+
sourceUrl: t.Optional(t.String({ minLength: 1, maxLength: 2_000 })),
|
|
67
|
+
attributes: t.Optional(propertiesSchema("Source-specific attributes")),
|
|
68
|
+
}, { additionalProperties: false });
|
|
69
|
+
}
|
|
70
|
+
function relationshipSchema() {
|
|
71
|
+
return t.Object({
|
|
72
|
+
toRecordId: idSchema("Target CRM record ID"),
|
|
73
|
+
relationshipType: t.String({
|
|
74
|
+
minLength: 1,
|
|
75
|
+
maxLength: 100,
|
|
76
|
+
pattern: "\\S",
|
|
77
|
+
description: "Relationship type, e.g. participant, attendee, subject",
|
|
78
|
+
}),
|
|
79
|
+
role: t.Optional(t.String({ minLength: 1, maxLength: 200, pattern: "\\S" })),
|
|
80
|
+
attributes: t.Optional(propertiesSchema("Governed link attributes")),
|
|
81
|
+
}, { additionalProperties: false });
|
|
82
|
+
}
|
|
83
|
+
/** Shared create-time record fields (owner, custom properties, links). */
|
|
84
|
+
export function createRecordFields() {
|
|
85
|
+
return {
|
|
86
|
+
ownerId: t.Optional(idSchema("Owning Shift user ID")),
|
|
87
|
+
customProperties: t.Optional(propertiesSchema("Governed custom properties; every key must have a property definition")),
|
|
88
|
+
externalRefs: t.Optional(t.Array(externalRefSchema())),
|
|
89
|
+
relationships: t.Optional(t.Array(relationshipSchema())),
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
/** Shared update-time record fields (tri-state PATCH semantics). */
|
|
93
|
+
export function updateRecordFields() {
|
|
94
|
+
return {
|
|
95
|
+
ownerId: t.Optional(t.Union([idSchema("Owning Shift user ID"), t.Null()])),
|
|
96
|
+
customProperties: t.Optional(propertiesSchema("Replacement governed custom properties")),
|
|
97
|
+
externalRefs: t.Optional(t.Array(externalRefSchema())),
|
|
98
|
+
relationships: t.Optional(t.Array(relationshipSchema())),
|
|
99
|
+
archived: t.Optional(t.Boolean({ description: "Archive or restore" })),
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
export function paginationFields(maxLimit = 200) {
|
|
103
|
+
return {
|
|
104
|
+
cursor: t.Optional(cursorSchema()),
|
|
105
|
+
limit: t.Optional(t.Integer({
|
|
106
|
+
minimum: 1,
|
|
107
|
+
maximum: maxLimit,
|
|
108
|
+
description: "Max results, default 50",
|
|
109
|
+
})),
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
export function recordTypeSchema(description = "CRM record type") {
|
|
113
|
+
return enumSchema(description, CRM_RECORD_TYPE);
|
|
114
|
+
}
|
|
115
|
+
export function assertTimeOrder(start, end, message) {
|
|
116
|
+
if (typeof start === "string" && typeof end === "string" && start > end) {
|
|
117
|
+
throw new Error(message);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
export function nullableTimestamp(description) {
|
|
121
|
+
return t.Optional(t.Union([timestampSchema(description), t.Null()]));
|
|
122
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import * as t from "typebox";
|
|
2
|
+
import { CRM_BASE, CRM_TASK_PRIORITY, CRM_TASK_STATUS, createRecordFields, enumSchema, idSchema, listParams, nullableTimestamp, paginationFields, pathSegment, request, STRICT_OBJECT, STRICT_UPDATE_OBJECT, timestampSchema, updateRecordFields, withQuery, } from "./shared.js";
|
|
3
|
+
const taskListFields = {
|
|
4
|
+
linkedRecordId: t.Optional(idSchema("Filter by linked CRM record ID")),
|
|
5
|
+
status: t.Optional(enumSchema("Task status", CRM_TASK_STATUS)),
|
|
6
|
+
includeArchived: t.Optional(t.Boolean()),
|
|
7
|
+
...paginationFields(),
|
|
8
|
+
};
|
|
9
|
+
export function registerTaskActions(rl) {
|
|
10
|
+
rl.registerAction("task.list", {
|
|
11
|
+
access: "read",
|
|
12
|
+
description: "List the first page of CRM tasks.",
|
|
13
|
+
inputSchema: t.Object(taskListFields, STRICT_OBJECT),
|
|
14
|
+
async execute(input, ctx) {
|
|
15
|
+
const body = await request(ctx, withQuery(`${CRM_BASE}/tasks`, listParams(input)));
|
|
16
|
+
return body.tasks;
|
|
17
|
+
},
|
|
18
|
+
});
|
|
19
|
+
rl.registerAction("task.listPage", {
|
|
20
|
+
access: "read",
|
|
21
|
+
description: "List a cursor-paginated page of CRM tasks.",
|
|
22
|
+
inputSchema: t.Object(taskListFields, STRICT_OBJECT),
|
|
23
|
+
async execute(input, ctx) {
|
|
24
|
+
return request(ctx, withQuery(`${CRM_BASE}/tasks`, listParams(input)));
|
|
25
|
+
},
|
|
26
|
+
});
|
|
27
|
+
rl.registerAction("task.get", {
|
|
28
|
+
access: "read",
|
|
29
|
+
description: "Get a CRM task by record ID.",
|
|
30
|
+
inputSchema: t.Object({ id: idSchema("Task record ID") }, STRICT_OBJECT),
|
|
31
|
+
async execute(input, ctx) {
|
|
32
|
+
const { id } = input;
|
|
33
|
+
const body = await request(ctx, `${CRM_BASE}/tasks/${pathSegment(id)}`);
|
|
34
|
+
return body.task;
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
rl.registerAction("task.create", {
|
|
38
|
+
access: "write",
|
|
39
|
+
description: "Create a CRM follow-up task, optionally linked to records via relationships.",
|
|
40
|
+
inputSchema: t.Object({
|
|
41
|
+
title: t.String({
|
|
42
|
+
minLength: 1,
|
|
43
|
+
maxLength: 500,
|
|
44
|
+
pattern: "\\S",
|
|
45
|
+
description: "Task title",
|
|
46
|
+
}),
|
|
47
|
+
status: t.Optional(enumSchema("Task status", CRM_TASK_STATUS)),
|
|
48
|
+
priority: t.Optional(enumSchema("Task priority", CRM_TASK_PRIORITY)),
|
|
49
|
+
dueAt: t.Optional(timestampSchema("ISO-8601 due timestamp")),
|
|
50
|
+
assignedTo: t.Optional(idSchema("Assigned Shift user ID")),
|
|
51
|
+
...createRecordFields(),
|
|
52
|
+
}, STRICT_OBJECT),
|
|
53
|
+
async execute(input, ctx) {
|
|
54
|
+
const body = await request(ctx, `${CRM_BASE}/tasks`, {
|
|
55
|
+
method: "POST",
|
|
56
|
+
body: JSON.stringify(input),
|
|
57
|
+
});
|
|
58
|
+
return body.task;
|
|
59
|
+
},
|
|
60
|
+
});
|
|
61
|
+
rl.registerAction("task.update", {
|
|
62
|
+
access: "write",
|
|
63
|
+
description: "Update or archive a CRM task: title, status, priority, due date, or assignee.",
|
|
64
|
+
inputSchema: t.Object({
|
|
65
|
+
id: idSchema("Task record ID"),
|
|
66
|
+
title: t.Optional(t.String({ minLength: 1, maxLength: 500, pattern: "\\S" })),
|
|
67
|
+
status: t.Optional(enumSchema("Task status", CRM_TASK_STATUS)),
|
|
68
|
+
priority: t.Optional(enumSchema("Task priority", CRM_TASK_PRIORITY)),
|
|
69
|
+
dueAt: nullableTimestamp("ISO-8601 due timestamp"),
|
|
70
|
+
assignedTo: t.Optional(t.Union([idSchema("Assigned Shift user ID"), t.Null()])),
|
|
71
|
+
...updateRecordFields(),
|
|
72
|
+
}, STRICT_UPDATE_OBJECT),
|
|
73
|
+
async execute(input, ctx) {
|
|
74
|
+
const { id, ...patch } = input;
|
|
75
|
+
const body = await request(ctx, `${CRM_BASE}/tasks/${pathSegment(id)}`, { method: "PATCH", body: JSON.stringify(patch) });
|
|
76
|
+
return body.task;
|
|
77
|
+
},
|
|
78
|
+
});
|
|
79
|
+
}
|
|
@@ -1,17 +1,24 @@
|
|
|
1
1
|
import * as t from "typebox";
|
|
2
|
-
import { enumSchema, ISSUE_PRIORITY, ISSUE_STATUS, ISSUE_VIEW_GROUP, ISSUE_VIEW_LAYOUT, ISSUE_VIEW_SORT, ISSUE_VIEW_VISIBILITY, pathSegment, request, } from "./shared.js";
|
|
2
|
+
import { cursorSchema, enumSchema, ISSUE_PRIORITY, ISSUE_STATUS, ISSUE_VIEW_GROUP, ISSUE_VIEW_LAYOUT, ISSUE_VIEW_SORT, ISSUE_VIEW_VISIBILITY, idSchema, pathSegment, request, STRICT_OBJECT, STRICT_UPDATE_OBJECT, timestampSchema, } from "./shared.js";
|
|
3
3
|
const viewFields = {
|
|
4
|
-
name: t.String({
|
|
4
|
+
name: t.String({
|
|
5
|
+
minLength: 1,
|
|
6
|
+
maxLength: 120,
|
|
7
|
+
pattern: "\\S",
|
|
8
|
+
description: "View name",
|
|
9
|
+
}),
|
|
5
10
|
visibility: t.Optional(t.Literal("organization")),
|
|
6
11
|
layout: t.Optional(enumSchema("View layout", ISSUE_VIEW_LAYOUT)),
|
|
7
|
-
projectId: t.Optional(
|
|
8
|
-
rootIssueId: t.Optional(
|
|
12
|
+
projectId: t.Optional(idSchema("Project ID")),
|
|
13
|
+
rootIssueId: t.Optional(idSchema("Root Issue ID")),
|
|
9
14
|
statuses: t.Optional(t.Array(enumSchema("Issue status", ISSUE_STATUS), { maxItems: 5 })),
|
|
10
15
|
priorities: t.Optional(t.Array(enumSchema("Issue priority", ISSUE_PRIORITY), { maxItems: 5 })),
|
|
11
|
-
assigneeUserIds: t.Optional(t.Array(
|
|
12
|
-
labels: t.Optional(t.Array(t.String({ minLength: 1, maxLength: 64
|
|
13
|
-
|
|
14
|
-
|
|
16
|
+
assigneeUserIds: t.Optional(t.Array(idSchema("Assignee user ID"), { maxItems: 20 })),
|
|
17
|
+
labels: t.Optional(t.Array(t.String({ minLength: 1, maxLength: 64, pattern: "\\S" }), {
|
|
18
|
+
maxItems: 20,
|
|
19
|
+
})),
|
|
20
|
+
startAfter: t.Optional(timestampSchema("ISO-8601 filter start timestamp")),
|
|
21
|
+
dueBefore: t.Optional(timestampSchema("ISO-8601 filter due timestamp")),
|
|
15
22
|
blocked: t.Optional(t.Boolean()),
|
|
16
23
|
groupBy: t.Optional(enumSchema("View grouping", ISSUE_VIEW_GROUP)),
|
|
17
24
|
sortBy: t.Optional(t.Array(enumSchema("View sort", ISSUE_VIEW_SORT), {
|
|
@@ -22,13 +29,14 @@ const viewFields = {
|
|
|
22
29
|
export function registerIssueViewActions(rl) {
|
|
23
30
|
const listFields = {
|
|
24
31
|
visibility: t.Optional(enumSchema("View visibility", ISSUE_VIEW_VISIBILITY)),
|
|
25
|
-
|
|
32
|
+
createdById: t.Optional(idSchema("Creator ID")),
|
|
33
|
+
cursor: t.Optional(cursorSchema()),
|
|
26
34
|
limit: t.Optional(t.Integer({ minimum: 1, maximum: 100 })),
|
|
27
35
|
};
|
|
28
36
|
rl.registerAction("issueView.list", {
|
|
29
37
|
access: "read",
|
|
30
38
|
description: "List the first page of personal and organization Issue Views.",
|
|
31
|
-
inputSchema: t.Object(listFields),
|
|
39
|
+
inputSchema: t.Object(listFields, STRICT_OBJECT),
|
|
32
40
|
async execute(input, ctx) {
|
|
33
41
|
const body = await request(ctx, `/v1/issue-views?${listParams(input)}`);
|
|
34
42
|
return body.views;
|
|
@@ -37,7 +45,7 @@ export function registerIssueViewActions(rl) {
|
|
|
37
45
|
rl.registerAction("issueView.listPage", {
|
|
38
46
|
access: "read",
|
|
39
47
|
description: "List a cursor-paginated page of Issue Views.",
|
|
40
|
-
inputSchema: t.Object(listFields),
|
|
48
|
+
inputSchema: t.Object(listFields, STRICT_OBJECT),
|
|
41
49
|
async execute(input, ctx) {
|
|
42
50
|
return request(ctx, `/v1/issue-views?${listParams(input)}`);
|
|
43
51
|
},
|
|
@@ -45,7 +53,7 @@ export function registerIssueViewActions(rl) {
|
|
|
45
53
|
rl.registerAction("issueView.get", {
|
|
46
54
|
access: "read",
|
|
47
55
|
description: "Get an Issue View by ID.",
|
|
48
|
-
inputSchema: t.Object({ id:
|
|
56
|
+
inputSchema: t.Object({ id: idSchema("Issue View ID") }, STRICT_OBJECT),
|
|
49
57
|
async execute(input, ctx) {
|
|
50
58
|
const { id } = input;
|
|
51
59
|
const body = await request(ctx, `/v1/issue-views/${pathSegment(id)}`);
|
|
@@ -56,9 +64,9 @@ export function registerIssueViewActions(rl) {
|
|
|
56
64
|
access: "read",
|
|
57
65
|
description: "Execute the first page of a saved Issue View.",
|
|
58
66
|
inputSchema: t.Object({
|
|
59
|
-
id:
|
|
67
|
+
id: idSchema("Issue View ID"),
|
|
60
68
|
limit: t.Optional(t.Integer({ minimum: 1, maximum: 100 })),
|
|
61
|
-
}),
|
|
69
|
+
}, STRICT_OBJECT),
|
|
62
70
|
async execute(input, ctx) {
|
|
63
71
|
const { id, ...pagination } = input;
|
|
64
72
|
const body = await request(ctx, withQuery(`/v1/issue-views/${pathSegment(id)}/issues`, listParams(pagination)));
|
|
@@ -69,10 +77,10 @@ export function registerIssueViewActions(rl) {
|
|
|
69
77
|
access: "read",
|
|
70
78
|
description: "Execute a cursor-paginated page of a saved Issue View.",
|
|
71
79
|
inputSchema: t.Object({
|
|
72
|
-
id:
|
|
73
|
-
cursor: t.Optional(
|
|
80
|
+
id: idSchema("Issue View ID"),
|
|
81
|
+
cursor: t.Optional(cursorSchema()),
|
|
74
82
|
limit: t.Optional(t.Integer({ minimum: 1, maximum: 100 })),
|
|
75
|
-
}),
|
|
83
|
+
}, STRICT_OBJECT),
|
|
76
84
|
async execute(input, ctx) {
|
|
77
85
|
const { id, ...pagination } = input;
|
|
78
86
|
return request(ctx, withQuery(`/v1/issue-views/${pathSegment(id)}/issues`, listParams(pagination)));
|
|
@@ -81,11 +89,13 @@ export function registerIssueViewActions(rl) {
|
|
|
81
89
|
rl.registerAction("issueView.create", {
|
|
82
90
|
access: "write",
|
|
83
91
|
description: "Create a bounded saved Issue query. Views never own Issue membership.",
|
|
84
|
-
inputSchema: t.Object(viewFields),
|
|
92
|
+
inputSchema: t.Object(viewFields, STRICT_OBJECT),
|
|
85
93
|
async execute(input, ctx) {
|
|
94
|
+
const fields = input;
|
|
95
|
+
assertDateOrder(fields.startAfter, fields.dueBefore);
|
|
86
96
|
const body = await request(ctx, "/v1/issue-views", {
|
|
87
97
|
method: "POST",
|
|
88
|
-
body: JSON.stringify(toViewBody(
|
|
98
|
+
body: JSON.stringify(toViewBody(fields)),
|
|
89
99
|
});
|
|
90
100
|
return body.view;
|
|
91
101
|
},
|
|
@@ -94,27 +104,28 @@ export function registerIssueViewActions(rl) {
|
|
|
94
104
|
access: "write",
|
|
95
105
|
description: "Patch a saved Issue View. Null filter values remove that filter without replacing unrelated filters.",
|
|
96
106
|
inputSchema: t.Object({
|
|
97
|
-
id:
|
|
98
|
-
name: t.Optional(t.String({ minLength: 1, maxLength: 120 })),
|
|
107
|
+
id: idSchema("Issue View ID"),
|
|
108
|
+
name: t.Optional(t.String({ minLength: 1, maxLength: 120, pattern: "\\S" })),
|
|
99
109
|
visibility: t.Optional(t.Literal("organization")),
|
|
100
110
|
layout: t.Optional(enumSchema("View layout", ISSUE_VIEW_LAYOUT)),
|
|
101
|
-
projectId:
|
|
102
|
-
rootIssueId:
|
|
111
|
+
projectId: optionalNullableId("Project ID"),
|
|
112
|
+
rootIssueId: optionalNullableId("Root Issue ID"),
|
|
103
113
|
statuses: optionalNullableArray(enumSchema("Issue status", ISSUE_STATUS), 5),
|
|
104
114
|
priorities: optionalNullableArray(enumSchema("Issue priority", ISSUE_PRIORITY), 5),
|
|
105
|
-
assigneeUserIds: optionalNullableArray(
|
|
106
|
-
labels: optionalNullableArray(t.String({ minLength: 1, maxLength: 64 }), 20),
|
|
107
|
-
startAfter:
|
|
108
|
-
dueBefore:
|
|
115
|
+
assigneeUserIds: optionalNullableArray(idSchema("Assignee user ID"), 20),
|
|
116
|
+
labels: optionalNullableArray(t.String({ minLength: 1, maxLength: 64, pattern: "\\S" }), 20),
|
|
117
|
+
startAfter: optionalNullableTimestamp("ISO-8601 filter start timestamp"),
|
|
118
|
+
dueBefore: optionalNullableTimestamp("ISO-8601 filter due timestamp"),
|
|
109
119
|
blocked: t.Optional(t.Union([t.Boolean(), t.Null()])),
|
|
110
120
|
groupBy: t.Optional(t.Union([enumSchema("View grouping", ISSUE_VIEW_GROUP), t.Null()])),
|
|
111
121
|
sortBy: t.Optional(t.Array(enumSchema("View sort", ISSUE_VIEW_SORT), {
|
|
112
122
|
minItems: 1,
|
|
113
123
|
maxItems: 3,
|
|
114
124
|
})),
|
|
115
|
-
},
|
|
125
|
+
}, STRICT_UPDATE_OBJECT),
|
|
116
126
|
async execute(input, ctx) {
|
|
117
127
|
const { id, ...fields } = input;
|
|
128
|
+
assertDateOrder(fields.startAfter, fields.dueBefore);
|
|
118
129
|
const body = await request(ctx, `/v1/issue-views/${pathSegment(id)}`, { method: "PATCH", body: JSON.stringify(toViewBody(fields, false)) });
|
|
119
130
|
return body.view;
|
|
120
131
|
},
|
|
@@ -122,7 +133,7 @@ export function registerIssueViewActions(rl) {
|
|
|
122
133
|
rl.registerAction("issueView.delete", {
|
|
123
134
|
access: "write",
|
|
124
135
|
description: "Delete a saved Issue View without changing any Issues.",
|
|
125
|
-
inputSchema: t.Object({ id:
|
|
136
|
+
inputSchema: t.Object({ id: idSchema("Issue View ID") }, STRICT_OBJECT),
|
|
126
137
|
async execute(input, ctx) {
|
|
127
138
|
const { id } = input;
|
|
128
139
|
await request(ctx, `/v1/issue-views/${pathSegment(id)}`, {
|
|
@@ -144,8 +155,18 @@ function withQuery(path, params) {
|
|
|
144
155
|
const query = params.toString();
|
|
145
156
|
return query ? `${path}?${query}` : path;
|
|
146
157
|
}
|
|
147
|
-
function
|
|
148
|
-
|
|
158
|
+
function assertDateOrder(startAfter, dueBefore) {
|
|
159
|
+
if (typeof startAfter === "string" &&
|
|
160
|
+
typeof dueBefore === "string" &&
|
|
161
|
+
startAfter > dueBefore) {
|
|
162
|
+
throw new Error("View dueBefore must not precede startAfter");
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
function optionalNullableId(description) {
|
|
166
|
+
return t.Optional(t.Union([idSchema(description), t.Null()]));
|
|
167
|
+
}
|
|
168
|
+
function optionalNullableTimestamp(description) {
|
|
169
|
+
return t.Optional(t.Union([timestampSchema(description), t.Null()]));
|
|
149
170
|
}
|
|
150
171
|
function optionalNullableArray(item, maxItems) {
|
|
151
172
|
return t.Optional(t.Union([t.Array(item, { maxItems }), t.Null()]));
|