@anthusai/papyrus 1.17.0 → 1.18.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/amplify/data/resource.js +13 -0
- package/amplify/data/resource.ts +14 -0
- package/amplify/data/schema.js +13 -0
- package/amplify/data/schema.ts +14 -0
- package/amplify/functions/assignment-action/handler.ts +24 -163
- package/amplify/functions/shared/editorial-context.ts +160 -0
- package/amplify/functions/shared/model-payloads.ts +9 -0
- package/bin/papyrus-infra.mjs +42 -4
- package/infra/build-specs.js +3 -0
- package/infra/build-specs.ts +4 -0
- package/infra/site-config.js +5 -1
- package/infra/site-config.ts +6 -1
- package/package.json +1 -1
- package/routes.manifest.json +1 -1
package/amplify/data/resource.js
CHANGED
|
@@ -808,6 +808,7 @@ const schema = a.schema({
|
|
|
808
808
|
categoryLineageId: a.id(),
|
|
809
809
|
}),
|
|
810
810
|
AssignmentContext: a.customType({
|
|
811
|
+
editorialContext: a.json().required(),
|
|
811
812
|
assignment: a.ref("AssignmentSummary"),
|
|
812
813
|
doctrine: a.ref("AssignmentDoctrineSummary").array().required(),
|
|
813
814
|
targets: a.ref("AssignmentTargetSummary").array().required(),
|
|
@@ -834,6 +835,18 @@ const schema = a.schema({
|
|
|
834
835
|
.returns(a.ref("AssignmentContext").array().required())
|
|
835
836
|
.authorization((allow) => [allow.groups(categoryWriteGroups)])
|
|
836
837
|
.handler(a.handler.function(assignmentAction)),
|
|
838
|
+
getEditorialContext: a
|
|
839
|
+
.query()
|
|
840
|
+
.arguments({
|
|
841
|
+
publicationKey: a.string().required(),
|
|
842
|
+
sectionId: a.id(),
|
|
843
|
+
storyLineageId: a.id(),
|
|
844
|
+
assignmentId: a.id(),
|
|
845
|
+
maxCharacters: a.integer(),
|
|
846
|
+
})
|
|
847
|
+
.returns(a.json().required())
|
|
848
|
+
.authorization((allow) => [allow.groups(categoryWriteGroups)])
|
|
849
|
+
.handler(a.handler.function(assignmentAction)),
|
|
837
850
|
getAssignmentContext: a
|
|
838
851
|
.query()
|
|
839
852
|
.arguments({
|
package/amplify/data/resource.ts
CHANGED
|
@@ -878,6 +878,7 @@ const schema = a.schema({
|
|
|
878
878
|
}),
|
|
879
879
|
|
|
880
880
|
AssignmentContext: a.customType({
|
|
881
|
+
editorialContext: a.json().required(),
|
|
881
882
|
assignment: a.ref("AssignmentSummary"),
|
|
882
883
|
doctrine: a.ref("AssignmentDoctrineSummary").array().required(),
|
|
883
884
|
targets: a.ref("AssignmentTargetSummary").array().required(),
|
|
@@ -907,6 +908,19 @@ const schema = a.schema({
|
|
|
907
908
|
.authorization((allow) => [allow.groups(categoryWriteGroups)])
|
|
908
909
|
.handler(a.handler.function(assignmentAction)),
|
|
909
910
|
|
|
911
|
+
getEditorialContext: a
|
|
912
|
+
.query()
|
|
913
|
+
.arguments({
|
|
914
|
+
publicationKey: a.string().required(),
|
|
915
|
+
sectionId: a.id(),
|
|
916
|
+
storyLineageId: a.id(),
|
|
917
|
+
assignmentId: a.id(),
|
|
918
|
+
maxCharacters: a.integer(),
|
|
919
|
+
})
|
|
920
|
+
.returns(a.json().required())
|
|
921
|
+
.authorization((allow) => [allow.groups(categoryWriteGroups)])
|
|
922
|
+
.handler(a.handler.function(assignmentAction)),
|
|
923
|
+
|
|
910
924
|
getAssignmentContext: a
|
|
911
925
|
.query()
|
|
912
926
|
.arguments({
|
package/amplify/data/schema.js
CHANGED
|
@@ -715,6 +715,7 @@ const baseDefinitions = {
|
|
|
715
715
|
categoryLineageId: a.id(),
|
|
716
716
|
}),
|
|
717
717
|
AssignmentContext: a.customType({
|
|
718
|
+
editorialContext: a.json().required(),
|
|
718
719
|
assignment: a.ref("AssignmentSummary"),
|
|
719
720
|
doctrine: a.ref("AssignmentDoctrineSummary").array().required(),
|
|
720
721
|
targets: a.ref("AssignmentTargetSummary").array().required(),
|
|
@@ -741,6 +742,18 @@ const baseDefinitions = {
|
|
|
741
742
|
.returns(a.ref("AssignmentContext").array().required())
|
|
742
743
|
.authorization((allow) => [allow.groups(categoryWriteGroups)])
|
|
743
744
|
.handler(a.handler.function(assignmentAction)),
|
|
745
|
+
getEditorialContext: a
|
|
746
|
+
.query()
|
|
747
|
+
.arguments({
|
|
748
|
+
publicationKey: a.string().required(),
|
|
749
|
+
sectionId: a.id(),
|
|
750
|
+
storyLineageId: a.id(),
|
|
751
|
+
assignmentId: a.id(),
|
|
752
|
+
maxCharacters: a.integer(),
|
|
753
|
+
})
|
|
754
|
+
.returns(a.json().required())
|
|
755
|
+
.authorization((allow) => [allow.groups(categoryWriteGroups)])
|
|
756
|
+
.handler(a.handler.function(assignmentAction)),
|
|
744
757
|
getAssignmentContext: a
|
|
745
758
|
.query()
|
|
746
759
|
.arguments({
|
package/amplify/data/schema.ts
CHANGED
|
@@ -779,6 +779,7 @@ const baseDefinitions = {
|
|
|
779
779
|
}),
|
|
780
780
|
|
|
781
781
|
AssignmentContext: a.customType({
|
|
782
|
+
editorialContext: a.json().required(),
|
|
782
783
|
assignment: a.ref("AssignmentSummary"),
|
|
783
784
|
doctrine: a.ref("AssignmentDoctrineSummary").array().required(),
|
|
784
785
|
targets: a.ref("AssignmentTargetSummary").array().required(),
|
|
@@ -808,6 +809,19 @@ const baseDefinitions = {
|
|
|
808
809
|
.authorization((allow) => [allow.groups(categoryWriteGroups)])
|
|
809
810
|
.handler(a.handler.function(assignmentAction)),
|
|
810
811
|
|
|
812
|
+
getEditorialContext: a
|
|
813
|
+
.query()
|
|
814
|
+
.arguments({
|
|
815
|
+
publicationKey: a.string().required(),
|
|
816
|
+
sectionId: a.id(),
|
|
817
|
+
storyLineageId: a.id(),
|
|
818
|
+
assignmentId: a.id(),
|
|
819
|
+
maxCharacters: a.integer(),
|
|
820
|
+
})
|
|
821
|
+
.returns(a.json().required())
|
|
822
|
+
.authorization((allow) => [allow.groups(categoryWriteGroups)])
|
|
823
|
+
.handler(a.handler.function(assignmentAction)),
|
|
824
|
+
|
|
811
825
|
getAssignmentContext: a
|
|
812
826
|
.query()
|
|
813
827
|
.arguments({
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { assignmentDoctrine, resolveEditorialContext, type EditorialClient } from "../shared/editorial-context";
|
|
2
|
+
import { tenantIdFromEnvironment } from "../../../lib/tenant-keys";
|
|
1
3
|
import { randomUUID } from "node:crypto";
|
|
2
4
|
import { spawnSync } from "node:child_process";
|
|
3
5
|
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
@@ -7,7 +9,7 @@ import { getAmplifyDataClientConfig } from "@aws-amplify/backend/function/runtim
|
|
|
7
9
|
import { Amplify } from "aws-amplify";
|
|
8
10
|
import { generateClient } from "aws-amplify/data";
|
|
9
11
|
import type { Schema } from "../../data/resource";
|
|
10
|
-
import { putJsonModelPayload, readJsonModelPayload } from "../shared/model-payloads";
|
|
12
|
+
import { putJsonModelPayload, readJsonModelPayload, readEditorialAttachment } from "../shared/model-payloads";
|
|
11
13
|
|
|
12
14
|
type AssignmentHandler =
|
|
13
15
|
| Schema["claimAssignment"]["functionHandler"]
|
|
@@ -17,6 +19,7 @@ type AssignmentHandler =
|
|
|
17
19
|
| Schema["reopenAssignment"]["functionHandler"]
|
|
18
20
|
| Schema["retryImmediateAssignment"]["functionHandler"]
|
|
19
21
|
| Schema["getAssignmentContext"]["functionHandler"]
|
|
22
|
+
| Schema["getEditorialContext"]["functionHandler"]
|
|
20
23
|
| Schema["listAssignmentQueue"]["functionHandler"]
|
|
21
24
|
| Schema["listAssignmentsForObject"]["functionHandler"];
|
|
22
25
|
type DataClient = ReturnType<typeof generateClient<Schema>>;
|
|
@@ -32,14 +35,12 @@ const IMMEDIATE_ASSIGNMENT_TYPES = new Set(["procedure.run"]);
|
|
|
32
35
|
const NEWSROOM_SUMMARY_PAYLOAD_ID = "knowledge-raw-payload-newsroom-summary-current";
|
|
33
36
|
const NEWSROOM_SUMMARY_PAYLOAD_OWNER_KIND = "knowledgeRawPayload";
|
|
34
37
|
const SUMMARY_STALE_AFTER_MS = 15 * 60 * 1000;
|
|
35
|
-
const PUBLICATION_DOCTRINE_DEFINITIONS = [
|
|
36
|
-
{ scope: "publication", kind: "mission", label: "Editorial Mission", slug: "editorial-doctrine-mission" },
|
|
37
|
-
{ scope: "publication", kind: "policy", label: "Editorial Policy", slug: "editorial-doctrine-policy" },
|
|
38
|
-
] as const;
|
|
39
38
|
let clientPromise: Promise<DataClient> | null = null;
|
|
39
|
+
let editorialEndpoint: string | undefined;
|
|
40
40
|
|
|
41
41
|
export const handler = async (event: any): Promise<any> => {
|
|
42
42
|
const fieldName = normalizeRequiredString(event.info?.fieldName ?? event.fieldName, "fieldName");
|
|
43
|
+
if (fieldName === "getEditorialContext") return getEditorialContext(event as Parameters<Schema["getEditorialContext"]["functionHandler"]>[0]);
|
|
43
44
|
if (fieldName === "getAssignmentContext") return getAssignmentContext(event as Parameters<Schema["getAssignmentContext"]["functionHandler"]>[0]);
|
|
44
45
|
if (fieldName === "listAssignmentQueue") return listAssignmentQueue(event as Parameters<Schema["listAssignmentQueue"]["functionHandler"]>[0]);
|
|
45
46
|
if (fieldName === "listAssignmentsForObject") return listAssignmentsForObject(event as Parameters<Schema["listAssignmentsForObject"]["functionHandler"]>[0]);
|
|
@@ -315,6 +316,20 @@ async function listAssignmentsForObject(event: Parameters<Schema["listAssignment
|
|
|
315
316
|
return contexts;
|
|
316
317
|
}
|
|
317
318
|
|
|
319
|
+
async function editorialContext(client: DataClient, scope: { publicationKey?: string; sectionId?: string | null; storyLineageId?: string | null; assignmentId?: string | null; maxCharacters?: number | null }) {
|
|
320
|
+
if (!editorialEndpoint) throw new Error("Editorial context requires the configured backend endpoint.");
|
|
321
|
+
return resolveEditorialContext(client as unknown as EditorialClient, {
|
|
322
|
+
...scope, endpoint: editorialEndpoint, tenantId: tenantIdFromEnvironment(), readAttachment: readEditorialAttachment,
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
async function getEditorialContext(event: Parameters<Schema["getEditorialContext"]["functionHandler"]>[0]) {
|
|
327
|
+
const client = await getDataClient();
|
|
328
|
+
return JSON.stringify(await editorialContext(client, {
|
|
329
|
+
...event.arguments, publicationKey: normalizeRequiredString(event.arguments.publicationKey, "publicationKey"),
|
|
330
|
+
}));
|
|
331
|
+
}
|
|
332
|
+
|
|
318
333
|
async function buildAssignmentContext(client: DataClient, assignment: any) {
|
|
319
334
|
const [relations, events] = await Promise.all([
|
|
320
335
|
listRelationPages(client, "listSemanticRelationsBySubjectState", { subjectStateKey: semanticStateKey("assignment", assignment.id) }),
|
|
@@ -329,172 +344,17 @@ async function buildAssignmentContext(client: DataClient, assignment: any) {
|
|
|
329
344
|
label: targetLabelFromRelation(relation),
|
|
330
345
|
detail: relation.predicate,
|
|
331
346
|
}));
|
|
332
|
-
const
|
|
333
|
-
const doctrine =
|
|
347
|
+
const pack = await editorialContext(client, { assignmentId: assignment.id });
|
|
348
|
+
const doctrine = assignmentDoctrine(pack);
|
|
334
349
|
return {
|
|
335
350
|
assignment,
|
|
351
|
+
editorialContext: JSON.stringify(pack),
|
|
336
352
|
doctrine,
|
|
337
353
|
targets,
|
|
338
354
|
events,
|
|
339
355
|
};
|
|
340
356
|
}
|
|
341
357
|
|
|
342
|
-
async function buildAssignmentDoctrineContext(client: DataClient, rootDeskCategory: any | null): Promise<any[]> {
|
|
343
|
-
const definitions = [
|
|
344
|
-
...PUBLICATION_DOCTRINE_DEFINITIONS,
|
|
345
|
-
...(rootDeskCategory ? [
|
|
346
|
-
{
|
|
347
|
-
scope: "desk",
|
|
348
|
-
kind: "mission",
|
|
349
|
-
label: "Desk Mission",
|
|
350
|
-
slug: deskDoctrineSlug(rootDeskCategory.categoryKey, "mission"),
|
|
351
|
-
categoryKey: rootDeskCategory.categoryKey,
|
|
352
|
-
categoryLineageId: rootDeskCategory.lineageId ?? rootDeskCategory.id,
|
|
353
|
-
},
|
|
354
|
-
{
|
|
355
|
-
scope: "desk",
|
|
356
|
-
kind: "policy",
|
|
357
|
-
label: "Desk Policies",
|
|
358
|
-
slug: deskDoctrineSlug(rootDeskCategory.categoryKey, "policy"),
|
|
359
|
-
categoryKey: rootDeskCategory.categoryKey,
|
|
360
|
-
categoryLineageId: rootDeskCategory.lineageId ?? rootDeskCategory.id,
|
|
361
|
-
},
|
|
362
|
-
] : []),
|
|
363
|
-
];
|
|
364
|
-
|
|
365
|
-
const context = [];
|
|
366
|
-
for (const definition of definitions) {
|
|
367
|
-
const record = await getDoctrineRecordBySlug(client, definition.slug);
|
|
368
|
-
if (!record) continue;
|
|
369
|
-
context.push({
|
|
370
|
-
scope: definition.scope,
|
|
371
|
-
kind: definition.kind,
|
|
372
|
-
label: definition.label,
|
|
373
|
-
slug: definition.slug,
|
|
374
|
-
body: compactStringArray(record.body),
|
|
375
|
-
categoryKey: "categoryKey" in definition ? definition.categoryKey : null,
|
|
376
|
-
categoryLineageId: "categoryLineageId" in definition ? definition.categoryLineageId : null,
|
|
377
|
-
});
|
|
378
|
-
}
|
|
379
|
-
return context;
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
async function resolveRootDeskCategory(client: DataClient, assignment: any, relations: any[]): Promise<any | null> {
|
|
383
|
-
const categoryLineageId = assignmentCategoryLineageIdFromRelations(relations);
|
|
384
|
-
if (categoryLineageId) {
|
|
385
|
-
const category = await getCurrentCategoryByLineage(client, categoryLineageId);
|
|
386
|
-
return category ? resolveRootCategory(client, category) : null;
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
const categoryKey = normalizeOptionalString(assignment.primaryFocusCategoryKey);
|
|
390
|
-
if (categoryKey && assignment.categorySetId) {
|
|
391
|
-
const category = await getCurrentCategoryBySetAndKey(client, assignment.categorySetId, categoryKey);
|
|
392
|
-
return category ? resolveRootCategory(client, category) : null;
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
return null;
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
function assignmentCategoryLineageIdFromRelations(relations: any[]): string | null {
|
|
399
|
-
const direct = relations.find((relation) => (
|
|
400
|
-
relation.relationState === "current"
|
|
401
|
-
&& relation.objectKind === "category"
|
|
402
|
-
&& relation.predicate === "requests_work_on"
|
|
403
|
-
)) ?? relations.find((relation) => (
|
|
404
|
-
relation.relationState === "current"
|
|
405
|
-
&& relation.objectKind === "category"
|
|
406
|
-
));
|
|
407
|
-
return normalizeOptionalString(direct?.objectLineageId);
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
async function resolveRootCategory(client: DataClient, category: any): Promise<any | null> {
|
|
411
|
-
let current = category;
|
|
412
|
-
for (let depth = 0; depth < 10; depth += 1) {
|
|
413
|
-
const parentCategoryKey = normalizeOptionalString(current?.parentCategoryKey);
|
|
414
|
-
if (!parentCategoryKey) return current;
|
|
415
|
-
const parent = await getCurrentCategoryBySetAndKey(client, current.categorySetId, parentCategoryKey);
|
|
416
|
-
if (!parent) return current;
|
|
417
|
-
current = parent;
|
|
418
|
-
}
|
|
419
|
-
return current;
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
async function getCurrentCategoryByLineage(client: DataClient, lineageId: string): Promise<any | null> {
|
|
423
|
-
const model = client.models.Category as any;
|
|
424
|
-
const query = model.listCategoriesByLineageAndVersion;
|
|
425
|
-
if (typeof query !== "function") return null;
|
|
426
|
-
const categories: any[] = [];
|
|
427
|
-
let nextToken: string | null | undefined;
|
|
428
|
-
do {
|
|
429
|
-
const response: DataClientResult<any[]> = await query({ lineageId }, { limit: 100, nextToken });
|
|
430
|
-
assertNoDataErrors(response.errors, "list Category lineage");
|
|
431
|
-
categories.push(...(response.data ?? []));
|
|
432
|
-
nextToken = response.nextToken;
|
|
433
|
-
} while (nextToken);
|
|
434
|
-
return selectCurrentVersion(categories);
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
async function getCurrentCategoryBySetAndKey(client: DataClient, categorySetId: string, categoryKey: string): Promise<any | null> {
|
|
438
|
-
const model = client.models.Category as any;
|
|
439
|
-
const query = model.listCategoriesBySetAndKey;
|
|
440
|
-
if (typeof query !== "function") return null;
|
|
441
|
-
const categories: any[] = [];
|
|
442
|
-
let nextToken: string | null | undefined;
|
|
443
|
-
do {
|
|
444
|
-
const response: DataClientResult<any[]> = await query({ categorySetId, categoryKey }, { limit: 100, nextToken });
|
|
445
|
-
assertNoDataErrors(response.errors, "list Category set/key");
|
|
446
|
-
categories.push(...(response.data ?? []));
|
|
447
|
-
nextToken = response.nextToken;
|
|
448
|
-
} while (nextToken);
|
|
449
|
-
return selectCurrentVersion(categories);
|
|
450
|
-
}
|
|
451
|
-
|
|
452
|
-
async function getDoctrineRecordBySlug(client: DataClient, slug: string): Promise<any | null> {
|
|
453
|
-
const model = client.models.Item as any;
|
|
454
|
-
const query = model.itemBySlug;
|
|
455
|
-
if (typeof query !== "function") return null;
|
|
456
|
-
const records: any[] = [];
|
|
457
|
-
let nextToken: string | null | undefined;
|
|
458
|
-
do {
|
|
459
|
-
const response: DataClientResult<any[]> = await query({ slug }, { limit: 100, nextToken });
|
|
460
|
-
assertNoDataErrors(response.errors, "query Item by slug");
|
|
461
|
-
records.push(...(response.data ?? []));
|
|
462
|
-
nextToken = response.nextToken;
|
|
463
|
-
} while (nextToken);
|
|
464
|
-
return records.find((record) => record.type === "doctrine" && record.status === "private")
|
|
465
|
-
?? records.find((record) => record.type === "doctrine")
|
|
466
|
-
?? null;
|
|
467
|
-
}
|
|
468
|
-
|
|
469
|
-
function selectCurrentVersion(records: any[]): any | null {
|
|
470
|
-
return [...records]
|
|
471
|
-
.filter((record) => record && record.status !== "deprecated")
|
|
472
|
-
.sort((left, right) => {
|
|
473
|
-
const stateDiff = versionStateRank(left.versionState) - versionStateRank(right.versionState);
|
|
474
|
-
if (stateDiff !== 0) return stateDiff;
|
|
475
|
-
return Number(right.versionNumber ?? 0) - Number(left.versionNumber ?? 0);
|
|
476
|
-
})[0] ?? null;
|
|
477
|
-
}
|
|
478
|
-
|
|
479
|
-
function versionStateRank(versionState: unknown): number {
|
|
480
|
-
if (versionState === "current") return 0;
|
|
481
|
-
if (versionState === "draft") return 1;
|
|
482
|
-
if (versionState === "superseded") return 8;
|
|
483
|
-
return 5;
|
|
484
|
-
}
|
|
485
|
-
|
|
486
|
-
function deskDoctrineSlug(categoryKey: string, kind: "mission" | "policy"): string {
|
|
487
|
-
return `desk-doctrine-${safeDoctrineKey(categoryKey)}-${kind}`;
|
|
488
|
-
}
|
|
489
|
-
|
|
490
|
-
function safeDoctrineKey(value: string): string {
|
|
491
|
-
return value
|
|
492
|
-
.trim()
|
|
493
|
-
.toLowerCase()
|
|
494
|
-
.replace(/[^a-z0-9]+/g, "-")
|
|
495
|
-
.replace(/^-+|-+$/g, "")
|
|
496
|
-
|| "topic";
|
|
497
|
-
}
|
|
498
358
|
|
|
499
359
|
function compactStringArray(value: unknown): string[] {
|
|
500
360
|
if (!Array.isArray(value)) return [];
|
|
@@ -1159,6 +1019,7 @@ async function getDataClient(): Promise<DataClient> {
|
|
|
1159
1019
|
clientPromise = (async () => {
|
|
1160
1020
|
const { resourceConfig, libraryOptions } = await getAmplifyDataClientConfig(process.env as never);
|
|
1161
1021
|
Amplify.configure(resourceConfig, libraryOptions);
|
|
1022
|
+
editorialEndpoint = resourceConfig.API?.GraphQL?.endpoint;
|
|
1162
1023
|
return generateClient<Schema>();
|
|
1163
1024
|
})();
|
|
1164
1025
|
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { tenantScopedValue } from "../../../lib/tenant-keys";
|
|
3
|
+
|
|
4
|
+
export type EditorialRecord = Record<string, any>;
|
|
5
|
+
type Result = { data?: EditorialRecord | EditorialRecord[] | null; errors?: unknown; nextToken?: string | null };
|
|
6
|
+
type Model = Record<string, (args: Record<string, any>, options?: Record<string, any>) => Promise<Result>>;
|
|
7
|
+
export type EditorialClient = { models: Record<string, Model> };
|
|
8
|
+
export type EditorialOptions = {
|
|
9
|
+
publicationKey?: string;
|
|
10
|
+
endpoint: string;
|
|
11
|
+
tenantId?: string;
|
|
12
|
+
sectionId?: string | null;
|
|
13
|
+
storyLineageId?: string | null;
|
|
14
|
+
assignmentId?: string | null;
|
|
15
|
+
maxCharacters?: number | null;
|
|
16
|
+
readAttachment: (attachment: EditorialRecord) => Promise<string>;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
function checked(response: Result, operation: string) {
|
|
20
|
+
if (response.errors && (!Array.isArray(response.errors) || response.errors.length)) throw new Error(`${operation}: ${JSON.stringify(response.errors)}`);
|
|
21
|
+
return response;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async function pages(client: EditorialClient, model: string, index: string, args: EditorialRecord) {
|
|
25
|
+
const rows: EditorialRecord[] = [];
|
|
26
|
+
let nextToken: string | null | undefined;
|
|
27
|
+
do {
|
|
28
|
+
const response = checked(await client.models[model][index](args, { limit: 100, nextToken }), `read ${model} ${index}`);
|
|
29
|
+
rows.push(...(response.data as EditorialRecord[] ?? []));
|
|
30
|
+
nextToken = response.nextToken;
|
|
31
|
+
} while (nextToken);
|
|
32
|
+
return rows;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async function get(client: EditorialClient, model: string, id: string) {
|
|
36
|
+
const row = checked(await client.models[model].get({ id }), `read ${model}`).data as EditorialRecord | null;
|
|
37
|
+
if (!row) throw new Error(`${model} not found: ${id}`);
|
|
38
|
+
return row;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function tenant(row: EditorialRecord, tenantId?: string) {
|
|
42
|
+
if ((row.tenantId || null) !== (tenantId || null)) throw new Error("Editorial record tenant does not match the configured authoring scope.");
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function canonical(value: any): any {
|
|
46
|
+
if (Array.isArray(value)) return value.map(canonical);
|
|
47
|
+
if (value !== null && typeof value === "object") return Object.fromEntries(Object.keys(value).sort().map(key => [key, canonical(value[key])]));
|
|
48
|
+
return value;
|
|
49
|
+
}
|
|
50
|
+
function digest(value: string) { return createHash("sha256").update(value, "utf8").digest("hex"); }
|
|
51
|
+
function entry(scope: string, kind: string, text: string, record: EditorialRecord) {
|
|
52
|
+
return { scope, kind, text, source: {
|
|
53
|
+
...Object.fromEntries(["id", "lineageId", "versionNumber", "updatedAt", "contentHash"].map(key => [key, record[key] ?? null])),
|
|
54
|
+
sha256: digest(text),
|
|
55
|
+
} };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function buildEditorialContext(publication: EditorialRecord, scope: EditorialRecord, entries: EditorialRecord[], warnings: string[], maxCharacters = 0) {
|
|
59
|
+
const lines = [`Editorial context: ${publication.key}`, "Source: cloud",
|
|
60
|
+
"Precedence: publication > section > assignment. Surface conflicts; narrower instructions do not silently override publication policy.",
|
|
61
|
+
"Private operating guidance; not evidence or reader-facing copy."];
|
|
62
|
+
for (const value of entries) lines.push("", `## ${value.scope}: ${value.kind}`, value.text);
|
|
63
|
+
const text = lines.join("\n") + "\n";
|
|
64
|
+
const totalCharacters = Array.from(text).length;
|
|
65
|
+
if (!Number.isInteger(maxCharacters) || maxCharacters < 0) throw new Error("Editorial context budget must be nonnegative.");
|
|
66
|
+
if (maxCharacters && totalCharacters > maxCharacters) throw new Error(`Editorial context budget ${maxCharacters} cannot hold ${totalCharacters} required characters; policy was not truncated.`);
|
|
67
|
+
const payload = { schemaVersion: 1, rendererVersion: "editorial-context/v1", publication, scope, sourceMode: "cloud",
|
|
68
|
+
precedence: ["publication", "section", "assignment"], entries, sources: entries.map(value => value.source), warnings, text, totalCharacters };
|
|
69
|
+
return { ...payload, contextHash: digest(JSON.stringify(canonical(payload))), resolvedAt: new Date().toISOString() };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Private source adapter for the same deterministic contract exposed by papyrus editorial context. */
|
|
73
|
+
export async function resolveEditorialContext(client: EditorialClient, options: EditorialOptions) {
|
|
74
|
+
if (!options.endpoint) throw new Error("Explicit publication endpoint is required for hosted editorial context.");
|
|
75
|
+
const corpora = await pages(client, "KnowledgeCorpus", "listKnowledgeCorporaByRoleAndName", { role: "canonical" });
|
|
76
|
+
const requested = options.publicationKey;
|
|
77
|
+
const normalized = requested?.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 140);
|
|
78
|
+
if (corpora.length !== 1 || !corpora[0].id?.startsWith("knowledge-corpus-") || corpora[0].role !== "canonical" || !corpora[0].name?.trim() ||
|
|
79
|
+
(requested !== undefined && (!normalized || corpora[0].id !== `knowledge-corpus-${normalized}`))) {
|
|
80
|
+
throw new Error("Requested publication does not match the hosted canonical corpus; missing or ambiguous publication binding.");
|
|
81
|
+
}
|
|
82
|
+
const publication = { key: corpora[0].id.slice("knowledge-corpus-".length), name: corpora[0].name,
|
|
83
|
+
corpusId: corpora[0].id, tenantId: options.tenantId ?? null, endpoint: options.endpoint };
|
|
84
|
+
const entries: EditorialRecord[] = [];
|
|
85
|
+
const warnings: string[] = [];
|
|
86
|
+
let sectionId = options.sectionId || null;
|
|
87
|
+
let storyLineageId = options.storyLineageId || null;
|
|
88
|
+
const assignmentId = options.assignmentId || null;
|
|
89
|
+
let assignment: EditorialRecord | undefined;
|
|
90
|
+
if (assignmentId) {
|
|
91
|
+
assignment = await get(client, "Assignment", assignmentId);
|
|
92
|
+
const assignedStory = assignment.itemLineageId;
|
|
93
|
+
if (storyLineageId && assignedStory && storyLineageId !== assignedStory) throw new Error("Requested story conflicts with assignment story lineage.");
|
|
94
|
+
storyLineageId ||= assignedStory || null;
|
|
95
|
+
const assignedSection = assignment.sectionKey || assignment.sectionId;
|
|
96
|
+
if (sectionId && assignedSection && sectionId !== assignedSection) throw new Error("Requested section conflicts with assignment section.");
|
|
97
|
+
sectionId ||= assignedSection || null;
|
|
98
|
+
warnings.push("Assignment policy snapshots are ignored; current publication and section records govern. Surface any conflicting assignment instructions.");
|
|
99
|
+
}
|
|
100
|
+
if (storyLineageId) {
|
|
101
|
+
const versions = await pages(client, "Item", "listItemsByLineageAndVersion", { lineageId: storyLineageId });
|
|
102
|
+
if (!versions.length) throw new Error(`Story not found: ${storyLineageId}`);
|
|
103
|
+
const story = versions.reduce((a, b) => Number(b.versionNumber || 0) > Number(a.versionNumber || 0) ? b : a);
|
|
104
|
+
tenant(story, options.tenantId);
|
|
105
|
+
if (sectionId && story.section && sectionId !== story.section) throw new Error("Requested section conflicts with story section.");
|
|
106
|
+
sectionId ||= story.section || null;
|
|
107
|
+
}
|
|
108
|
+
for (const kind of ["mission", "policy"]) {
|
|
109
|
+
const slug = `editorial-doctrine-${kind}`;
|
|
110
|
+
const rows = await pages(client, "Item", options.tenantId ? "itemByTenantSlug" : "itemBySlug",
|
|
111
|
+
options.tenantId ? { tenantSlug: tenantScopedValue(options.tenantId, slug) } : { slug });
|
|
112
|
+
const current = rows.filter(row => row.type === "doctrine" && row.status === "private" && row.versionState === "current");
|
|
113
|
+
if (current.length !== 1) throw new Error(`Publication ${kind}: expected exactly one current record, found ${current.length}.`);
|
|
114
|
+
const record = current[0];
|
|
115
|
+
tenant(record, options.tenantId);
|
|
116
|
+
const text = record.bodyMarkus ? String(record.bodyMarkus).trim() : (record.body ?? []).map((part: any) => String(part).trim()).filter(Boolean).join("\n\n");
|
|
117
|
+
if (!text) throw new Error(`Required publication ${kind} is empty.`);
|
|
118
|
+
entries.push(entry("publication", kind, text, record));
|
|
119
|
+
}
|
|
120
|
+
if (sectionId) {
|
|
121
|
+
const section = await get(client, "NewsroomSection", sectionId);
|
|
122
|
+
tenant(section, options.tenantId);
|
|
123
|
+
for (const [kind, key] of [["mission", "editorialMission"], ["policy", "editorialPolicy"], ["guidance", "assignmentGuidance"], ["kill criteria", "killCriteria"]]) {
|
|
124
|
+
const text = String(section[key] || "").trim();
|
|
125
|
+
if (text) entries.push(entry("section", kind, text, section));
|
|
126
|
+
else if (kind === "mission" || kind === "policy") warnings.push(`Section ${section.id} intentionally has an empty ${kind} slot; publication doctrine still applies.`);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
if (assignment) {
|
|
130
|
+
if (options.tenantId && !storyLineageId && !sectionId) throw new Error("A tenant assignment needs a scoped story or section to establish its publication.");
|
|
131
|
+
const attachments = await pages(client, "ModelAttachment", "listModelAttachmentsByOwnerRoleAndSortKey", { ownerId: assignment.id });
|
|
132
|
+
let foundBrief = false;
|
|
133
|
+
for (const [role, kind] of [["assignment_brief", "brief"], ["assignment_instructions", "instructions"]]) {
|
|
134
|
+
const matching = attachments.filter(row => row.role === role && row.status !== "deleted");
|
|
135
|
+
if (matching.length > 1) throw new Error(`Ambiguous assignment ${kind} attachments.`);
|
|
136
|
+
if (matching.length) {
|
|
137
|
+
const attachment = matching[0];
|
|
138
|
+
if (attachment.ownerKind !== "assignment" || attachment.ownerId !== assignment.id) throw new Error("Assignment attachment owner does not match the requested scope.");
|
|
139
|
+
const text = (await options.readAttachment(attachment)).trim();
|
|
140
|
+
if (!text) throw new Error(`Assignment ${kind} attachment is empty.`);
|
|
141
|
+
entries.push(entry("assignment", kind, text, attachment));
|
|
142
|
+
foundBrief ||= kind === "brief";
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
if (!foundBrief && assignment.summary) {
|
|
146
|
+
entries.push(entry("assignment", "brief", assignment.summary, assignment));
|
|
147
|
+
warnings.push("No assignment brief attachment exists; the Assignment summary is supplied as its brief.");
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return buildEditorialContext(publication, { sectionId, storyLineageId, assignmentId }, entries, warnings, options.maxCharacters ?? 0);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export function assignmentDoctrine(pack: Awaited<ReturnType<typeof resolveEditorialContext>>) {
|
|
154
|
+
return pack.entries.filter(value => value.scope !== "assignment").map(value => ({
|
|
155
|
+
scope: value.scope === "section" ? "desk" : "publication", kind: value.kind,
|
|
156
|
+
label: `${value.scope === "section" ? "Section" : "Editorial"} ${value.kind}`,
|
|
157
|
+
slug: value.scope === "section" ? `desk-section-${pack.scope.sectionId}-${value.kind.replaceAll(" ", "-")}` : `editorial-doctrine-${value.kind}`,
|
|
158
|
+
body: [value.text], ...(value.scope === "section" ? { categoryKey: pack.scope.sectionId, categoryLineageId: pack.scope.sectionId } : {}),
|
|
159
|
+
}));
|
|
160
|
+
}
|
|
@@ -135,6 +135,15 @@ async function readTextFromStorage(storagePath: string): Promise<string> {
|
|
|
135
135
|
}
|
|
136
136
|
}
|
|
137
137
|
|
|
138
|
+
/** Strict read for required private editorial payloads; storage failures propagate. */
|
|
139
|
+
export async function readEditorialAttachment(attachment: { storagePath?: string | null }): Promise<string> {
|
|
140
|
+
if (!attachment.storagePath) throw new Error("Editorial attachment has no storage path.");
|
|
141
|
+
const result = await s3.send(new GetObjectCommand({ Bucket: payloadBucketName(), Key: attachment.storagePath }));
|
|
142
|
+
const text = await result.Body?.transformToString("utf8");
|
|
143
|
+
if (!text?.trim()) throw new Error("Editorial attachment is empty.");
|
|
144
|
+
return text;
|
|
145
|
+
}
|
|
146
|
+
|
|
138
147
|
function isMissingStorageKeyError(error: unknown): boolean {
|
|
139
148
|
if (!error || typeof error !== "object") return false;
|
|
140
149
|
const record = error as Record<string, unknown>;
|
package/bin/papyrus-infra.mjs
CHANGED
|
@@ -3,11 +3,13 @@
|
|
|
3
3
|
* papyrus-infra synth --site infra/site.json [--out cdk.out] [--account A] [--region R]
|
|
4
4
|
* papyrus-infra synth --account-stack github-oidc [--out cdk.out] [--account A] [--region R]
|
|
5
5
|
* papyrus-infra print-buildspec --site infra/site.json --app cms-production|cms-staging|reader
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
6
|
+
* papyrus-infra deploy --site infra/site.json [--only-branch main] [--only-app-id ID] [--out cdk.out] [--account A] [--region R]
|
|
7
|
+
* Synthesizes the app-shell stack for a publication from its own site.json,
|
|
8
|
+
* prints one of the generated Amplify build specs, or deploys the stack from
|
|
9
|
+
* inside the Amplify build of the one branch that owns it.
|
|
9
10
|
*/
|
|
10
11
|
import fs from "node:fs";
|
|
12
|
+
import { spawnSync } from "node:child_process";
|
|
11
13
|
import path from "node:path";
|
|
12
14
|
import { createRequire } from "node:module";
|
|
13
15
|
import { buildSpecFor } from "../infra/build-specs.js";
|
|
@@ -17,6 +19,7 @@ const USAGE = [
|
|
|
17
19
|
"Usage: papyrus-infra synth --site infra/site.json [--out cdk.out] [--account A] [--region R]",
|
|
18
20
|
" papyrus-infra synth --account-stack github-oidc [--out cdk.out] [--account A] [--region R]",
|
|
19
21
|
" papyrus-infra print-buildspec --site infra/site.json --app cms-production|cms-staging|reader",
|
|
22
|
+
" papyrus-infra deploy --site infra/site.json [--only-branch main] [--only-app-id ID] [--out cdk.out] [--account A] [--region R]",
|
|
20
23
|
].join("\n");
|
|
21
24
|
|
|
22
25
|
const args = process.argv.slice(2);
|
|
@@ -25,7 +28,7 @@ const opt = (name, fallback) => {
|
|
|
25
28
|
return i >= 0 ? args[i + 1] : fallback;
|
|
26
29
|
};
|
|
27
30
|
const command = args[0];
|
|
28
|
-
if (command !== "synth" && command !== "print-buildspec") {
|
|
31
|
+
if (command !== "synth" && command !== "print-buildspec" && command !== "deploy") {
|
|
29
32
|
console.error(USAGE);
|
|
30
33
|
process.exit(command ? 1 : 0);
|
|
31
34
|
}
|
|
@@ -77,6 +80,41 @@ if (command === "print-buildspec") {
|
|
|
77
80
|
process.exit(0);
|
|
78
81
|
}
|
|
79
82
|
|
|
83
|
+
if (command === "deploy") {
|
|
84
|
+
const buildAppId = process.env.AWS_APP_ID;
|
|
85
|
+
const buildBranch = process.env.AWS_BRANCH;
|
|
86
|
+
const owningBranch = opt("only-branch", "main");
|
|
87
|
+
const owningAppId = opt("only-app-id");
|
|
88
|
+
if (!buildAppId || !buildBranch) {
|
|
89
|
+
console.log("papyrus-infra deploy: not running inside an Amplify build, skipping");
|
|
90
|
+
process.exit(0);
|
|
91
|
+
}
|
|
92
|
+
if (buildBranch !== owningBranch || (owningAppId && buildAppId !== owningAppId)) {
|
|
93
|
+
console.log(`papyrus-infra deploy: ${buildAppId}/${buildBranch} does not own the stack, skipping`);
|
|
94
|
+
process.exit(0);
|
|
95
|
+
}
|
|
96
|
+
const outputDirectory = path.resolve(opt("out", process.env.CDK_OUTDIR ?? "cdk.out"));
|
|
97
|
+
const synthCommand = [
|
|
98
|
+
process.execPath,
|
|
99
|
+
path.resolve(process.argv[1]),
|
|
100
|
+
"synth",
|
|
101
|
+
"--site",
|
|
102
|
+
path.resolve(opt("site", "infra/site.json")),
|
|
103
|
+
"--out",
|
|
104
|
+
outputDirectory,
|
|
105
|
+
"--account",
|
|
106
|
+
opt("account", process.env.CDK_DEFAULT_ACCOUNT ?? "335163751677"),
|
|
107
|
+
"--region",
|
|
108
|
+
opt("region", process.env.CDK_DEFAULT_REGION ?? "us-east-1"),
|
|
109
|
+
].map((part) => JSON.stringify(part)).join(" ");
|
|
110
|
+
const deployment = spawnSync(
|
|
111
|
+
"npx",
|
|
112
|
+
["--yes", "aws-cdk@2", "deploy", "--app", synthCommand, "--require-approval", "never", `AmplifyAppShell-${config.siteId}`],
|
|
113
|
+
{ stdio: "inherit" },
|
|
114
|
+
);
|
|
115
|
+
process.exit(deployment.status ?? 1);
|
|
116
|
+
}
|
|
117
|
+
|
|
80
118
|
let AmplifyAppShellStack;
|
|
81
119
|
try {
|
|
82
120
|
({ AmplifyAppShellStack } = await import("../infra/index.js"));
|
package/infra/build-specs.js
CHANGED
|
@@ -43,7 +43,9 @@ function frontendSection(preBuildCommands, buildCommands, artifactsDirectory, ca
|
|
|
43
43
|
lines.push(" build:", " commands:", ...commandLines(buildCommands, " "), " artifacts:", ` baseDirectory: ${artifactsDirectory}`, " files:", " - '**/*'", " cache:", " paths:", ...cachePaths.map((cachePath) => ` - ${cachePath}`));
|
|
44
44
|
return lines;
|
|
45
45
|
}
|
|
46
|
+
const DEPLOY_STACK_IN_BUILD = "npx papyrus-infra deploy --site infra/site.json --only-branch main";
|
|
46
47
|
export function cmsProductionBuildSpec(config) {
|
|
48
|
+
const deployStackCommands = config.cms?.deployStackInBuild ? [DEPLOY_STACK_IN_BUILD] : [];
|
|
47
49
|
return [
|
|
48
50
|
"version: 1",
|
|
49
51
|
"backend:",
|
|
@@ -52,6 +54,7 @@ export function cmsProductionBuildSpec(config) {
|
|
|
52
54
|
" commands:",
|
|
53
55
|
...commandLines([
|
|
54
56
|
NPM_CLEAN_INSTALL,
|
|
57
|
+
...deployStackCommands,
|
|
55
58
|
"if [ -z \"${PAPYRUS_CONSOLE_RESPONDER_IMAGE_URI:-}\" ]; then export PAPYRUS_CONSOLE_RESPONDER_ALLOW_LOCAL_BUILD=true; fi",
|
|
56
59
|
"npx ampx pipeline-deploy --branch $AWS_BRANCH --app-id $AWS_APP_ID",
|
|
57
60
|
], " "),
|
package/infra/build-specs.ts
CHANGED
|
@@ -67,7 +67,10 @@ function frontendSection(preBuildCommands: string[], buildCommands: string[], ar
|
|
|
67
67
|
return lines;
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
+
const DEPLOY_STACK_IN_BUILD = "npx papyrus-infra deploy --site infra/site.json --only-branch main";
|
|
71
|
+
|
|
70
72
|
export function cmsProductionBuildSpec(config: AmplifyAppShellSiteConfig): string {
|
|
73
|
+
const deployStackCommands = config.cms?.deployStackInBuild ? [DEPLOY_STACK_IN_BUILD] : [];
|
|
71
74
|
return [
|
|
72
75
|
"version: 1",
|
|
73
76
|
"backend:",
|
|
@@ -77,6 +80,7 @@ export function cmsProductionBuildSpec(config: AmplifyAppShellSiteConfig): strin
|
|
|
77
80
|
...commandLines(
|
|
78
81
|
[
|
|
79
82
|
NPM_CLEAN_INSTALL,
|
|
83
|
+
...deployStackCommands,
|
|
80
84
|
"if [ -z \"${PAPYRUS_CONSOLE_RESPONDER_IMAGE_URI:-}\" ]; then export PAPYRUS_CONSOLE_RESPONDER_ALLOW_LOCAL_BUILD=true; fi",
|
|
81
85
|
"npx ampx pipeline-deploy --branch $AWS_BRANCH --app-id $AWS_APP_ID",
|
|
82
86
|
],
|
package/infra/site-config.js
CHANGED
|
@@ -199,7 +199,7 @@ function parseStagingPreview(value) {
|
|
|
199
199
|
}
|
|
200
200
|
function parseCms(value) {
|
|
201
201
|
const cmsRecord = requireObject(value, "cms");
|
|
202
|
-
rejectUnknownKeys(cmsRecord, ["appName", "domainName", "staging", "cognitoDomainPrefix", "applyCognitoDomainPrefix", "buildComputeType", "environment", "stagingDomainName", "domainPrefix", "stagingDomainPrefix", "redirects", "stagingPreview", "prospectusAccountsApiStageArn"], "cms");
|
|
202
|
+
rejectUnknownKeys(cmsRecord, ["appName", "domainName", "staging", "cognitoDomainPrefix", "applyCognitoDomainPrefix", "buildComputeType", "environment", "stagingDomainName", "domainPrefix", "stagingDomainPrefix", "redirects", "stagingPreview", "prospectusAccountsApiStageArn", "deployStackInBuild"], "cms");
|
|
203
203
|
const buildComputeType = cmsRecord.buildComputeType;
|
|
204
204
|
if (buildComputeType !== undefined && buildComputeType !== "STANDARD" && buildComputeType !== "STANDARD_8GB") {
|
|
205
205
|
fail("cms.buildComputeType", `must be "STANDARD" or "STANDARD_8GB", got ${JSON.stringify(buildComputeType)}`);
|
|
@@ -210,6 +210,9 @@ function parseCms(value) {
|
|
|
210
210
|
if (cmsRecord.applyCognitoDomainPrefix !== undefined && typeof cmsRecord.applyCognitoDomainPrefix !== "boolean") {
|
|
211
211
|
fail("cms.applyCognitoDomainPrefix", `must be a boolean, got ${JSON.stringify(cmsRecord.applyCognitoDomainPrefix)}`);
|
|
212
212
|
}
|
|
213
|
+
if (cmsRecord.deployStackInBuild !== undefined && typeof cmsRecord.deployStackInBuild !== "boolean") {
|
|
214
|
+
fail("cms.deployStackInBuild", `must be a boolean, got ${JSON.stringify(cmsRecord.deployStackInBuild)}`);
|
|
215
|
+
}
|
|
213
216
|
const cms = {
|
|
214
217
|
appName: optionalString(cmsRecord.appName, "cms.appName", APP_NAME_PATTERN),
|
|
215
218
|
domainName: optionalString(cmsRecord.domainName, "cms.domainName", HOST_NAME_PATTERN, "host name"),
|
|
@@ -222,6 +225,7 @@ function parseCms(value) {
|
|
|
222
225
|
stagingDomainName: optionalString(cmsRecord.stagingDomainName, "cms.stagingDomainName", HOST_NAME_PATTERN, "host name"),
|
|
223
226
|
stagingDomainPrefix: optionalString(cmsRecord.stagingDomainPrefix, "cms.stagingDomainPrefix", DOMAIN_PREFIX_PATTERN, "single DNS label"),
|
|
224
227
|
redirects: parseRedirects(cmsRecord.redirects),
|
|
228
|
+
deployStackInBuild: cmsRecord.deployStackInBuild,
|
|
225
229
|
stagingPreview: parseStagingPreview(cmsRecord.stagingPreview),
|
|
226
230
|
prospectusAccountsApiStageArn: optionalString(cmsRecord.prospectusAccountsApiStageArn, "cms.prospectusAccountsApiStageArn", PROSPECTUS_ACCOUNTS_API_STAGE_ARN_PATTERN, "arn:aws:execute-api:<region>:<account>:<api id>/api"),
|
|
227
231
|
};
|
package/infra/site-config.ts
CHANGED
|
@@ -42,6 +42,7 @@ export type CmsConfig = {
|
|
|
42
42
|
stagingDomainPrefix?: string;
|
|
43
43
|
redirects?: CmsRedirect[];
|
|
44
44
|
prospectusAccountsApiStageArn?: string;
|
|
45
|
+
deployStackInBuild?: boolean;
|
|
45
46
|
};
|
|
46
47
|
|
|
47
48
|
export type AmplifyAppShellSiteConfig = {
|
|
@@ -281,7 +282,7 @@ function parseStagingPreview(value: unknown): StagingPreviewConfig | undefined {
|
|
|
281
282
|
|
|
282
283
|
function parseCms(value: unknown): CmsConfig {
|
|
283
284
|
const cmsRecord = requireObject(value, "cms");
|
|
284
|
-
rejectUnknownKeys(cmsRecord, ["appName", "domainName", "staging", "cognitoDomainPrefix", "applyCognitoDomainPrefix", "buildComputeType", "environment", "stagingDomainName", "domainPrefix", "stagingDomainPrefix", "redirects", "stagingPreview", "prospectusAccountsApiStageArn"], "cms");
|
|
285
|
+
rejectUnknownKeys(cmsRecord, ["appName", "domainName", "staging", "cognitoDomainPrefix", "applyCognitoDomainPrefix", "buildComputeType", "environment", "stagingDomainName", "domainPrefix", "stagingDomainPrefix", "redirects", "stagingPreview", "prospectusAccountsApiStageArn", "deployStackInBuild"], "cms");
|
|
285
286
|
const buildComputeType = cmsRecord.buildComputeType;
|
|
286
287
|
if (buildComputeType !== undefined && buildComputeType !== "STANDARD" && buildComputeType !== "STANDARD_8GB") {
|
|
287
288
|
fail("cms.buildComputeType", `must be "STANDARD" or "STANDARD_8GB", got ${JSON.stringify(buildComputeType)}`);
|
|
@@ -292,6 +293,9 @@ function parseCms(value: unknown): CmsConfig {
|
|
|
292
293
|
if (cmsRecord.applyCognitoDomainPrefix !== undefined && typeof cmsRecord.applyCognitoDomainPrefix !== "boolean") {
|
|
293
294
|
fail("cms.applyCognitoDomainPrefix", `must be a boolean, got ${JSON.stringify(cmsRecord.applyCognitoDomainPrefix)}`);
|
|
294
295
|
}
|
|
296
|
+
if (cmsRecord.deployStackInBuild !== undefined && typeof cmsRecord.deployStackInBuild !== "boolean") {
|
|
297
|
+
fail("cms.deployStackInBuild", `must be a boolean, got ${JSON.stringify(cmsRecord.deployStackInBuild)}`);
|
|
298
|
+
}
|
|
295
299
|
const cms: CmsConfig = {
|
|
296
300
|
appName: optionalString(cmsRecord.appName, "cms.appName", APP_NAME_PATTERN),
|
|
297
301
|
domainName: optionalString(cmsRecord.domainName, "cms.domainName", HOST_NAME_PATTERN, "host name"),
|
|
@@ -309,6 +313,7 @@ function parseCms(value: unknown): CmsConfig {
|
|
|
309
313
|
stagingDomainName: optionalString(cmsRecord.stagingDomainName, "cms.stagingDomainName", HOST_NAME_PATTERN, "host name"),
|
|
310
314
|
stagingDomainPrefix: optionalString(cmsRecord.stagingDomainPrefix, "cms.stagingDomainPrefix", DOMAIN_PREFIX_PATTERN, "single DNS label"),
|
|
311
315
|
redirects: parseRedirects(cmsRecord.redirects),
|
|
316
|
+
deployStackInBuild: cmsRecord.deployStackInBuild as boolean | undefined,
|
|
312
317
|
stagingPreview: parseStagingPreview(cmsRecord.stagingPreview),
|
|
313
318
|
prospectusAccountsApiStageArn: optionalString(
|
|
314
319
|
cmsRecord.prospectusAccountsApiStageArn,
|
package/package.json
CHANGED
package/routes.manifest.json
CHANGED