@anthusai/papyrus 5.43.0-next.49 → 5.43.0-next.50
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/amplify/data/message-actions.js +68 -0
- package/amplify/data/message-actions.ts +70 -0
- package/amplify/data/resolvers/delete-own-message.js +30 -0
- package/amplify/data/resolvers/edit-message.js +33 -0
- package/amplify/data/resolvers/moderate-message.js +32 -0
- package/amplify/data/resolvers/post-message.js +68 -0
- package/amplify/data/resolvers/rename-thread.js +28 -0
- package/amplify/data/resource.js +4 -5
- package/amplify/data/resource.ts +4 -5
- package/amplify/data/schema.js +10 -7
- package/amplify/data/schema.ts +10 -7
- package/components/news-desk-taxonomy-client.ts +6 -37
- package/lib/console-chat-client.ts +8 -178
- package/lib/post-message.ts +54 -0
- package/package.json +2 -1
- package/routes.manifest.json +1 -1
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { a } from "@aws-amplify/backend";
|
|
2
|
+
/**
|
|
3
|
+
* Messages belong to their author, and the backend says so. The browser cannot create, edit or delete a Message through the
|
|
4
|
+
* model (editors and admins only read it); it calls these four mutations, whose resolvers stamp the author from the signed-in
|
|
5
|
+
* identity and check ownership against the stored `authorSub` in the same DynamoDB write. `renameThread` does the same for a
|
|
6
|
+
* conversation's title. Functions and IAM callers (the router, procedures, the console responder, email, Slack) write the
|
|
7
|
+
* model directly with their own grants, as before.
|
|
8
|
+
*
|
|
9
|
+
* Each resolver is one self-contained file (AppSync takes a single file and offers `@aws-appsync/utils` only).
|
|
10
|
+
*/
|
|
11
|
+
const people = (allow) => [allow.group("editor"), allow.group("admin")];
|
|
12
|
+
const adminsOnly = (allow) => [allow.group("admin")];
|
|
13
|
+
const onMessages = (entry) => a.handler.custom({ dataSource: a.ref("Message"), entry });
|
|
14
|
+
const onThreads = (entry) => a.handler.custom({ dataSource: a.ref("MessageThread"), entry });
|
|
15
|
+
export const messageActionsSchemaDefinitions = {
|
|
16
|
+
postMessage: a
|
|
17
|
+
.mutation()
|
|
18
|
+
.arguments({
|
|
19
|
+
id: a.id().required(),
|
|
20
|
+
threadId: a.id().required(),
|
|
21
|
+
content: a.string(),
|
|
22
|
+
summary: a.string(),
|
|
23
|
+
parentMessageId: a.id(),
|
|
24
|
+
sequenceNumber: a.integer(),
|
|
25
|
+
messageKind: a.string().required(),
|
|
26
|
+
messageDomain: a.string().required(),
|
|
27
|
+
messageType: a.string(),
|
|
28
|
+
role: a.string(),
|
|
29
|
+
source: a.string(),
|
|
30
|
+
authorLabel: a.string(),
|
|
31
|
+
semanticLayer: a.string(),
|
|
32
|
+
searchVisibility: a.string(),
|
|
33
|
+
responseTarget: a.string(),
|
|
34
|
+
responseStatus: a.string(),
|
|
35
|
+
responseOwner: a.string(),
|
|
36
|
+
responseStartedAt: a.datetime(),
|
|
37
|
+
responseCompletedAt: a.datetime(),
|
|
38
|
+
metadata: a.json(),
|
|
39
|
+
newsroomFeedKey: a.string(),
|
|
40
|
+
})
|
|
41
|
+
.returns(a.ref("Message"))
|
|
42
|
+
.authorization(people)
|
|
43
|
+
.handler(onMessages("./resolvers/post-message.js")),
|
|
44
|
+
editMessage: a
|
|
45
|
+
.mutation()
|
|
46
|
+
.arguments({ id: a.id().required(), content: a.string().required(), summary: a.string() })
|
|
47
|
+
.returns(a.ref("Message"))
|
|
48
|
+
.authorization(people)
|
|
49
|
+
.handler(onMessages("./resolvers/edit-message.js")),
|
|
50
|
+
deleteOwnMessage: a
|
|
51
|
+
.mutation()
|
|
52
|
+
.arguments({ id: a.id().required() })
|
|
53
|
+
.returns(a.ref("Message"))
|
|
54
|
+
.authorization(people)
|
|
55
|
+
.handler(onMessages("./resolvers/delete-own-message.js")),
|
|
56
|
+
moderateMessage: a
|
|
57
|
+
.mutation()
|
|
58
|
+
.arguments({ id: a.id().required() })
|
|
59
|
+
.returns(a.ref("Message"))
|
|
60
|
+
.authorization(adminsOnly)
|
|
61
|
+
.handler(onMessages("./resolvers/moderate-message.js")),
|
|
62
|
+
renameThread: a
|
|
63
|
+
.mutation()
|
|
64
|
+
.arguments({ id: a.id().required(), title: a.string().required() })
|
|
65
|
+
.returns(a.ref("MessageThread"))
|
|
66
|
+
.authorization(people)
|
|
67
|
+
.handler(onThreads("./resolvers/rename-thread.js")),
|
|
68
|
+
};
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { a } from "@aws-amplify/backend";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Messages belong to their author, and the backend says so. The browser cannot create, edit or delete a Message through the
|
|
5
|
+
* model (editors and admins only read it); it calls these four mutations, whose resolvers stamp the author from the signed-in
|
|
6
|
+
* identity and check ownership against the stored `authorSub` in the same DynamoDB write. `renameThread` does the same for a
|
|
7
|
+
* conversation's title. Functions and IAM callers (the router, procedures, the console responder, email, Slack) write the
|
|
8
|
+
* model directly with their own grants, as before.
|
|
9
|
+
*
|
|
10
|
+
* Each resolver is one self-contained file (AppSync takes a single file and offers `@aws-appsync/utils` only).
|
|
11
|
+
*/
|
|
12
|
+
const people = (allow: any) => [allow.group("editor"), allow.group("admin")];
|
|
13
|
+
const adminsOnly = (allow: any) => [allow.group("admin")];
|
|
14
|
+
const onMessages = (entry: string) => a.handler.custom({ dataSource: a.ref("Message"), entry });
|
|
15
|
+
const onThreads = (entry: string) => a.handler.custom({ dataSource: a.ref("MessageThread"), entry });
|
|
16
|
+
|
|
17
|
+
export const messageActionsSchemaDefinitions = {
|
|
18
|
+
postMessage: a
|
|
19
|
+
.mutation()
|
|
20
|
+
.arguments({
|
|
21
|
+
id: a.id().required(),
|
|
22
|
+
threadId: a.id().required(),
|
|
23
|
+
content: a.string(),
|
|
24
|
+
summary: a.string(),
|
|
25
|
+
parentMessageId: a.id(),
|
|
26
|
+
sequenceNumber: a.integer(),
|
|
27
|
+
messageKind: a.string().required(),
|
|
28
|
+
messageDomain: a.string().required(),
|
|
29
|
+
messageType: a.string(),
|
|
30
|
+
role: a.string(),
|
|
31
|
+
source: a.string(),
|
|
32
|
+
authorLabel: a.string(),
|
|
33
|
+
semanticLayer: a.string(),
|
|
34
|
+
searchVisibility: a.string(),
|
|
35
|
+
responseTarget: a.string(),
|
|
36
|
+
responseStatus: a.string(),
|
|
37
|
+
responseOwner: a.string(),
|
|
38
|
+
responseStartedAt: a.datetime(),
|
|
39
|
+
responseCompletedAt: a.datetime(),
|
|
40
|
+
metadata: a.json(),
|
|
41
|
+
newsroomFeedKey: a.string(),
|
|
42
|
+
})
|
|
43
|
+
.returns(a.ref("Message"))
|
|
44
|
+
.authorization(people)
|
|
45
|
+
.handler(onMessages("./resolvers/post-message.js")),
|
|
46
|
+
editMessage: a
|
|
47
|
+
.mutation()
|
|
48
|
+
.arguments({ id: a.id().required(), content: a.string().required(), summary: a.string() })
|
|
49
|
+
.returns(a.ref("Message"))
|
|
50
|
+
.authorization(people)
|
|
51
|
+
.handler(onMessages("./resolvers/edit-message.js")),
|
|
52
|
+
deleteOwnMessage: a
|
|
53
|
+
.mutation()
|
|
54
|
+
.arguments({ id: a.id().required() })
|
|
55
|
+
.returns(a.ref("Message"))
|
|
56
|
+
.authorization(people)
|
|
57
|
+
.handler(onMessages("./resolvers/delete-own-message.js")),
|
|
58
|
+
moderateMessage: a
|
|
59
|
+
.mutation()
|
|
60
|
+
.arguments({ id: a.id().required() })
|
|
61
|
+
.returns(a.ref("Message"))
|
|
62
|
+
.authorization(adminsOnly)
|
|
63
|
+
.handler(onMessages("./resolvers/moderate-message.js")),
|
|
64
|
+
renameThread: a
|
|
65
|
+
.mutation()
|
|
66
|
+
.arguments({ id: a.id().required(), title: a.string().required() })
|
|
67
|
+
.returns(a.ref("MessageThread"))
|
|
68
|
+
.authorization(people)
|
|
69
|
+
.handler(onThreads("./resolvers/rename-thread.js")),
|
|
70
|
+
};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// deleteOwnMessage: the author deletes their own Message. A soft delete: the Message stays (replies keep their place) marked
|
|
2
|
+
// deleted, with when and by whom. Nothing is removed.
|
|
3
|
+
import { util } from "@aws-appsync/utils";
|
|
4
|
+
import * as ddb from "@aws-appsync/utils/dynamodb";
|
|
5
|
+
|
|
6
|
+
export function request(ctx) {
|
|
7
|
+
const identity = ctx.identity;
|
|
8
|
+
if (!identity || !identity.sub) util.error("Sign in to delete a message.", "Unauthorized");
|
|
9
|
+
const now = util.time.nowISO8601();
|
|
10
|
+
return ddb.update({
|
|
11
|
+
key: { id: ctx.args.id },
|
|
12
|
+
update: {
|
|
13
|
+
status: ddb.operations.replace("deleted"),
|
|
14
|
+
deletedAt: ddb.operations.replace(now),
|
|
15
|
+
deletedBySub: ddb.operations.replace(identity.sub),
|
|
16
|
+
updatedAt: ddb.operations.replace(now),
|
|
17
|
+
},
|
|
18
|
+
condition: { authorSub: { eq: identity.sub }, status: { ne: "deleted" } },
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function response(ctx) {
|
|
23
|
+
if (ctx.error) {
|
|
24
|
+
if (ctx.error.type === "DynamoDB:ConditionalCheckFailedException") {
|
|
25
|
+
util.error("Only the author can delete their own message, and it may already be deleted.", "Unauthorized");
|
|
26
|
+
}
|
|
27
|
+
util.error(ctx.error.message, ctx.error.type);
|
|
28
|
+
}
|
|
29
|
+
return ctx.result;
|
|
30
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// editMessage: a person changes the text of their own Message, and no one else's. The condition is checked by DynamoDB against the
|
|
2
|
+
// stored authorSub in the same write, so there is no window between the check and the change. A deleted Message cannot be edited.
|
|
3
|
+
// Authorship cannot be reassigned: the update never touches authorSub.
|
|
4
|
+
import { util } from "@aws-appsync/utils";
|
|
5
|
+
import * as ddb from "@aws-appsync/utils/dynamodb";
|
|
6
|
+
|
|
7
|
+
export function request(ctx) {
|
|
8
|
+
const identity = ctx.identity;
|
|
9
|
+
if (!identity || !identity.sub) util.error("Sign in to edit a message.", "Unauthorized");
|
|
10
|
+
const content = typeof ctx.args.content === "string" ? ctx.args.content.trim() : "";
|
|
11
|
+
if (!content) util.error("A message cannot be empty.", "BadRequest");
|
|
12
|
+
const now = util.time.nowISO8601();
|
|
13
|
+
return ddb.update({
|
|
14
|
+
key: { id: ctx.args.id },
|
|
15
|
+
update: {
|
|
16
|
+
content: ddb.operations.replace(content),
|
|
17
|
+
summary: ddb.operations.replace(ctx.args.summary ? ctx.args.summary : content.slice(0, 120)),
|
|
18
|
+
editedAt: ddb.operations.replace(now),
|
|
19
|
+
updatedAt: ddb.operations.replace(now),
|
|
20
|
+
},
|
|
21
|
+
condition: { authorSub: { eq: identity.sub }, status: { ne: "deleted" } },
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function response(ctx) {
|
|
26
|
+
if (ctx.error) {
|
|
27
|
+
if (ctx.error.type === "DynamoDB:ConditionalCheckFailedException") {
|
|
28
|
+
util.error("Only the author can edit a message, and a deleted message cannot be edited.", "Unauthorized");
|
|
29
|
+
}
|
|
30
|
+
util.error(ctx.error.message, ctx.error.type);
|
|
31
|
+
}
|
|
32
|
+
return ctx.result;
|
|
33
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// moderateMessage: an admin removes anyone's Message. The same soft delete as the author's, with no change to the text, and only
|
|
2
|
+
// for the admin group (checked here as well as by the mutation's authorization).
|
|
3
|
+
import { util } from "@aws-appsync/utils";
|
|
4
|
+
import * as ddb from "@aws-appsync/utils/dynamodb";
|
|
5
|
+
|
|
6
|
+
export function request(ctx) {
|
|
7
|
+
const identity = ctx.identity;
|
|
8
|
+
if (!identity || !identity.sub) util.error("Sign in to moderate a message.", "Unauthorized");
|
|
9
|
+
const groups = identity.groups || [];
|
|
10
|
+
if (groups.indexOf("admin") < 0) util.error("Only an admin can delete someone else's message.", "Unauthorized");
|
|
11
|
+
const now = util.time.nowISO8601();
|
|
12
|
+
return ddb.update({
|
|
13
|
+
key: { id: ctx.args.id },
|
|
14
|
+
update: {
|
|
15
|
+
status: ddb.operations.replace("deleted"),
|
|
16
|
+
deletedAt: ddb.operations.replace(now),
|
|
17
|
+
deletedBySub: ddb.operations.replace(identity.sub),
|
|
18
|
+
updatedAt: ddb.operations.replace(now),
|
|
19
|
+
},
|
|
20
|
+
condition: { id: { attributeExists: true }, status: { ne: "deleted" } },
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function response(ctx) {
|
|
25
|
+
if (ctx.error) {
|
|
26
|
+
if (ctx.error.type === "DynamoDB:ConditionalCheckFailedException") {
|
|
27
|
+
util.error("That message does not exist or is already deleted.", "NotFound");
|
|
28
|
+
}
|
|
29
|
+
util.error(ctx.error.message, ctx.error.type);
|
|
30
|
+
}
|
|
31
|
+
return ctx.result;
|
|
32
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// postMessage: the only way a person's Message is created. The author is the signed-in user (the identity's sub), whatever the
|
|
2
|
+
// client sends: the mutation has no author argument, and a label is only a label. One DynamoDB PutItem that refuses to replace an
|
|
3
|
+
// existing Message (ids are derived from the sequence number, so a lost race comes back as a conflict the caller retries).
|
|
4
|
+
import { util } from "@aws-appsync/utils";
|
|
5
|
+
import * as ddb from "@aws-appsync/utils/dynamodb";
|
|
6
|
+
|
|
7
|
+
function clean(value) {
|
|
8
|
+
const out = {};
|
|
9
|
+
const keys = Object.keys(value);
|
|
10
|
+
for (let index = 0; index < keys.length; index++) {
|
|
11
|
+
const key = keys[index];
|
|
12
|
+
if (value[key] !== null && value[key] !== undefined) out[key] = value[key];
|
|
13
|
+
}
|
|
14
|
+
return out;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function request(ctx) {
|
|
18
|
+
const identity = ctx.identity;
|
|
19
|
+
if (!identity || !identity.sub) util.error("Sign in to post a message.", "Unauthorized");
|
|
20
|
+
const args = ctx.args;
|
|
21
|
+
const content = typeof args.content === "string" ? args.content.trim() : "";
|
|
22
|
+
// A placeholder waiting for a responder (PENDING) may start empty; any other message needs text.
|
|
23
|
+
if (!content && args.responseStatus !== "PENDING") util.error("A message cannot be empty.", "BadRequest");
|
|
24
|
+
if (!args.id || !args.threadId) util.error("A message needs an id and a conversation.", "BadRequest");
|
|
25
|
+
const now = util.time.nowISO8601();
|
|
26
|
+
const claims = identity.claims || {};
|
|
27
|
+
const email = typeof claims.email === "string" ? claims.email : "";
|
|
28
|
+
const label = typeof args.authorLabel === "string" && args.authorLabel.trim() ? args.authorLabel.trim() : email ? email : "editor";
|
|
29
|
+
const item = clean({
|
|
30
|
+
__typename: "Message",
|
|
31
|
+
id: args.id,
|
|
32
|
+
messageKind: args.messageKind,
|
|
33
|
+
messageDomain: args.messageDomain,
|
|
34
|
+
status: "active",
|
|
35
|
+
summary: args.summary ? args.summary : content.slice(0, 120),
|
|
36
|
+
source: args.source,
|
|
37
|
+
authorSub: identity.sub,
|
|
38
|
+
authorLabel: label,
|
|
39
|
+
threadId: args.threadId,
|
|
40
|
+
parentMessageId: args.parentMessageId,
|
|
41
|
+
sequenceNumber: args.sequenceNumber,
|
|
42
|
+
role: args.role ? args.role : "human",
|
|
43
|
+
messageType: args.messageType,
|
|
44
|
+
content: content,
|
|
45
|
+
semanticLayer: args.semanticLayer,
|
|
46
|
+
searchVisibility: args.searchVisibility,
|
|
47
|
+
responseTarget: args.responseTarget,
|
|
48
|
+
responseStatus: args.responseStatus ? args.responseStatus : "COMPLETED",
|
|
49
|
+
responseOwner: args.responseOwner,
|
|
50
|
+
responseStartedAt: args.responseStartedAt,
|
|
51
|
+
responseCompletedAt: args.responseCompletedAt,
|
|
52
|
+
metadata: args.metadata,
|
|
53
|
+
createdAt: now,
|
|
54
|
+
updatedAt: now,
|
|
55
|
+
newsroomFeedKey: args.newsroomFeedKey,
|
|
56
|
+
});
|
|
57
|
+
return ddb.put({ key: { id: args.id }, item: item, condition: { id: { attributeExists: false } } });
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function response(ctx) {
|
|
61
|
+
if (ctx.error) {
|
|
62
|
+
if (ctx.error.type === "DynamoDB:ConditionalCheckFailedException") {
|
|
63
|
+
util.error("A message with this id already exists (conditional check failed); try again.", "Conflict");
|
|
64
|
+
}
|
|
65
|
+
util.error(ctx.error.message, ctx.error.type);
|
|
66
|
+
}
|
|
67
|
+
return ctx.result;
|
|
68
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// renameThread: the person who started a conversation, or an admin, gives it a new title. The browser's own update of a thread
|
|
2
|
+
// cannot change the title (the field is read and create only), so this is the one way. Nothing else about the thread changes.
|
|
3
|
+
import { util } from "@aws-appsync/utils";
|
|
4
|
+
import * as ddb from "@aws-appsync/utils/dynamodb";
|
|
5
|
+
|
|
6
|
+
export function request(ctx) {
|
|
7
|
+
const identity = ctx.identity;
|
|
8
|
+
if (!identity || !identity.sub) util.error("Sign in to rename a conversation.", "Unauthorized");
|
|
9
|
+
const title = typeof ctx.args.title === "string" ? ctx.args.title.trim() : "";
|
|
10
|
+
if (!title) util.error("A title cannot be empty.", "BadRequest");
|
|
11
|
+
const groups = identity.groups || [];
|
|
12
|
+
const isAdmin = groups.indexOf("admin") >= 0;
|
|
13
|
+
return ddb.update({
|
|
14
|
+
key: { id: ctx.args.id },
|
|
15
|
+
update: { title: ddb.operations.replace(title.slice(0, 120)) },
|
|
16
|
+
condition: isAdmin ? { id: { attributeExists: true } } : { createdBySub: { eq: identity.sub } },
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function response(ctx) {
|
|
21
|
+
if (ctx.error) {
|
|
22
|
+
if (ctx.error.type === "DynamoDB:ConditionalCheckFailedException") {
|
|
23
|
+
util.error("Only the person who started this conversation, or an admin, can rename it.", "Unauthorized");
|
|
24
|
+
}
|
|
25
|
+
util.error(ctx.error.message, ctx.error.type);
|
|
26
|
+
}
|
|
27
|
+
return ctx.result;
|
|
28
|
+
}
|
package/amplify/data/resource.js
CHANGED
|
@@ -1726,12 +1726,11 @@ const schema = a.schema({
|
|
|
1726
1726
|
index("searchVisibility").sortKeys(["createdAt"]).name("messagesBySearchVisibilityCreatedAt").queryField("listMessagesBySearchVisibilityAndCreatedAt"),
|
|
1727
1727
|
])
|
|
1728
1728
|
.authorization((allow) => [
|
|
1729
|
-
//
|
|
1730
|
-
|
|
1731
|
-
allow.group(
|
|
1729
|
+
// Messages belong to their author, and the backend stamps the author: people read Messages here and write them only through
|
|
1730
|
+
// postMessage, editMessage, deleteOwnMessage and moderateMessage (message-actions.ts). Functions and IAM callers write directly.
|
|
1731
|
+
allow.group("editor").to(["read"]),
|
|
1732
|
+
allow.group(adminGroup).to(["read"]),
|
|
1732
1733
|
allow.authenticated("identityPool").to(["read", "create", "update"]),
|
|
1733
|
-
// Messages belong to their author: the author edits (and soft-deletes) their own, and only an admin can remove someone else's.
|
|
1734
|
-
allow.ownerDefinedIn("authorSub").identityClaim("sub").to(["read", "update", "delete"]),
|
|
1735
1734
|
]),
|
|
1736
1735
|
MessageThread: a
|
|
1737
1736
|
.model({
|
package/amplify/data/resource.ts
CHANGED
|
@@ -1842,12 +1842,11 @@ const schema = a.schema({
|
|
|
1842
1842
|
index("searchVisibility").sortKeys(["createdAt"]).name("messagesBySearchVisibilityCreatedAt").queryField("listMessagesBySearchVisibilityAndCreatedAt"),
|
|
1843
1843
|
])
|
|
1844
1844
|
.authorization((allow) => [
|
|
1845
|
-
//
|
|
1846
|
-
|
|
1847
|
-
allow.group(
|
|
1845
|
+
// Messages belong to their author, and the backend stamps the author: people read Messages here and write them only through
|
|
1846
|
+
// postMessage, editMessage, deleteOwnMessage and moderateMessage (message-actions.ts). Functions and IAM callers write directly.
|
|
1847
|
+
allow.group("editor").to(["read"]),
|
|
1848
|
+
allow.group(adminGroup).to(["read"]),
|
|
1848
1849
|
allow.authenticated("identityPool").to(["read", "create", "update"]),
|
|
1849
|
-
// Messages belong to their author: the author edits (and soft-deletes) their own, and only an admin can remove someone else's.
|
|
1850
|
-
allow.ownerDefinedIn("authorSub").identityClaim("sub").to(["read", "update", "delete"]),
|
|
1851
1850
|
]),
|
|
1852
1851
|
|
|
1853
1852
|
MessageThread: a
|
package/amplify/data/schema.js
CHANGED
|
@@ -4,6 +4,7 @@ import { categoryAction } from "../functions/category-action/resource.js";
|
|
|
4
4
|
import { editorialChecksSchemaDefinitions } from "../functions/editorial-checks/schema.js";
|
|
5
5
|
import { automationViewSchemaDefinitions } from "../functions/automation-view/schema.js";
|
|
6
6
|
import { proposalActionsSchemaDefinitions } from "../functions/proposal-actions/schema.js";
|
|
7
|
+
import { messageActionsSchemaDefinitions } from "./message-actions.js";
|
|
7
8
|
import { monitoringViewSchemaDefinitions } from "../functions/monitoring-view/schema.js";
|
|
8
9
|
import { contentActions } from "../functions/content-actions/resource.js";
|
|
9
10
|
import { knowledgeQuery } from "../functions/knowledge-query/resource.js";
|
|
@@ -1629,23 +1630,24 @@ function buildBaseDefinitions(includeEditions) {
|
|
|
1629
1630
|
index("searchVisibility").sortKeys(["createdAt"]).name("messagesBySearchVisibilityCreatedAt").queryField("listMessagesBySearchVisibilityAndCreatedAt"),
|
|
1630
1631
|
])
|
|
1631
1632
|
.authorization((allow) => [
|
|
1632
|
-
//
|
|
1633
|
-
|
|
1634
|
-
allow.group(
|
|
1635
|
-
|
|
1636
|
-
allow.ownerDefinedIn("authorSub").identityClaim("sub").to(["read", "update", "delete"]),
|
|
1633
|
+
// Messages belong to their author, and the backend stamps the author: people read Messages here and write them only through
|
|
1634
|
+
// postMessage, editMessage, deleteOwnMessage and moderateMessage (message-actions.ts). Functions and IAM callers write directly.
|
|
1635
|
+
allow.group("editor").to(["read"]),
|
|
1636
|
+
allow.group(adminGroup).to(["read"]),
|
|
1637
1637
|
]),
|
|
1638
1638
|
MessageThread: realtimeModel({
|
|
1639
1639
|
id: a.id().required(),
|
|
1640
1640
|
threadKind: a.string().required(),
|
|
1641
1641
|
status: a.string().required(),
|
|
1642
|
-
title
|
|
1642
|
+
// The title and the creator are set when a conversation is created and read afterwards; a title changes only through
|
|
1643
|
+
// renameThread (its creator or an admin), and the creator never changes.
|
|
1644
|
+
title: a.string().required().authorization((allow) => [allow.group("editor").to(["read", "create"]), allow.group(adminGroup).to(["read", "create"])]),
|
|
1643
1645
|
summary: a.string(),
|
|
1644
1646
|
primaryAnchorKind: a.string(),
|
|
1645
1647
|
primaryAnchorId: a.id(),
|
|
1646
1648
|
primaryAnchorLineageId: a.id(),
|
|
1647
1649
|
primaryAnchorKey: a.string(),
|
|
1648
|
-
createdBySub: a.string(),
|
|
1650
|
+
createdBySub: a.string().authorization((allow) => [allow.group("editor").to(["read", "create"]), allow.group(adminGroup).to(["read", "create"])]),
|
|
1649
1651
|
createdByUserProfileId: a.id(),
|
|
1650
1652
|
createdByLabel: a.string(),
|
|
1651
1653
|
messageCount: a.integer(),
|
|
@@ -2130,6 +2132,7 @@ function buildBaseDefinitions(includeEditions) {
|
|
|
2130
2132
|
...monitoringViewSchemaDefinitions,
|
|
2131
2133
|
...automationViewSchemaDefinitions,
|
|
2132
2134
|
...proposalActionsSchemaDefinitions,
|
|
2135
|
+
...messageActionsSchemaDefinitions,
|
|
2133
2136
|
};
|
|
2134
2137
|
if (!includeEditions) {
|
|
2135
2138
|
const mutableDefinitions = definitions;
|
package/amplify/data/schema.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { categoryAction } from "../functions/category-action/resource";
|
|
|
4
4
|
import { editorialChecksSchemaDefinitions } from "../functions/editorial-checks/schema";
|
|
5
5
|
import { automationViewSchemaDefinitions } from "../functions/automation-view/schema";
|
|
6
6
|
import { proposalActionsSchemaDefinitions } from "../functions/proposal-actions/schema";
|
|
7
|
+
import { messageActionsSchemaDefinitions } from "./message-actions";
|
|
7
8
|
import { monitoringViewSchemaDefinitions } from "../functions/monitoring-view/schema";
|
|
8
9
|
import { contentActions } from "../functions/content-actions/resource";
|
|
9
10
|
import { knowledgeQuery } from "../functions/knowledge-query/resource";
|
|
@@ -1743,24 +1744,25 @@ const definitions = {
|
|
|
1743
1744
|
index("searchVisibility").sortKeys(["createdAt"]).name("messagesBySearchVisibilityCreatedAt").queryField("listMessagesBySearchVisibilityAndCreatedAt"),
|
|
1744
1745
|
])
|
|
1745
1746
|
.authorization((allow) => [
|
|
1746
|
-
//
|
|
1747
|
-
|
|
1748
|
-
allow.group(
|
|
1749
|
-
|
|
1750
|
-
allow.ownerDefinedIn("authorSub").identityClaim("sub").to(["read", "update", "delete"]),
|
|
1747
|
+
// Messages belong to their author, and the backend stamps the author: people read Messages here and write them only through
|
|
1748
|
+
// postMessage, editMessage, deleteOwnMessage and moderateMessage (message-actions.ts). Functions and IAM callers write directly.
|
|
1749
|
+
allow.group("editor").to(["read"]),
|
|
1750
|
+
allow.group(adminGroup).to(["read"]),
|
|
1751
1751
|
]),
|
|
1752
1752
|
|
|
1753
1753
|
MessageThread: realtimeModel({
|
|
1754
1754
|
id: a.id().required(),
|
|
1755
1755
|
threadKind: a.string().required(),
|
|
1756
1756
|
status: a.string().required(),
|
|
1757
|
-
title
|
|
1757
|
+
// The title and the creator are set when a conversation is created and read afterwards; a title changes only through
|
|
1758
|
+
// renameThread (its creator or an admin), and the creator never changes.
|
|
1759
|
+
title: a.string().required().authorization((allow) => [allow.group("editor").to(["read", "create"]), allow.group(adminGroup).to(["read", "create"])]),
|
|
1758
1760
|
summary: a.string(),
|
|
1759
1761
|
primaryAnchorKind: a.string(),
|
|
1760
1762
|
primaryAnchorId: a.id(),
|
|
1761
1763
|
primaryAnchorLineageId: a.id(),
|
|
1762
1764
|
primaryAnchorKey: a.string(),
|
|
1763
|
-
createdBySub: a.string(),
|
|
1765
|
+
createdBySub: a.string().authorization((allow) => [allow.group("editor").to(["read", "create"]), allow.group(adminGroup).to(["read", "create"])]),
|
|
1764
1766
|
createdByUserProfileId: a.id(),
|
|
1765
1767
|
createdByLabel: a.string(),
|
|
1766
1768
|
messageCount: a.integer(),
|
|
@@ -2260,6 +2262,7 @@ const definitions = {
|
|
|
2260
2262
|
...monitoringViewSchemaDefinitions,
|
|
2261
2263
|
...automationViewSchemaDefinitions,
|
|
2262
2264
|
...proposalActionsSchemaDefinitions,
|
|
2265
|
+
...messageActionsSchemaDefinitions,
|
|
2263
2266
|
};
|
|
2264
2267
|
if (!includeEditions) {
|
|
2265
2268
|
const mutableDefinitions = definitions as Record<string, unknown>;
|
|
@@ -60,6 +60,7 @@ import {
|
|
|
60
60
|
type ConversationAnchor,
|
|
61
61
|
type ConversationMessage,
|
|
62
62
|
} from "../lib/conversation";
|
|
63
|
+
import { postMessageArguments, postMessageMutation } from "../lib/post-message";
|
|
63
64
|
import { legacyStageKeysToLoad, type ContentItemVersionRecord } from "../lib/content-item-lifecycle";
|
|
64
65
|
import { getContentItemStageDefinitions } from "../lib/site-brand";
|
|
65
66
|
import { DOCTRINE_DEFINITIONS, getCategoryDoctrineDefinitions, type DoctrineCategory } from "../lib/doctrine";
|
|
@@ -220,40 +221,7 @@ const UPDATE_MESSAGE_THREAD_MUTATION = `
|
|
|
220
221
|
}
|
|
221
222
|
`;
|
|
222
223
|
|
|
223
|
-
const
|
|
224
|
-
mutation CreateMessage($input: CreateMessageInput!) {
|
|
225
|
-
createMessage(input: $input) {
|
|
226
|
-
id
|
|
227
|
-
messageKind
|
|
228
|
-
messageDomain
|
|
229
|
-
status
|
|
230
|
-
summary
|
|
231
|
-
source
|
|
232
|
-
importRunId
|
|
233
|
-
authorSub
|
|
234
|
-
authorUserProfileId
|
|
235
|
-
authorLabel
|
|
236
|
-
threadId
|
|
237
|
-
parentMessageId
|
|
238
|
-
sequenceNumber
|
|
239
|
-
role
|
|
240
|
-
messageType
|
|
241
|
-
content
|
|
242
|
-
semanticLayer
|
|
243
|
-
searchVisibility
|
|
244
|
-
responseTarget
|
|
245
|
-
responseStatus
|
|
246
|
-
responseOwner
|
|
247
|
-
responseStartedAt
|
|
248
|
-
responseCompletedAt
|
|
249
|
-
responseError
|
|
250
|
-
metadata
|
|
251
|
-
createdAt
|
|
252
|
-
updatedAt
|
|
253
|
-
newsroomFeedKey
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
`;
|
|
224
|
+
const POST_MESSAGE_SELECTION = "id messageKind messageDomain status summary source importRunId authorSub authorUserProfileId authorLabel threadId parentMessageId sequenceNumber role messageType content semanticLayer searchVisibility responseTarget responseStatus responseOwner responseStartedAt responseCompletedAt responseError metadata createdAt updatedAt newsroomFeedKey";
|
|
257
225
|
|
|
258
226
|
const UPDATE_MESSAGE_MUTATION = `
|
|
259
227
|
mutation UpdateMessage($input: UpdateMessageInput!) {
|
|
@@ -1434,10 +1402,12 @@ async function appendMessageToThread(thread: MessageThreadRecord, build: (sequen
|
|
|
1434
1402
|
// `createdAt` is part of the composite sort key of messagesByResponseTargetStatusCreatedAt, so AppSync refuses a Message that
|
|
1435
1403
|
// sets it without `responseStatus`. A person's message waits on no responder: COMPLETED, as the Python message contract writes.
|
|
1436
1404
|
built = { responseStatus: "COMPLETED", ...build(sequenceNumber, now) };
|
|
1437
|
-
|
|
1405
|
+
// The backend stamps the author from the signed-in identity (postMessage); nothing here names one.
|
|
1406
|
+
const args = postMessageArguments(built as unknown as Record<string, unknown>);
|
|
1407
|
+
return runGraphql<{ postMessage?: MessageRecord | null }>(postMessageMutation(args, POST_MESSAGE_SELECTION), args);
|
|
1438
1408
|
},
|
|
1439
1409
|
});
|
|
1440
|
-
const createdMessage = normalizeMessageRecord(created.
|
|
1410
|
+
const createdMessage = normalizeMessageRecord(created.postMessage ?? built!);
|
|
1441
1411
|
const updated = await runGraphql<{ updateMessageThread?: MessageThreadRecord | null }>(
|
|
1442
1412
|
UPDATE_MESSAGE_THREAD_MUTATION,
|
|
1443
1413
|
{
|
|
@@ -1608,7 +1578,6 @@ export async function appendConversationMessage(input: {
|
|
|
1608
1578
|
status: "active",
|
|
1609
1579
|
summary: content.slice(0, 120),
|
|
1610
1580
|
source: "newsroom.conversation",
|
|
1611
|
-
authorSub: author.sub,
|
|
1612
1581
|
authorLabel: author.label,
|
|
1613
1582
|
threadId: thread.id,
|
|
1614
1583
|
parentMessageId: input.parentMessageId?.trim() || null,
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import { generateClient } from "aws-amplify/data";
|
|
4
4
|
import type { Schema } from "../amplify/data/resource";
|
|
5
5
|
import { configureAmplifyClient } from "../components/amplify-client-provider";
|
|
6
|
+
import { postMessageArguments, postMessageMutation } from "./post-message";
|
|
6
7
|
import { loadModelPayloadsForOwner, uploadModelPayloadForOwner } from "../components/news-desk-taxonomy-client";
|
|
7
8
|
|
|
8
9
|
const USER_POOL_AUTH_MODE = "userPool";
|
|
@@ -190,14 +191,6 @@ const UPDATE_CONSOLE_THREAD = `
|
|
|
190
191
|
}
|
|
191
192
|
`;
|
|
192
193
|
|
|
193
|
-
const CREATE_CONSOLE_MESSAGE_LEGACY = `
|
|
194
|
-
mutation CreateMessage($input: CreateMessageInput!) {
|
|
195
|
-
createMessage(input: $input) {
|
|
196
|
-
${CONSOLE_MESSAGE_FIELDS_LEGACY}
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
`;
|
|
200
|
-
|
|
201
194
|
export type ConsoleChatThread = {
|
|
202
195
|
id: string;
|
|
203
196
|
threadKind: string;
|
|
@@ -383,79 +376,6 @@ function chooseMessageSelectionSet(schemaFields: Set<string> | null): string {
|
|
|
383
376
|
return selected.join("\n ");
|
|
384
377
|
}
|
|
385
378
|
|
|
386
|
-
function fieldValuesForCreateMessageInput(input: CreateConsoleMessageInput): Record<string, unknown> {
|
|
387
|
-
return {
|
|
388
|
-
id: input.id,
|
|
389
|
-
messageKind: input.messageKind,
|
|
390
|
-
messageDomain: input.messageDomain,
|
|
391
|
-
status: input.status,
|
|
392
|
-
summary: input.summary ?? truncateSummary(input.content ?? null),
|
|
393
|
-
source: input.source,
|
|
394
|
-
importRunId: null,
|
|
395
|
-
authorSub: null,
|
|
396
|
-
authorUserProfileId: input.authorUserProfileId ?? null,
|
|
397
|
-
authorLabel: input.authorLabel,
|
|
398
|
-
threadId: input.threadId ?? null,
|
|
399
|
-
parentMessageId: input.parentMessageId ?? null,
|
|
400
|
-
sequenceNumber: input.sequenceNumber ?? null,
|
|
401
|
-
role: input.role ?? null,
|
|
402
|
-
messageType: input.messageType ?? null,
|
|
403
|
-
semanticLayer: input.semanticLayer,
|
|
404
|
-
searchVisibility: input.searchVisibility,
|
|
405
|
-
responseTarget: input.responseTarget,
|
|
406
|
-
responseStatus: input.responseStatus ?? null,
|
|
407
|
-
responseOwner: input.responseOwner ?? null,
|
|
408
|
-
responseStartedAt: input.responseStartedAt ?? null,
|
|
409
|
-
responseCompletedAt: input.responseCompletedAt ?? null,
|
|
410
|
-
responseError: input.responseError ?? null,
|
|
411
|
-
metadata: input.metadata,
|
|
412
|
-
createdAt: input.createdAt,
|
|
413
|
-
updatedAt: input.updatedAt,
|
|
414
|
-
newsroomFeedKey: input.newsroomFeedKey,
|
|
415
|
-
};
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
function buildCreateMessageInputCandidates(
|
|
419
|
-
input: CreateConsoleMessageInput,
|
|
420
|
-
availableFields: Set<string>,
|
|
421
|
-
): Record<string, unknown>[] {
|
|
422
|
-
const values = fieldValuesForCreateMessageInput(input);
|
|
423
|
-
const candidates: Record<string, unknown>[] = [];
|
|
424
|
-
const seen = new Set<string>();
|
|
425
|
-
|
|
426
|
-
const pushCandidate = (candidate: Record<string, unknown>) => {
|
|
427
|
-
const cleaned: Record<string, unknown> = {};
|
|
428
|
-
for (const [key, value] of Object.entries(candidate)) {
|
|
429
|
-
if (value !== undefined) cleaned[key] = value;
|
|
430
|
-
}
|
|
431
|
-
const signature = JSON.stringify(Object.keys(cleaned).sort().map((key) => [key, cleaned[key]]));
|
|
432
|
-
if (!seen.has(signature)) {
|
|
433
|
-
seen.add(signature);
|
|
434
|
-
candidates.push(cleaned);
|
|
435
|
-
}
|
|
436
|
-
};
|
|
437
|
-
|
|
438
|
-
if (availableFields.size) {
|
|
439
|
-
pushCandidate(sanitizeCreateMessageInput(input, availableFields));
|
|
440
|
-
} else {
|
|
441
|
-
pushCandidate(values);
|
|
442
|
-
pushCandidate({
|
|
443
|
-
id: values.id,
|
|
444
|
-
messageKind: values.messageKind,
|
|
445
|
-
messageDomain: values.messageDomain,
|
|
446
|
-
status: values.status,
|
|
447
|
-
summary: values.summary,
|
|
448
|
-
source: values.source,
|
|
449
|
-
authorLabel: values.authorLabel,
|
|
450
|
-
createdAt: values.createdAt,
|
|
451
|
-
updatedAt: values.updatedAt,
|
|
452
|
-
newsroomFeedKey: values.newsroomFeedKey,
|
|
453
|
-
});
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
return candidates;
|
|
457
|
-
}
|
|
458
|
-
|
|
459
379
|
async function loadConsoleSchemaSnapshot(): Promise<ConsoleSchemaSnapshot> {
|
|
460
380
|
if (consoleSchemaSnapshot) return consoleSchemaSnapshot;
|
|
461
381
|
if (consoleSchemaSnapshotPromise) return consoleSchemaSnapshotPromise;
|
|
@@ -540,70 +460,6 @@ function buildListMessagesQuery(
|
|
|
540
460
|
`;
|
|
541
461
|
}
|
|
542
462
|
|
|
543
|
-
function sanitizeCreateMessageInput(
|
|
544
|
-
input: CreateConsoleMessageInput,
|
|
545
|
-
availableFields: Set<string>,
|
|
546
|
-
): Record<string, unknown> {
|
|
547
|
-
const fieldValues: Record<string, unknown> = {
|
|
548
|
-
id: input.id,
|
|
549
|
-
messageKind: input.messageKind,
|
|
550
|
-
messageDomain: input.messageDomain,
|
|
551
|
-
status: input.status,
|
|
552
|
-
summary: input.summary ?? truncateSummary(input.content ?? null),
|
|
553
|
-
source: input.source,
|
|
554
|
-
importRunId: null,
|
|
555
|
-
authorSub: null,
|
|
556
|
-
authorUserProfileId: input.authorUserProfileId ?? null,
|
|
557
|
-
authorLabel: input.authorLabel,
|
|
558
|
-
threadId: input.threadId ?? null,
|
|
559
|
-
parentMessageId: input.parentMessageId ?? null,
|
|
560
|
-
sequenceNumber: input.sequenceNumber ?? null,
|
|
561
|
-
role: input.role ?? null,
|
|
562
|
-
messageType: input.messageType ?? null,
|
|
563
|
-
semanticLayer: input.semanticLayer,
|
|
564
|
-
searchVisibility: input.searchVisibility,
|
|
565
|
-
responseTarget: input.responseTarget,
|
|
566
|
-
responseStatus: input.responseStatus ?? null,
|
|
567
|
-
responseOwner: input.responseOwner ?? null,
|
|
568
|
-
responseStartedAt: input.responseStartedAt ?? null,
|
|
569
|
-
responseCompletedAt: input.responseCompletedAt ?? null,
|
|
570
|
-
responseError: input.responseError ?? null,
|
|
571
|
-
metadata: input.metadata,
|
|
572
|
-
createdAt: input.createdAt,
|
|
573
|
-
updatedAt: input.updatedAt,
|
|
574
|
-
newsroomFeedKey: input.newsroomFeedKey,
|
|
575
|
-
};
|
|
576
|
-
|
|
577
|
-
if (!availableFields.size) {
|
|
578
|
-
const conservativeFields = [
|
|
579
|
-
"id",
|
|
580
|
-
"messageKind",
|
|
581
|
-
"messageDomain",
|
|
582
|
-
"status",
|
|
583
|
-
"summary",
|
|
584
|
-
"source",
|
|
585
|
-
"authorLabel",
|
|
586
|
-
"createdAt",
|
|
587
|
-
"updatedAt",
|
|
588
|
-
"newsroomFeedKey",
|
|
589
|
-
] as const;
|
|
590
|
-
const conservative: Record<string, unknown> = {};
|
|
591
|
-
for (const key of conservativeFields) {
|
|
592
|
-
const value = fieldValues[key];
|
|
593
|
-
if (value !== undefined) conservative[key] = value;
|
|
594
|
-
}
|
|
595
|
-
return conservative;
|
|
596
|
-
}
|
|
597
|
-
|
|
598
|
-
const sanitized: Record<string, unknown> = {};
|
|
599
|
-
for (const [key, value] of Object.entries(fieldValues)) {
|
|
600
|
-
if (!availableFields.has(key)) continue;
|
|
601
|
-
if (value === undefined) continue;
|
|
602
|
-
sanitized[key] = value;
|
|
603
|
-
}
|
|
604
|
-
return sanitized;
|
|
605
|
-
}
|
|
606
|
-
|
|
607
463
|
function truncateSummary(value: string | null): string | null {
|
|
608
464
|
if (!value) return null;
|
|
609
465
|
const normalized = value.replace(/\s+/g, " ").trim();
|
|
@@ -733,53 +589,27 @@ export async function createConsoleMessage(input: CreateConsoleMessageInput): Pr
|
|
|
733
589
|
const selectionSets = snapshot.messageFields.size
|
|
734
590
|
? [chooseMessageSelectionSet(snapshot.messageFields)]
|
|
735
591
|
: [CONSOLE_MESSAGE_FIELDS, CONSOLE_MESSAGE_FIELDS_LEGACY];
|
|
736
|
-
|
|
592
|
+
// The backend stamps the author from the signed-in identity (postMessage): the browser names none and sets no timestamps.
|
|
593
|
+
const args = postMessageArguments({ ...input, summary: input.summary ?? truncateSummary(input.content ?? null) } as unknown as Record<string, unknown>);
|
|
737
594
|
let lastError: unknown = null;
|
|
738
595
|
|
|
739
596
|
for (const selectionSet of selectionSets) {
|
|
740
|
-
const createMutation = `
|
|
741
|
-
mutation CreateMessage($input: CreateMessageInput!) {
|
|
742
|
-
createMessage(input: $input) {
|
|
743
|
-
${selectionSet}
|
|
744
|
-
}
|
|
745
|
-
}
|
|
746
|
-
`;
|
|
747
|
-
for (const candidateInput of inputCandidates) {
|
|
748
|
-
try {
|
|
749
|
-
const response = await getGraphQLClient().graphql({
|
|
750
|
-
query: createMutation,
|
|
751
|
-
variables: { input: candidateInput },
|
|
752
|
-
authMode: USER_POOL_AUTH_MODE,
|
|
753
|
-
});
|
|
754
|
-
const created = normalizeConsoleMessageRecord(assertGraphQL<Record<string, unknown>>(response, "createMessage"));
|
|
755
|
-
await maybeUploadConsoleMessageBody(created.id, input.content);
|
|
756
|
-
return input.content?.trim() ? { ...created, content: input.content } : created;
|
|
757
|
-
} catch (error) {
|
|
758
|
-
if (!isUnavailableQueryError(error)) throw normalizeUnknownError(error, "createMessage");
|
|
759
|
-
lastError = error;
|
|
760
|
-
}
|
|
761
|
-
}
|
|
762
|
-
}
|
|
763
|
-
|
|
764
|
-
if (lastError) {
|
|
765
597
|
try {
|
|
766
598
|
const response = await getGraphQLClient().graphql({
|
|
767
|
-
query:
|
|
768
|
-
variables:
|
|
769
|
-
input: inputCandidates[inputCandidates.length - 1] ?? {},
|
|
770
|
-
},
|
|
599
|
+
query: postMessageMutation(args, selectionSet),
|
|
600
|
+
variables: args,
|
|
771
601
|
authMode: USER_POOL_AUTH_MODE,
|
|
772
602
|
});
|
|
773
|
-
const created = normalizeConsoleMessageRecord(assertGraphQL<Record<string, unknown>>(response, "
|
|
603
|
+
const created = normalizeConsoleMessageRecord(assertGraphQL<Record<string, unknown>>(response, "postMessage"));
|
|
774
604
|
await maybeUploadConsoleMessageBody(created.id, input.content);
|
|
775
605
|
return input.content?.trim() ? { ...created, content: input.content } : created;
|
|
776
606
|
} catch (error) {
|
|
777
|
-
if (!isUnavailableQueryError(error)) throw normalizeUnknownError(error, "
|
|
607
|
+
if (!isUnavailableQueryError(error)) throw normalizeUnknownError(error, "postMessage");
|
|
778
608
|
lastError = error;
|
|
779
609
|
}
|
|
780
610
|
}
|
|
781
611
|
|
|
782
|
-
throw normalizeUnknownError(lastError, "
|
|
612
|
+
throw normalizeUnknownError(lastError, "postMessage");
|
|
783
613
|
}
|
|
784
614
|
|
|
785
615
|
export async function updateConsoleMessage(
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Messages are created through the `postMessage` mutation, never through `createMessage`: the backend stamps the author from the
|
|
3
|
+
* signed-in identity, so a browser cannot name one. This is the one place that turns a Message record into that mutation and its
|
|
4
|
+
* arguments, shared by the conversation, forum and console chat writers. Authorship and timestamps are left out on purpose.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const ARGUMENT_TYPES = {
|
|
8
|
+
id: "ID!",
|
|
9
|
+
threadId: "ID!",
|
|
10
|
+
content: "String",
|
|
11
|
+
messageKind: "String!",
|
|
12
|
+
messageDomain: "String!",
|
|
13
|
+
summary: "String",
|
|
14
|
+
parentMessageId: "ID",
|
|
15
|
+
sequenceNumber: "Int",
|
|
16
|
+
messageType: "String",
|
|
17
|
+
role: "String",
|
|
18
|
+
source: "String",
|
|
19
|
+
authorLabel: "String",
|
|
20
|
+
semanticLayer: "String",
|
|
21
|
+
searchVisibility: "String",
|
|
22
|
+
responseTarget: "String",
|
|
23
|
+
responseStatus: "String",
|
|
24
|
+
responseOwner: "String",
|
|
25
|
+
responseStartedAt: "AWSDateTime",
|
|
26
|
+
responseCompletedAt: "AWSDateTime",
|
|
27
|
+
metadata: "AWSJSON",
|
|
28
|
+
newsroomFeedKey: "String",
|
|
29
|
+
} as const;
|
|
30
|
+
|
|
31
|
+
export type PostMessageArguments = Partial<Record<keyof typeof ARGUMENT_TYPES, string | number | null>>;
|
|
32
|
+
|
|
33
|
+
type MessageLike = Record<string, unknown>;
|
|
34
|
+
|
|
35
|
+
/** The arguments of `postMessage` from a Message record. Anything else on the record (authorSub, authorUserProfileId, createdAt, updatedAt) is dropped. */
|
|
36
|
+
export function postMessageArguments(record: MessageLike): PostMessageArguments {
|
|
37
|
+
const args: PostMessageArguments = {};
|
|
38
|
+
for (const key of Object.keys(ARGUMENT_TYPES) as Array<keyof typeof ARGUMENT_TYPES>) {
|
|
39
|
+
let value = record[key === "content" ? "content" : key];
|
|
40
|
+
if (key === "content") value = record.content ?? record.body;
|
|
41
|
+
if (value === undefined || value === null) continue;
|
|
42
|
+
if (key === "metadata") value = typeof value === "string" ? value : JSON.stringify(value);
|
|
43
|
+
args[key] = value as string | number;
|
|
44
|
+
}
|
|
45
|
+
return args;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** `mutation PostMessage($id: ID!, …) { postMessage(id: $id, …) { <selection> } }` declaring exactly the arguments given. */
|
|
49
|
+
export function postMessageMutation(args: PostMessageArguments, selection: string): string {
|
|
50
|
+
const keys = Object.keys(args) as Array<keyof typeof ARGUMENT_TYPES>;
|
|
51
|
+
const declared = keys.map((key) => `$${key}: ${ARGUMENT_TYPES[key]}`).join(", ");
|
|
52
|
+
const passed = keys.map((key) => `${key}: $${key}`).join(", ");
|
|
53
|
+
return `mutation PostMessage(${declared}) { postMessage(${passed}) { ${selection} } }`;
|
|
54
|
+
}
|
package/package.json
CHANGED
|
@@ -327,6 +327,7 @@
|
|
|
327
327
|
"./lib/papyrus-web-locations": "./lib/papyrus-web-locations.ts",
|
|
328
328
|
"./lib/pilobol-sample": "./lib/pilobol-sample.ts",
|
|
329
329
|
"./lib/plugin-settings": "./lib/plugin-settings.ts",
|
|
330
|
+
"./lib/post-message": "./lib/post-message.ts",
|
|
330
331
|
"./lib/presentation-footer": "./lib/presentation-footer.ts",
|
|
331
332
|
"./lib/pretext-layout": "./lib/pretext-layout.ts",
|
|
332
333
|
"./lib/priming-proof-report": "./lib/priming-proof-report.ts",
|
|
@@ -517,7 +518,7 @@
|
|
|
517
518
|
"optional": true
|
|
518
519
|
}
|
|
519
520
|
},
|
|
520
|
-
"version": "5.43.0-next.
|
|
521
|
+
"version": "5.43.0-next.50",
|
|
521
522
|
"dependencies": {
|
|
522
523
|
"@aws-amplify/adapter-nextjs": "^1.7.3",
|
|
523
524
|
"@aws-crypto/sha256-js": "5.2.0",
|
package/routes.manifest.json
CHANGED