@anthusai/papyrus 2.0.0-next.43 → 2.0.0-next.44
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/functions/content-actions/handler.py +17 -2
- package/amplify/plugins/newsletter/papyrus.plugin.js +16 -2
- package/amplify/plugins/newsletter/papyrus.plugin.ts +19 -2
- package/amplify/plugins/newsletter/sending.js +3 -2
- package/amplify/plugins/newsletter/sending.ts +3 -2
- package/package.json +1 -1
- package/routes.manifest.json +1 -1
|
@@ -27,6 +27,7 @@ from papyrus_content.story_reporting import save_story_version
|
|
|
27
27
|
from papyrus_content.publication import ensure_publication_from_environment
|
|
28
28
|
from papyrus_content.editorial_checks import GraphQLEditorialStore, ensure_default_policies
|
|
29
29
|
from papyrus_content.reference_intake import IntakeError, intake_references
|
|
30
|
+
from papyrus_content.publish_alerts import PUBLISHED_TASK_KEY, announce_published
|
|
30
31
|
from papyrus_content.publishing import (
|
|
31
32
|
ItemFields,
|
|
32
33
|
PublishError,
|
|
@@ -119,11 +120,17 @@ def projection_context(client):
|
|
|
119
120
|
return context
|
|
120
121
|
|
|
121
122
|
|
|
122
|
-
def project_item_action(client, item_id: str, *, unpublished: bool) -> dict:
|
|
123
|
+
def project_item_action(client, item_id: str, *, unpublished: bool, published_item_id: str | None = None) -> dict:
|
|
124
|
+
"""Project one item. ``published_item_id`` is passed only for a real publish: the alert is announced once the page
|
|
125
|
+
is live, here when the projection succeeds, or by the scheduled retry (which carries the same marker) when it did not."""
|
|
123
126
|
if not projection_configured():
|
|
127
|
+
if published_item_id:
|
|
128
|
+
announce_published(item_id, published_item_id)
|
|
124
129
|
return {}
|
|
125
130
|
liveness = {} if unpublished else {"live": False}
|
|
126
131
|
task = {"task": "item", "itemId": item_id, "unpublished": unpublished}
|
|
132
|
+
if published_item_id:
|
|
133
|
+
task[PUBLISHED_TASK_KEY] = {"publishedItemId": published_item_id}
|
|
127
134
|
try:
|
|
128
135
|
report = project(projection_context(client), item_id, unpublished=unpublished)
|
|
129
136
|
except Exception as error: # noqa: BLE001 - a projection failure never fails the save
|
|
@@ -145,6 +152,8 @@ def project_item_action(client, item_id: str, *, unpublished: bool) -> dict:
|
|
|
145
152
|
schedule_unpurged_invalidation(report.unpurged)
|
|
146
153
|
if report.follow_up_item_pages:
|
|
147
154
|
schedule_projection_task({"task": "item-pages", "exceptItemId": item_id}, "follow-up")
|
|
155
|
+
if report.ok and published_item_id:
|
|
156
|
+
announce_published(item_id, published_item_id)
|
|
148
157
|
return response
|
|
149
158
|
|
|
150
159
|
|
|
@@ -201,6 +210,10 @@ def run_projection_task(task: dict) -> dict:
|
|
|
201
210
|
schedule_unpurged_invalidation(report.unpurged)
|
|
202
211
|
if not report.ok:
|
|
203
212
|
raise ProjectionTaskFailed("; ".join(report.errors))
|
|
213
|
+
published = task.get(PUBLISHED_TASK_KEY)
|
|
214
|
+
if task.get("task") == "item" and not task.get("unpublished") and isinstance(published, dict):
|
|
215
|
+
# A real publish (the CLI marks it, as does the newsroom's retry): the page is live, so tell the newsletter.
|
|
216
|
+
announce_published(task["itemId"], published.get("publishedItemId"))
|
|
204
217
|
return {"ok": True, "paths": [*report.written, *report.deleted]}
|
|
205
218
|
|
|
206
219
|
|
|
@@ -286,7 +299,9 @@ def publish_item_action(arguments: dict, actor: str) -> dict:
|
|
|
286
299
|
}
|
|
287
300
|
if result.changed:
|
|
288
301
|
response.update(site_triggers_for(client, arguments["id"]))
|
|
289
|
-
response.update(
|
|
302
|
+
response.update(
|
|
303
|
+
project_item_action(client, arguments["id"], unpublished=False, published_item_id=result.published_item_id if result.changed else None)
|
|
304
|
+
)
|
|
290
305
|
return response
|
|
291
306
|
|
|
292
307
|
|
|
@@ -15,9 +15,11 @@ import { pythonLambdaCode } from "../../functions/shared/python-bundle.js";
|
|
|
15
15
|
import { resolveNewsletterConfig } from "./config.js";
|
|
16
16
|
import { newsletterOps } from "./ops/resource.js";
|
|
17
17
|
import { NEWSLETTER_MUTATIONS, NEWSLETTER_QUERIES, newsletterSchemaDefinitions } from "./schema.js";
|
|
18
|
-
import { newsletterFunctionName, wireSending } from "./sending.js";
|
|
18
|
+
import { functionArn, newsletterFunctionName, wireSending } from "./sending.js";
|
|
19
19
|
export { resolveNewsletterConfig };
|
|
20
20
|
export const NEWSLETTER_OUTPUT_KEY = "newsletter";
|
|
21
|
+
/** Set on the site's content-actions function: it invokes this function (`item-published`) after a real publish. Unset: no alerts. */
|
|
22
|
+
export const NEWSLETTER_FUNCTION_ENV = "PAPYRUS_NEWSLETTER_FUNCTION";
|
|
21
23
|
export const NEWSLETTER_HANDLER = "papyrus_content.newsletter.handler.handler";
|
|
22
24
|
/** `boto3` is bundled because the runtime's copy may predate SESv2 `Simple.Headers` (List-Unsubscribe). */
|
|
23
25
|
export const NEWSLETTER_REQUIREMENTS = ["PyYAML>=6.0.2,<7", "citeproc-py>=0.8.2,<1", "anthus-markus==0.7.0", "Pillow>=11.3,<12", "boto3>=1.35.8,<2"];
|
|
@@ -70,13 +72,16 @@ export function newsletterEnvironment(config, tableName, keyParameter) {
|
|
|
70
72
|
FROM_NAME: config.fromName,
|
|
71
73
|
CONFIGURATION_SET: config.configurationSet,
|
|
72
74
|
ITEM_URL_STYLE: config.itemUrlStyle,
|
|
75
|
+
INSTANT_ALERTS: config.instantAlerts ? "true" : "false",
|
|
73
76
|
LISTS_JSON: JSON.stringify(config.lists.map((list) => ({
|
|
74
77
|
id: list.id,
|
|
75
78
|
name: list.name,
|
|
76
79
|
description: list.description,
|
|
77
80
|
kind: list.kind,
|
|
78
|
-
// A roundup's issue type, archive section and time zone are what drafting needs;
|
|
81
|
+
// A roundup's issue type, archive section and time zone are what drafting needs; its schedule stays in the stack.
|
|
79
82
|
...(list.kind === "roundup" ? { issueType: list.issueType, archivePath: list.archivePath, timezone: list.schedule.timezone } : {}),
|
|
83
|
+
// An alert's rule and approval decide which published items raise an alert and whether an admin approves it.
|
|
84
|
+
...(list.kind === "alert" ? { rule: list.rule, approval: list.approval } : {}),
|
|
80
85
|
}))),
|
|
81
86
|
THEME_JSON: JSON.stringify(config.theme),
|
|
82
87
|
ALARM_NAMES: Object.values(newsletterAlarmNames(config.siteId)).join(","),
|
|
@@ -229,6 +234,15 @@ export const newsletterPlugin = {
|
|
|
229
234
|
});
|
|
230
235
|
alarm.addAlarmAction(new SnsAction(alarmTopic));
|
|
231
236
|
}
|
|
237
|
+
// The publish path tells this function about a real publish. The ARN is spelled out (not referenced) so the content-actions
|
|
238
|
+
// stack does not depend on this one, and the grant is for this one function only.
|
|
239
|
+
const contentActions = context.contentActionsLambda;
|
|
240
|
+
contentActions.addEnvironment(NEWSLETTER_FUNCTION_ENV, newsletterFunctionName(config.siteId));
|
|
241
|
+
contentActions.addToRolePolicy(new PolicyStatement({
|
|
242
|
+
effect: Effect.ALLOW,
|
|
243
|
+
actions: ["lambda:InvokeFunction"],
|
|
244
|
+
resources: [functionArn(Stack.of(contentActions), newsletterFunctionName(config.siteId))],
|
|
245
|
+
}));
|
|
232
246
|
lambda.addPermission("InvokeFromCmsCompute", {
|
|
233
247
|
principal: new ArnPrincipal(config.computeRoleArn),
|
|
234
248
|
action: "lambda:InvokeFunction",
|
|
@@ -16,12 +16,14 @@ import type { PapyrusPlugin, PluginManifest, PluginWiringContext } from "../type
|
|
|
16
16
|
import { resolveNewsletterConfig, type NewsletterConfig } from "./config";
|
|
17
17
|
import { newsletterOps } from "./ops/resource";
|
|
18
18
|
import { NEWSLETTER_MUTATIONS, NEWSLETTER_QUERIES, newsletterSchemaDefinitions } from "./schema";
|
|
19
|
-
import { newsletterFunctionName, wireSending } from "./sending";
|
|
19
|
+
import { functionArn, newsletterFunctionName, wireSending } from "./sending";
|
|
20
20
|
|
|
21
21
|
export { resolveNewsletterConfig };
|
|
22
22
|
export type { NewsletterConfig };
|
|
23
23
|
|
|
24
24
|
export const NEWSLETTER_OUTPUT_KEY = "newsletter";
|
|
25
|
+
/** Set on the site's content-actions function: it invokes this function (`item-published`) after a real publish. Unset: no alerts. */
|
|
26
|
+
export const NEWSLETTER_FUNCTION_ENV = "PAPYRUS_NEWSLETTER_FUNCTION";
|
|
25
27
|
export const NEWSLETTER_HANDLER = "papyrus_content.newsletter.handler.handler";
|
|
26
28
|
/** `boto3` is bundled because the runtime's copy may predate SESv2 `Simple.Headers` (List-Unsubscribe). */
|
|
27
29
|
export const NEWSLETTER_REQUIREMENTS = ["PyYAML>=6.0.2,<7", "citeproc-py>=0.8.2,<1", "anthus-markus==0.7.0", "Pillow>=11.3,<12", "boto3>=1.35.8,<2"];
|
|
@@ -83,14 +85,17 @@ export function newsletterEnvironment(config: NewsletterConfig, tableName: strin
|
|
|
83
85
|
FROM_NAME: config.fromName,
|
|
84
86
|
CONFIGURATION_SET: config.configurationSet,
|
|
85
87
|
ITEM_URL_STYLE: config.itemUrlStyle,
|
|
88
|
+
INSTANT_ALERTS: config.instantAlerts ? "true" : "false",
|
|
86
89
|
LISTS_JSON: JSON.stringify(
|
|
87
90
|
config.lists.map((list) => ({
|
|
88
91
|
id: list.id,
|
|
89
92
|
name: list.name,
|
|
90
93
|
description: list.description,
|
|
91
94
|
kind: list.kind,
|
|
92
|
-
// A roundup's issue type, archive section and time zone are what drafting needs;
|
|
95
|
+
// A roundup's issue type, archive section and time zone are what drafting needs; its schedule stays in the stack.
|
|
93
96
|
...(list.kind === "roundup" ? { issueType: list.issueType, archivePath: list.archivePath, timezone: list.schedule.timezone } : {}),
|
|
97
|
+
// An alert's rule and approval decide which published items raise an alert and whether an admin approves it.
|
|
98
|
+
...(list.kind === "alert" ? { rule: list.rule, approval: list.approval } : {}),
|
|
94
99
|
})),
|
|
95
100
|
),
|
|
96
101
|
THEME_JSON: JSON.stringify(config.theme),
|
|
@@ -268,6 +273,18 @@ export const newsletterPlugin: PapyrusPlugin<NewsletterConfig, { newsletterOps:
|
|
|
268
273
|
alarm.addAlarmAction(new SnsAction(alarmTopic));
|
|
269
274
|
}
|
|
270
275
|
|
|
276
|
+
// The publish path tells this function about a real publish. The ARN is spelled out (not referenced) so the content-actions
|
|
277
|
+
// stack does not depend on this one, and the grant is for this one function only.
|
|
278
|
+
const contentActions = context.contentActionsLambda;
|
|
279
|
+
contentActions.addEnvironment(NEWSLETTER_FUNCTION_ENV, newsletterFunctionName(config.siteId));
|
|
280
|
+
contentActions.addToRolePolicy(
|
|
281
|
+
new PolicyStatement({
|
|
282
|
+
effect: Effect.ALLOW,
|
|
283
|
+
actions: ["lambda:InvokeFunction"],
|
|
284
|
+
resources: [functionArn(Stack.of(contentActions), newsletterFunctionName(config.siteId))],
|
|
285
|
+
}),
|
|
286
|
+
);
|
|
287
|
+
|
|
271
288
|
lambda.addPermission("InvokeFromCmsCompute", {
|
|
272
289
|
principal: new ArnPrincipal(config.computeRoleArn),
|
|
273
290
|
action: "lambda:InvokeFunction",
|
|
@@ -13,9 +13,10 @@ export const SCHEDULE_NAME_PREFIX = "papyrus-nl-";
|
|
|
13
13
|
export const newsletterFunctionName = (siteId) => `papyrus-newsletter-${siteId}`;
|
|
14
14
|
export const newsletterWorkerName = (siteId) => `papyrus-newsletter-send-${siteId}`;
|
|
15
15
|
export const newsletterSendQueueName = (siteId) => `papyrus-newsletter-send-${siteId}`;
|
|
16
|
-
/** The AppSync fields the `newsletter` function reads and writes through the content API (the roundup draft
|
|
16
|
+
/** The AppSync fields the `newsletter` function reads and writes through the content API (the roundup draft, the issue's Item, and the published item an alert is about). */
|
|
17
17
|
export const NEWSLETTER_APPSYNC_FIELDS = [
|
|
18
18
|
"Query/fields/getItem",
|
|
19
|
+
"Query/fields/getPublishedItem",
|
|
19
20
|
"Query/fields/listPublishedItems",
|
|
20
21
|
"Mutation/fields/createItem",
|
|
21
22
|
"Mutation/fields/updateItem",
|
|
@@ -33,7 +34,7 @@ export function functionArn(stack, name) {
|
|
|
33
34
|
/**
|
|
34
35
|
* Everything that sends: the queue and its dead letter queue, the send worker, the scheduler role and the per-list
|
|
35
36
|
* draft schedules, and the IAM they need. Nothing here lets the function reach anything but its own queue, its own
|
|
36
|
-
* schedules (`papyrus-nl-<siteId>-*`), two postal address parameters and
|
|
37
|
+
* schedules (`papyrus-nl-<siteId>-*`), two postal address parameters and five content API fields.
|
|
37
38
|
*/
|
|
38
39
|
export function wireSending(inputs) {
|
|
39
40
|
const { stack, config, context, backend, table, lambda, environment, keyParameterArn, sesResources, code } = inputs;
|
|
@@ -19,9 +19,10 @@ export const newsletterFunctionName = (siteId: string) => `papyrus-newsletter-${
|
|
|
19
19
|
export const newsletterWorkerName = (siteId: string) => `papyrus-newsletter-send-${siteId}`;
|
|
20
20
|
export const newsletterSendQueueName = (siteId: string) => `papyrus-newsletter-send-${siteId}`;
|
|
21
21
|
|
|
22
|
-
/** The AppSync fields the `newsletter` function reads and writes through the content API (the roundup draft
|
|
22
|
+
/** The AppSync fields the `newsletter` function reads and writes through the content API (the roundup draft, the issue's Item, and the published item an alert is about). */
|
|
23
23
|
export const NEWSLETTER_APPSYNC_FIELDS = [
|
|
24
24
|
"Query/fields/getItem",
|
|
25
|
+
"Query/fields/getPublishedItem",
|
|
25
26
|
"Query/fields/listPublishedItems",
|
|
26
27
|
"Mutation/fields/createItem",
|
|
27
28
|
"Mutation/fields/updateItem",
|
|
@@ -58,7 +59,7 @@ type SendingInputs = {
|
|
|
58
59
|
/**
|
|
59
60
|
* Everything that sends: the queue and its dead letter queue, the send worker, the scheduler role and the per-list
|
|
60
61
|
* draft schedules, and the IAM they need. Nothing here lets the function reach anything but its own queue, its own
|
|
61
|
-
* schedules (`papyrus-nl-<siteId>-*`), two postal address parameters and
|
|
62
|
+
* schedules (`papyrus-nl-<siteId>-*`), two postal address parameters and five content API fields.
|
|
62
63
|
*/
|
|
63
64
|
export function wireSending(inputs: SendingInputs): { queue: Queue; deadLetterQueue: Queue; worker: LambdaFunction; schedulerRole: Role } {
|
|
64
65
|
const { stack, config, context, backend, table, lambda, environment, keyParameterArn, sesResources, code } = inputs;
|
package/package.json
CHANGED
package/routes.manifest.json
CHANGED