@anthusai/papyrus 5.43.0-next.43 → 5.43.0-next.46

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.
@@ -1670,6 +1670,9 @@ const schema = a.schema({
1670
1670
  authorSub: a.string(),
1671
1671
  authorUserProfileId: a.id(),
1672
1672
  authorLabel: a.string(),
1673
+ editedAt: a.datetime(),
1674
+ deletedAt: a.datetime(),
1675
+ deletedBySub: a.string(),
1673
1676
  threadId: a.id(),
1674
1677
  parentMessageId: a.id(),
1675
1678
  sequenceNumber: a.integer(),
@@ -1704,8 +1707,12 @@ const schema = a.schema({
1704
1707
  index("searchVisibility").sortKeys(["createdAt"]).name("messagesBySearchVisibilityCreatedAt").queryField("listMessagesBySearchVisibilityAndCreatedAt"),
1705
1708
  ])
1706
1709
  .authorization((allow) => [
1707
- allow.groups(categoryWriteGroups).to(categoryAppendOnlyOperations),
1710
+ // One rule per group (a group may appear once per model): editors read and add; admins also delete, for moderation.
1711
+ allow.group("editor").to(categoryAppendOnlyOperations),
1712
+ allow.group(adminGroup).to(["read", "create", "delete"]),
1708
1713
  allow.authenticated("identityPool").to(["read", "create", "update"]),
1714
+ // Messages belong to their author: the author edits (and soft-deletes) their own, and only an admin can remove someone else's.
1715
+ allow.ownerDefinedIn("authorSub").identityClaim("sub").to(["read", "update", "delete"]),
1709
1716
  ]),
1710
1717
  MessageThread: a
1711
1718
  .model({
@@ -1786,6 +1786,9 @@ const schema = a.schema({
1786
1786
  authorSub: a.string(),
1787
1787
  authorUserProfileId: a.id(),
1788
1788
  authorLabel: a.string(),
1789
+ editedAt: a.datetime(),
1790
+ deletedAt: a.datetime(),
1791
+ deletedBySub: a.string(),
1789
1792
  threadId: a.id(),
1790
1793
  parentMessageId: a.id(),
1791
1794
  sequenceNumber: a.integer(),
@@ -1820,8 +1823,12 @@ const schema = a.schema({
1820
1823
  index("searchVisibility").sortKeys(["createdAt"]).name("messagesBySearchVisibilityCreatedAt").queryField("listMessagesBySearchVisibilityAndCreatedAt"),
1821
1824
  ])
1822
1825
  .authorization((allow) => [
1823
- allow.groups(categoryWriteGroups).to(categoryAppendOnlyOperations),
1826
+ // One rule per group (a group may appear once per model): editors read and add; admins also delete, for moderation.
1827
+ allow.group("editor").to(categoryAppendOnlyOperations),
1828
+ allow.group(adminGroup).to(["read", "create", "delete"]),
1824
1829
  allow.authenticated("identityPool").to(["read", "create", "update"]),
1830
+ // Messages belong to their author: the author edits (and soft-deletes) their own, and only an admin can remove someone else's.
1831
+ allow.ownerDefinedIn("authorSub").identityClaim("sub").to(["read", "update", "delete"]),
1825
1832
  ]),
1826
1833
 
1827
1834
  MessageThread: a
@@ -1573,6 +1573,9 @@ function buildBaseDefinitions(includeEditions) {
1573
1573
  authorSub: a.string(),
1574
1574
  authorUserProfileId: a.id(),
1575
1575
  authorLabel: a.string(),
1576
+ editedAt: a.datetime(),
1577
+ deletedAt: a.datetime(),
1578
+ deletedBySub: a.string(),
1576
1579
  threadId: a.id(),
1577
1580
  parentMessageId: a.id(),
1578
1581
  sequenceNumber: a.integer(),
@@ -1607,7 +1610,11 @@ function buildBaseDefinitions(includeEditions) {
1607
1610
  index("searchVisibility").sortKeys(["createdAt"]).name("messagesBySearchVisibilityCreatedAt").queryField("listMessagesBySearchVisibilityAndCreatedAt"),
1608
1611
  ])
1609
1612
  .authorization((allow) => [
1610
- allow.groups(categoryWriteGroups).to(categoryAppendOnlyOperations),
1613
+ // One rule per group (a group may appear once per model): editors read and add; admins also delete, for moderation.
1614
+ allow.group("editor").to(categoryAppendOnlyOperations),
1615
+ allow.group(adminGroup).to(["read", "create", "delete"]),
1616
+ // Messages belong to their author: the author edits (and soft-deletes) their own, and only an admin can remove someone else's.
1617
+ allow.ownerDefinedIn("authorSub").identityClaim("sub").to(["read", "update", "delete"]),
1611
1618
  ]),
1612
1619
  MessageThread: realtimeModel({
1613
1620
  id: a.id().required(),
@@ -1687,6 +1687,9 @@ const definitions = {
1687
1687
  authorSub: a.string(),
1688
1688
  authorUserProfileId: a.id(),
1689
1689
  authorLabel: a.string(),
1690
+ editedAt: a.datetime(),
1691
+ deletedAt: a.datetime(),
1692
+ deletedBySub: a.string(),
1690
1693
  threadId: a.id(),
1691
1694
  parentMessageId: a.id(),
1692
1695
  sequenceNumber: a.integer(),
@@ -1721,7 +1724,11 @@ const definitions = {
1721
1724
  index("searchVisibility").sortKeys(["createdAt"]).name("messagesBySearchVisibilityCreatedAt").queryField("listMessagesBySearchVisibilityAndCreatedAt"),
1722
1725
  ])
1723
1726
  .authorization((allow) => [
1724
- allow.groups(categoryWriteGroups).to(categoryAppendOnlyOperations),
1727
+ // One rule per group (a group may appear once per model): editors read and add; admins also delete, for moderation.
1728
+ allow.group("editor").to(categoryAppendOnlyOperations),
1729
+ allow.group(adminGroup).to(["read", "create", "delete"]),
1730
+ // Messages belong to their author: the author edits (and soft-deletes) their own, and only an admin can remove someone else's.
1731
+ allow.ownerDefinedIn("authorSub").identityClaim("sub").to(["read", "update", "delete"]),
1725
1732
  ]),
1726
1733
 
1727
1734
  MessageThread: realtimeModel({
@@ -1431,7 +1431,9 @@ async function appendMessageToThread(thread: MessageThreadRecord, build: (sequen
1431
1431
  list: () => listMessagesByThreadId(threadId, 1000),
1432
1432
  create: (sequenceNumber) => {
1433
1433
  now = new Date().toISOString();
1434
- built = build(sequenceNumber, now);
1434
+ // `createdAt` is part of the composite sort key of messagesByResponseTargetStatusCreatedAt, so AppSync refuses a Message that
1435
+ // sets it without `responseStatus`. A person's message waits on no responder: COMPLETED, as the Python message contract writes.
1436
+ built = { responseStatus: "COMPLETED", ...build(sequenceNumber, now) };
1435
1437
  return runGraphql<{ createMessage?: MessageRecord | null }>(CREATE_MESSAGE_MUTATION, { input: built });
1436
1438
  },
1437
1439
  });
@@ -1905,10 +1907,17 @@ async function runGraphql<T extends Record<string, unknown>>(
1905
1907
  const client = generateClient<Schema>() as unknown as {
1906
1908
  graphql: (options: Record<string, unknown>) => Promise<{ data?: T | null; errors?: unknown[] | null }>;
1907
1909
  };
1910
+ // The Amplify client rejects with the raw `{ data, errors }` response when the API returns errors; turn that into an
1911
+ // Error carrying the API's own words, so a refusal is said in words instead of as an unknown failure.
1908
1912
  const response = await client.graphql({
1909
1913
  query,
1910
1914
  variables,
1911
1915
  authMode: USER_POOL_AUTH_MODE,
1916
+ }).catch((failure: unknown) => {
1917
+ if (failure && typeof failure === "object" && Array.isArray((failure as { errors?: unknown }).errors)) {
1918
+ assertNoGraphQLErrors((failure as { errors: unknown[] }).errors);
1919
+ }
1920
+ throw failure;
1912
1921
  });
1913
1922
  assertNoGraphQLErrors(response.errors);
1914
1923
  return (response.data ?? {}) as T;
@@ -4,7 +4,7 @@ import { generateClient } from "aws-amplify/data";
4
4
  import type { Schema } from "../amplify/data/resource";
5
5
  import { isLocalPreviewEnabled } from "../lib/local-preview";
6
6
  import { createLocalPreviewReportingTrail, createLocalPreviewStoriesForReference } from "../lib/local-preview-seed";
7
- import { EMPTY_STORY_REPORTING_TRAIL, referencesByRole, siteNameOf, type ReferenceStory, type StoryReportingTrail, type TrailAssignment, type TrailAttachment, type TrailReference, type TrailThread, type TrailVersion } from "../lib/story-reporting-trail";
7
+ import { EMPTY_STORY_REPORTING_TRAIL, referencesByRole, siteNameOf, stageMovesOf, type ReferenceStory, type StoryReportingTrail, type TrailAssignment, type TrailAttachment, type TrailReference, type TrailThread, type TrailVersion } from "../lib/story-reporting-trail";
8
8
  import { configureAmplifyClient } from "./amplify-client-provider";
9
9
 
10
10
  type GraphqlResponse = { data?: Record<string, { items?: Record<string, unknown>[] | null; nextToken?: string | null } | null> | null; errors?: { message?: string }[] | null };
@@ -89,7 +89,7 @@ export async function loadStoryReportingTrail(lineageId: string, itemId: string,
89
89
  configureAmplifyClient();
90
90
  const client = generateClient<Schema>() as unknown as GraphqlClient;
91
91
  const [versionRows, assignmentRows, attachmentRows, relationRows] = await Promise.all([
92
- listAll(client, "listItemsByLineageAndVersion", "id versionNumber versionState status stage headline title changeReason versionCreatedBy versionCreatedAt updatedAt", ", $lineageId: ID!", ", lineageId: $lineageId", { lineageId }).catch(() => []),
92
+ listAll(client, "listItemsByLineageAndVersion", "id versionNumber versionState status stage headline title changeReason versionCreatedBy versionCreatedAt updatedAt metadata", ", $lineageId: ID!", ", lineageId: $lineageId", { lineageId }).catch(() => []),
93
93
  listAll(client, "listAssignmentsByItemLineageAndCreatedAt", "id title assignmentTypeKey status assigneeKey summary createdAt", ", $itemLineageId: ID!", ", itemLineageId: $itemLineageId", { itemLineageId: lineageId }).catch(() => []),
94
94
  listAll(client, "listModelAttachmentsByOwnerRoleAndSortKey", "id role filename mediaType byteSize", ", $ownerId: ID!", ", ownerId: $ownerId", { ownerId: itemId }).catch(() => []),
95
95
  listAll(client, "listSemanticRelations", "id predicate subjectKind subjectLineageId objectKind objectId objectLineageId metadata", ", $filter: ModelSemanticRelationFilterInput", ", filter: $filter", { filter: { or: [{ subjectLineageId: { eq: lineageId } }, { objectLineageId: { eq: lineageId } }] } }).catch(() => []),
@@ -124,5 +124,6 @@ export async function loadStoryReportingTrail(lineageId: string, itemId: string,
124
124
  };
125
125
  })));
126
126
  const attachments: TrailAttachment[] = attachmentRows.map(row => ({ id: String(row.id), role: text(row.role), filename: text(row.filename), mediaType: text(row.mediaType), byteSize: typeof row.byteSize === "number" ? row.byteSize : null }));
127
- return { ...EMPTY_STORY_REPORTING_TRAIL, versions, assignments, threads, references, attachments };
127
+ const latestRow = [...versionRows].sort((left, right) => Number(right.versionNumber ?? 0) - Number(left.versionNumber ?? 0))[0];
128
+ return { ...EMPTY_STORY_REPORTING_TRAIL, versions, assignments, threads, references, attachments, stageMoves: stageMovesOf(latestRow?.metadata) };
128
129
  }
@@ -91,6 +91,14 @@ export function StoryReportingTrailView({ citedKeys = [], demo = false, focusedR
91
91
  </li>)}
92
92
  </ol>
93
93
  </TrailSection>
94
+ {trail.stageMoves.length > 0 && <TrailSection count={trail.stageMoves.length} title="Stage moves">
95
+ <ol className="story-trail-list" data-story-trail-stage-moves>
96
+ {[...trail.stageMoves].reverse().map(move => <li key={`${move.at}-${move.to}`}>
97
+ <div className="story-trail-row"><strong>{move.from ?? "start"} → {move.to}</strong>{move.by && <span className="story-trail-chip">{move.by}</span>}<span className="story-trail-meta">{formatTimestamp(move.at)}</span></div>
98
+ {move.reason && <p className="story-trail-meta">{move.reason}</p>}
99
+ </li>)}
100
+ </ol>
101
+ </TrailSection>}
94
102
  <TrailSection count={trail.versions.length} title="Draft versions">
95
103
  <ol className="story-trail-list">
96
104
  {trail.versions.map((version, versionIndex) => <li key={version.id}>
@@ -17,6 +17,15 @@ export type TrailVersion = {
17
17
  updatedAt: string | null;
18
18
  };
19
19
 
20
+ /** One stage move of the story, read from the latest Item version's `metadata.stageMoves` (not from Messages). */
21
+ export type TrailStageMove = {
22
+ at: string;
23
+ from: string | null;
24
+ to: string;
25
+ by: string | null;
26
+ reason: string | null;
27
+ };
28
+
20
29
  export type TrailAssignmentEvent = {
21
30
  id: string;
22
31
  eventType: string;
@@ -89,12 +98,29 @@ export type StoryReportingTrail = {
89
98
  threads: TrailThread[];
90
99
  references: TrailReference[];
91
100
  attachments: TrailAttachment[];
101
+ stageMoves: TrailStageMove[];
92
102
  };
93
103
 
94
- export const EMPTY_STORY_REPORTING_TRAIL: StoryReportingTrail = { versions: [], assignments: [], threads: [], references: [], attachments: [] };
104
+ export const EMPTY_STORY_REPORTING_TRAIL: StoryReportingTrail = { versions: [], assignments: [], threads: [], references: [], attachments: [], stageMoves: [] };
105
+
106
+ /** The stage moves recorded on an Item version's metadata, oldest first. Anything that is not a well-formed move is skipped. */
107
+ export function stageMovesOf(metadata: unknown): TrailStageMove[] {
108
+ let parsed: unknown = metadata;
109
+ if (typeof metadata === "string") {
110
+ try { parsed = JSON.parse(metadata); } catch { return []; }
111
+ }
112
+ const moves = typeof parsed === "object" && parsed ? (parsed as Record<string, unknown>).stageMoves : null;
113
+ if (!Array.isArray(moves)) return [];
114
+ return moves.flatMap(entry => {
115
+ if (typeof entry !== "object" || !entry) return [];
116
+ const move = entry as Record<string, unknown>;
117
+ if (typeof move.at !== "string" || typeof move.to !== "string") return [];
118
+ return [{ at: move.at, from: typeof move.from === "string" ? move.from : null, to: move.to, by: typeof move.by === "string" ? move.by : null, reason: typeof move.reason === "string" ? move.reason : null }];
119
+ });
120
+ }
95
121
 
96
122
  export function storyReportingTrailIsEmpty(trail: StoryReportingTrail): boolean {
97
- return trail.assignments.length === 0 && trail.threads.length === 0 && trail.references.length === 0 && trail.attachments.length === 0 && trail.versions.length <= 1;
123
+ return trail.assignments.length === 0 && trail.threads.length === 0 && trail.stageMoves.length === 0 && trail.references.length === 0 && trail.attachments.length === 0 && trail.versions.length <= 1;
98
124
  }
99
125
 
100
126
  export type StoryFeedEntry =
package/package.json CHANGED
@@ -516,7 +516,7 @@
516
516
  "optional": true
517
517
  }
518
518
  },
519
- "version": "5.43.0-next.43",
519
+ "version": "5.43.0-next.46",
520
520
  "dependencies": {
521
521
  "@aws-amplify/adapter-nextjs": "^1.7.3",
522
522
  "@aws-crypto/sha256-js": "5.2.0",
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "5.43.0-next.43",
2
+ "version": "5.43.0-next.46",
3
3
  "routes": [
4
4
  {
5
5
  "file": "app/%5F_preview/[[...path]]/route.ts",