@projectdochelp/s3te 1.0.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.
@@ -0,0 +1,917 @@
1
+ import path from "node:path";
2
+ import {
3
+ CopyObjectCommand,
4
+ DeleteObjectCommand,
5
+ GetObjectCommand,
6
+ HeadObjectCommand,
7
+ ListObjectsV2Command,
8
+ PutObjectCommand,
9
+ S3Client
10
+ } from "@aws-sdk/client-s3";
11
+ import { CloudFrontClient, CreateInvalidationCommand } from "@aws-sdk/client-cloudfront";
12
+ import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
13
+ import { InvokeCommand, LambdaClient } from "@aws-sdk/client-lambda";
14
+ import { SSMClient, GetParameterCommand } from "@aws-sdk/client-ssm";
15
+ import { SFNClient, StartExecutionCommand } from "@aws-sdk/client-sfn";
16
+ import {
17
+ BatchWriteCommand,
18
+ DeleteCommand,
19
+ DynamoDBDocumentClient,
20
+ PutCommand,
21
+ QueryCommand,
22
+ ScanCommand
23
+ } from "@aws-sdk/lib-dynamodb";
24
+ import { unmarshall } from "@aws-sdk/util-dynamodb";
25
+
26
+ import {
27
+ applyContentQuery,
28
+ createManualRenderTargets,
29
+ getContentTypeForPath,
30
+ isRenderableKey,
31
+ renderSourceTemplate
32
+ } from "../../../core/src/index.mjs";
33
+
34
+ function normalizeKey(value) {
35
+ return String(value).replace(/\\/g, "/");
36
+ }
37
+
38
+ function buildSourceId(environment, variant, language, outputKey) {
39
+ return `${environment}#${variant}#${language}#${outputKey}`;
40
+ }
41
+
42
+ function chunk(items, size) {
43
+ const chunks = [];
44
+ for (let index = 0; index < items.length; index += size) {
45
+ chunks.push(items.slice(index, index + size));
46
+ }
47
+ return chunks;
48
+ }
49
+
50
+ function normalizeLocale(value) {
51
+ return String(value).trim().toLowerCase();
52
+ }
53
+
54
+ function buildLocaleCandidates(language, languageLocaleMap) {
55
+ const requested = String(language ?? "").trim();
56
+ const configured = String(languageLocaleMap?.[requested] ?? requested).trim();
57
+ return [...new Set([configured, requested].filter(Boolean).map(normalizeLocale))];
58
+ }
59
+
60
+ function localeMatchScore(itemLocale, language, languageLocaleMap) {
61
+ if (!itemLocale) {
62
+ return 1;
63
+ }
64
+
65
+ const normalizedItemLocale = normalizeLocale(itemLocale);
66
+ const candidates = buildLocaleCandidates(language, languageLocaleMap);
67
+ if (candidates.includes(normalizedItemLocale)) {
68
+ return 3;
69
+ }
70
+
71
+ if (candidates.some((candidate) => candidate.length >= 2 && normalizedItemLocale.startsWith(`${candidate}-`))) {
72
+ return 2;
73
+ }
74
+
75
+ return 0;
76
+ }
77
+
78
+ function matchesRequestedLocale(item, language, languageLocaleMap) {
79
+ return localeMatchScore(item?.locale, language, languageLocaleMap) > 0;
80
+ }
81
+
82
+ function filterItemsByRequestedLocale(items, language, languageLocaleMap) {
83
+ const grouped = new Map();
84
+
85
+ for (const item of items) {
86
+ const groupKey = item.contentId ?? item.id ?? JSON.stringify(item);
87
+ if (!grouped.has(groupKey)) {
88
+ grouped.set(groupKey, []);
89
+ }
90
+ grouped.get(groupKey).push(item);
91
+ }
92
+
93
+ return [...grouped.values()].flatMap((groupItems) => {
94
+ const scored = groupItems
95
+ .map((item) => ({
96
+ item,
97
+ score: localeMatchScore(item.locale, language, languageLocaleMap)
98
+ }))
99
+ .filter((entry) => entry.score > 0);
100
+ if (scored.length === 0) {
101
+ return [];
102
+ }
103
+
104
+ const bestScore = Math.max(...scored.map((entry) => entry.score));
105
+ return scored.filter((entry) => entry.score === bestScore).map((entry) => entry.item);
106
+ });
107
+ }
108
+
109
+ function wrapCommandClient(client, mapping) {
110
+ return Object.fromEntries(Object.entries(mapping).map(([name, CommandType]) => [
111
+ name,
112
+ (input) => ({
113
+ promise: () => client.send(new CommandType(input))
114
+ })
115
+ ]));
116
+ }
117
+
118
+ export function createAwsClients(region = process.env.AWS_REGION) {
119
+ const clientConfig = region ? { region } : {};
120
+ const dynamoBaseClient = new DynamoDBClient(clientConfig);
121
+ const dynamoDocumentClient = DynamoDBDocumentClient.from(dynamoBaseClient);
122
+
123
+ return {
124
+ AWS: {
125
+ DynamoDB: {
126
+ Converter: {
127
+ unmarshall
128
+ }
129
+ }
130
+ },
131
+ s3: wrapCommandClient(new S3Client(clientConfig), {
132
+ getObject: GetObjectCommand,
133
+ headObject: HeadObjectCommand,
134
+ listObjectsV2: ListObjectsV2Command,
135
+ putObject: PutObjectCommand,
136
+ copyObject: CopyObjectCommand,
137
+ deleteObject: DeleteObjectCommand
138
+ }),
139
+ ssm: wrapCommandClient(new SSMClient(clientConfig), {
140
+ getParameter: GetParameterCommand
141
+ }),
142
+ lambda: wrapCommandClient(new LambdaClient(clientConfig), {
143
+ invoke: InvokeCommand
144
+ }),
145
+ dynamo: wrapCommandClient(dynamoDocumentClient, {
146
+ query: QueryCommand,
147
+ scan: ScanCommand,
148
+ batchWrite: BatchWriteCommand,
149
+ put: PutCommand,
150
+ delete: DeleteCommand
151
+ }),
152
+ stepFunctions: wrapCommandClient(new SFNClient(clientConfig), {
153
+ startExecution: StartExecutionCommand
154
+ }),
155
+ cloudFront: wrapCommandClient(new CloudFrontClient(clientConfig), {
156
+ createInvalidation: CreateInvalidationCommand
157
+ })
158
+ };
159
+ }
160
+
161
+ export async function loadEnvironmentManifest(ssm, parameterName, environmentName) {
162
+ const response = await ssm.getParameter({ Name: parameterName }).promise();
163
+ const manifest = JSON.parse(response.Parameter.Value);
164
+ const environment = manifest.environments?.[environmentName];
165
+ if (!environment) {
166
+ throw new Error(`Runtime manifest does not contain environment ${environmentName}.`);
167
+ }
168
+ return {
169
+ manifest,
170
+ environment
171
+ };
172
+ }
173
+
174
+ export function decodeS3Key(value) {
175
+ return decodeURIComponent(String(value).replace(/\+/g, " "));
176
+ }
177
+
178
+ async function bodyToUtf8(body) {
179
+ if (typeof body?.transformToString === "function") {
180
+ return body.transformToString("utf8");
181
+ }
182
+
183
+ if (Buffer.isBuffer(body)) {
184
+ return body.toString("utf8");
185
+ }
186
+
187
+ if (body instanceof Uint8Array) {
188
+ return Buffer.from(body).toString("utf8");
189
+ }
190
+
191
+ return String(body ?? "");
192
+ }
193
+
194
+ export function buildCoreConfigFromEnvironment(manifest, environmentName) {
195
+ const environment = manifest.environments[environmentName];
196
+ return {
197
+ project: {
198
+ name: manifest.project.name,
199
+ displayName: manifest.project.displayName
200
+ },
201
+ environments: {
202
+ [environmentName]: {
203
+ name: environment.name,
204
+ awsRegion: environment.awsRegion,
205
+ stackPrefix: environment.stackPrefix,
206
+ certificateArn: "",
207
+ route53HostedZoneId: undefined
208
+ }
209
+ },
210
+ rendering: {
211
+ ...environment.rendering
212
+ },
213
+ variants: Object.fromEntries(Object.entries(environment.variants).map(([variantName, variantConfig]) => [
214
+ variantName,
215
+ {
216
+ name: variantName,
217
+ sourceDir: variantConfig.sourceDir,
218
+ partDir: variantConfig.partDir,
219
+ defaultLanguage: variantConfig.defaultLanguage,
220
+ routing: { ...variantConfig.routing },
221
+ languages: Object.fromEntries(Object.entries(variantConfig.languages).map(([languageCode, languageConfig]) => [
222
+ languageCode,
223
+ {
224
+ code: languageCode,
225
+ baseUrl: languageConfig.baseUrl,
226
+ targetBucket: languageConfig.targetBucket,
227
+ cloudFrontAliases: [...languageConfig.cloudFrontAliases],
228
+ webinyLocale: languageConfig.webinyLocale ?? languageCode
229
+ }
230
+ ]))
231
+ }
232
+ ])),
233
+ aws: {
234
+ codeBuckets: Object.fromEntries(Object.entries(environment.variants).map(([variantName, variantConfig]) => [
235
+ variantName,
236
+ variantConfig.codeBucket
237
+ ])),
238
+ dependencyStore: {
239
+ tableName: environment.tables.dependency
240
+ },
241
+ contentStore: {
242
+ tableName: environment.tables.content,
243
+ contentIdIndexName: environment.tables.contentIdIndexName ?? "contentid"
244
+ },
245
+ invalidationStore: {
246
+ tableName: environment.tables.invalidation,
247
+ debounceSeconds: 60
248
+ },
249
+ lambda: {
250
+ runtime: "nodejs22.x",
251
+ architecture: "arm64"
252
+ }
253
+ },
254
+ integrations: {
255
+ webiny: {
256
+ enabled: environment.integrations.webiny.enabled,
257
+ sourceTableName: environment.integrations.webiny.sourceTableName,
258
+ mirrorTableName: environment.integrations.webiny.mirrorTableName,
259
+ relevantModels: [...environment.integrations.webiny.relevantModels],
260
+ tenant: environment.integrations.webiny.tenant
261
+ }
262
+ }
263
+ };
264
+ }
265
+
266
+ export class S3TemplateRepository {
267
+ constructor({ s3, environmentManifest, activeVariantName }) {
268
+ this.s3 = s3;
269
+ this.environmentManifest = environmentManifest;
270
+ this.activeVariantName = activeVariantName;
271
+ }
272
+
273
+ resolveLogicalKey(key) {
274
+ const normalized = normalizeKey(key);
275
+ const activeVariant = this.environmentManifest.variants[this.activeVariantName];
276
+
277
+ if (normalized.startsWith(`${this.activeVariantName}/`)) {
278
+ return {
279
+ bucket: activeVariant.codeBucket,
280
+ objectKey: normalized
281
+ };
282
+ }
283
+
284
+ if (normalized.startsWith(`${activeVariant.partDir}/`)) {
285
+ return {
286
+ bucket: activeVariant.codeBucket,
287
+ objectKey: `part/${normalized.slice(activeVariant.partDir.length + 1)}`
288
+ };
289
+ }
290
+
291
+ if (normalized.startsWith("part/")) {
292
+ return {
293
+ bucket: activeVariant.codeBucket,
294
+ objectKey: normalized
295
+ };
296
+ }
297
+
298
+ return {
299
+ bucket: activeVariant.codeBucket,
300
+ objectKey: normalized
301
+ };
302
+ }
303
+
304
+ async get(key) {
305
+ const resolved = this.resolveLogicalKey(key);
306
+ try {
307
+ const response = await this.s3.getObject({
308
+ Bucket: resolved.bucket,
309
+ Key: resolved.objectKey
310
+ }).promise();
311
+
312
+ return {
313
+ key: normalizeKey(key),
314
+ body: await bodyToUtf8(response.Body),
315
+ contentType: response.ContentType ?? getContentTypeForPath(resolved.objectKey),
316
+ lastModified: response.LastModified?.toISOString?.()
317
+ };
318
+ } catch (error) {
319
+ const errorCode = error?.name ?? error?.Code ?? error?.code;
320
+ if (errorCode === "NoSuchKey" || errorCode === "NoSuchBucket") {
321
+ return null;
322
+ }
323
+ throw error;
324
+ }
325
+ }
326
+
327
+ async listVariantEntries(variantName) {
328
+ const variant = this.environmentManifest.variants[variantName];
329
+ const entries = [];
330
+ let continuationToken;
331
+
332
+ do {
333
+ const response = await this.s3.listObjectsV2({
334
+ Bucket: variant.codeBucket,
335
+ Prefix: `${variantName}/`,
336
+ ContinuationToken: continuationToken
337
+ }).promise();
338
+
339
+ for (const item of response.Contents ?? []) {
340
+ if (!item.Key.endsWith("/")) {
341
+ entries.push({
342
+ key: item.Key,
343
+ body: null,
344
+ contentType: getContentTypeForPath(item.Key),
345
+ lastModified: item.LastModified?.toISOString?.()
346
+ });
347
+ }
348
+ }
349
+
350
+ continuationToken = response.NextContinuationToken;
351
+ } while (continuationToken);
352
+
353
+ return entries;
354
+ }
355
+
356
+ async exists(key) {
357
+ const resolved = this.resolveLogicalKey(key);
358
+ try {
359
+ await this.s3.headObject({
360
+ Bucket: resolved.bucket,
361
+ Key: resolved.objectKey
362
+ }).promise();
363
+ return true;
364
+ } catch {
365
+ return false;
366
+ }
367
+ }
368
+ }
369
+
370
+ export class DynamoContentRepository {
371
+ constructor({ dynamo, tableName, indexName, languageLocaleMap = {} }) {
372
+ this.dynamo = dynamo;
373
+ this.tableName = tableName;
374
+ this.indexName = indexName;
375
+ this.languageLocaleMap = languageLocaleMap;
376
+ }
377
+
378
+ async getByContentId(contentId, language) {
379
+ const response = await this.dynamo.query({
380
+ TableName: this.tableName,
381
+ IndexName: this.indexName,
382
+ KeyConditionExpression: "contentId = :contentId",
383
+ ExpressionAttributeValues: {
384
+ ":contentId": contentId
385
+ }
386
+ }).promise();
387
+ const items = response.Items ?? [];
388
+ const candidates = items
389
+ .map((item) => ({
390
+ item,
391
+ score: localeMatchScore(item.locale, language, this.languageLocaleMap)
392
+ }))
393
+ .filter((entry) => entry.score > 0)
394
+ .sort((left, right) => right.score - left.score || String(left.item.id).localeCompare(String(right.item.id)));
395
+ return candidates[0]?.item ?? null;
396
+ }
397
+
398
+ async query(query, language) {
399
+ let lastEvaluatedKey;
400
+ const items = [];
401
+
402
+ do {
403
+ const response = await this.dynamo.scan({
404
+ TableName: this.tableName,
405
+ ExclusiveStartKey: lastEvaluatedKey
406
+ }).promise();
407
+ items.push(...(response.Items ?? []));
408
+ lastEvaluatedKey = response.LastEvaluatedKey;
409
+ } while (lastEvaluatedKey);
410
+
411
+ return applyContentQuery(filterItemsByRequestedLocale(items, language, this.languageLocaleMap), query);
412
+ }
413
+ }
414
+
415
+ export class DynamoDependencyStore {
416
+ constructor({ dynamo, tableName }) {
417
+ this.dynamo = dynamo;
418
+ this.tableName = tableName;
419
+ }
420
+
421
+ async queryBySourceId(sourceId) {
422
+ const response = await this.dynamo.query({
423
+ TableName: this.tableName,
424
+ KeyConditionExpression: "sourceId = :sourceId",
425
+ ExpressionAttributeValues: {
426
+ ":sourceId": sourceId
427
+ }
428
+ }).promise();
429
+ return response.Items ?? [];
430
+ }
431
+
432
+ async batchWrite(requests) {
433
+ for (const batch of chunk(requests, 25)) {
434
+ await this.dynamo.batchWrite({
435
+ RequestItems: {
436
+ [this.tableName]: batch
437
+ }
438
+ }).promise();
439
+ }
440
+ }
441
+
442
+ async replaceSourceDependencies(record) {
443
+ const existing = await this.queryBySourceId(record.sourceId);
444
+ if (existing.length > 0) {
445
+ await this.batchWrite(existing.map((item) => ({
446
+ DeleteRequest: {
447
+ Key: {
448
+ sourceId: item.sourceId,
449
+ dependencyKey: item.dependencyKey
450
+ }
451
+ }
452
+ })));
453
+ }
454
+
455
+ if (!record.dependencies || record.dependencies.length === 0) {
456
+ return;
457
+ }
458
+
459
+ await this.batchWrite(record.dependencies.map((dependency) => ({
460
+ PutRequest: {
461
+ Item: {
462
+ sourceId: record.sourceId,
463
+ dependencyKey: `${dependency.kind}#${dependency.id}`,
464
+ templateKey: record.templateKey,
465
+ outputKey: record.outputKey,
466
+ environment: record.environment,
467
+ variant: record.variant,
468
+ language: record.language
469
+ }
470
+ }
471
+ })));
472
+ }
473
+
474
+ async findDependentsByDependency(ref) {
475
+ const response = await this.dynamo.query({
476
+ TableName: this.tableName,
477
+ IndexName: "dependencyKey-index",
478
+ KeyConditionExpression: "dependencyKey = :dependencyKey",
479
+ ExpressionAttributeValues: {
480
+ ":dependencyKey": `${ref.kind}#${ref.id}`
481
+ }
482
+ }).promise();
483
+
484
+ const grouped = new Map();
485
+ for (const item of response.Items ?? []) {
486
+ if (!grouped.has(item.sourceId)) {
487
+ grouped.set(item.sourceId, {
488
+ sourceId: item.sourceId,
489
+ environment: item.environment,
490
+ variant: item.variant,
491
+ language: item.language,
492
+ templateKey: item.templateKey,
493
+ outputKey: item.outputKey,
494
+ dependencies: []
495
+ });
496
+ }
497
+ grouped.get(item.sourceId).dependencies.push({
498
+ kind: item.dependencyKey.split("#")[0],
499
+ id: item.dependencyKey.split("#").slice(1).join("#")
500
+ });
501
+ }
502
+
503
+ return [...grouped.values()];
504
+ }
505
+
506
+ async findGeneratedOutputsByTemplate(templateKey, scope) {
507
+ const response = await this.dynamo.query({
508
+ TableName: this.tableName,
509
+ IndexName: "dependencyKey-index",
510
+ KeyConditionExpression: "dependencyKey = :dependencyKey",
511
+ ExpressionAttributeValues: {
512
+ ":dependencyKey": `generated-template#${templateKey}`
513
+ }
514
+ }).promise();
515
+
516
+ return (response.Items ?? [])
517
+ .filter((item) => item.environment === scope.environment
518
+ && item.variant === scope.variant
519
+ && item.language === scope.language)
520
+ .map((item) => ({
521
+ environment: item.environment,
522
+ variant: item.variant,
523
+ language: item.language,
524
+ templateKey: item.templateKey,
525
+ outputKey: item.outputKey
526
+ }));
527
+ }
528
+
529
+ async deleteOutput(output) {
530
+ const sourceId = buildSourceId(output.environment, output.variant, output.language, output.outputKey);
531
+ const existing = await this.queryBySourceId(sourceId);
532
+ if (existing.length === 0) {
533
+ return;
534
+ }
535
+ await this.batchWrite(existing.map((item) => ({
536
+ DeleteRequest: {
537
+ Key: {
538
+ sourceId: item.sourceId,
539
+ dependencyKey: item.dependencyKey
540
+ }
541
+ }
542
+ })));
543
+ }
544
+ }
545
+
546
+ export class S3OutputPublisher {
547
+ constructor({ s3, environmentManifest }) {
548
+ this.s3 = s3;
549
+ this.environmentManifest = environmentManifest;
550
+ }
551
+
552
+ resolveTargetBucket(target) {
553
+ return this.environmentManifest.variants[target.variant].languages[target.language].targetBucket;
554
+ }
555
+
556
+ async put(artifact, target) {
557
+ await this.s3.putObject({
558
+ Bucket: this.resolveTargetBucket(target),
559
+ Key: artifact.outputKey,
560
+ Body: artifact.body,
561
+ ContentType: artifact.contentType,
562
+ CacheControl: artifact.cacheControl
563
+ }).promise();
564
+ }
565
+
566
+ async copySourceObject(sourceKey, target) {
567
+ const sourceBucket = this.environmentManifest.variants[target.variant].codeBucket;
568
+ const encodedSource = `${sourceBucket}/${sourceKey.split("/").map(encodeURIComponent).join("/")}`;
569
+ await this.s3.copyObject({
570
+ Bucket: this.resolveTargetBucket(target),
571
+ Key: target.outputKey,
572
+ CopySource: encodedSource
573
+ }).promise();
574
+ }
575
+
576
+ async delete(outputKey, target) {
577
+ await this.s3.deleteObject({
578
+ Bucket: this.resolveTargetBucket(target),
579
+ Key: outputKey
580
+ }).promise();
581
+ }
582
+ }
583
+
584
+ export class LambdaInvalidationScheduler {
585
+ constructor({ lambda, functionName }) {
586
+ this.lambda = lambda;
587
+ this.functionName = functionName;
588
+ }
589
+
590
+ async enqueue(request) {
591
+ if (!request.distributionId) {
592
+ return;
593
+ }
594
+ await this.lambda.invoke({
595
+ FunctionName: this.functionName,
596
+ InvocationType: "Event",
597
+ Payload: JSON.stringify(request)
598
+ }).promise();
599
+ }
600
+ }
601
+
602
+ export function buildInvalidationRequest(environmentName, environmentManifest, variantName, languageCode, buildId, paths = ["/*"]) {
603
+ const language = environmentManifest.variants[variantName].languages[languageCode];
604
+ return {
605
+ buildId,
606
+ environment: environmentName,
607
+ variant: variantName,
608
+ language: languageCode,
609
+ distributionId: language.distributionId,
610
+ distributionAliases: [...language.cloudFrontAliases],
611
+ paths,
612
+ requestedAt: new Date().toISOString()
613
+ };
614
+ }
615
+
616
+ export function isRenderableBucketKey(environmentManifest, variantName, key, renderExtensions) {
617
+ if (key.startsWith("part/")) {
618
+ return true;
619
+ }
620
+
621
+ if (!key.startsWith(`${variantName}/`)) {
622
+ return false;
623
+ }
624
+
625
+ const extension = path.extname(key).toLowerCase();
626
+ return renderExtensions.includes(extension);
627
+ }
628
+
629
+ function defaultDeleteOutputKey(variantName, sourceKey) {
630
+ if (!sourceKey.startsWith(`${variantName}/`)) {
631
+ return sourceKey;
632
+ }
633
+ return sourceKey.slice(variantName.length + 1);
634
+ }
635
+
636
+ function uniqueTargets(targets) {
637
+ const seen = new Set();
638
+ return targets.filter((target) => {
639
+ const key = `${target.variant}#${target.language}#${target.sourceKey}`;
640
+ if (seen.has(key)) {
641
+ return false;
642
+ }
643
+ seen.add(key);
644
+ return true;
645
+ });
646
+ }
647
+
648
+ export async function resolveRenderTargetsForEvent({
649
+ event,
650
+ manifest,
651
+ environmentName,
652
+ environmentManifest,
653
+ dependencyStore,
654
+ templateRepositoryFactory
655
+ }) {
656
+ const coreConfig = buildCoreConfigFromEnvironment(manifest, environmentName);
657
+
658
+ if (event.type === "manual-build") {
659
+ const variants = event.variant ? [event.variant] : Object.keys(environmentManifest.variants);
660
+ const targets = [];
661
+ for (const variantName of variants) {
662
+ const templateRepository = templateRepositoryFactory(variantName);
663
+ const templateEntries = await templateRepository.listVariantEntries(variantName);
664
+ targets.push(...createManualRenderTargets({
665
+ config: coreConfig,
666
+ templateEntries,
667
+ environment: environmentName,
668
+ variant: variantName,
669
+ language: event.language,
670
+ entry: event.sourceKey
671
+ }));
672
+ }
673
+ return uniqueTargets(targets);
674
+ }
675
+
676
+ if (event.type === "content-item") {
677
+ const targets = [];
678
+ for (const variantName of Object.keys(environmentManifest.variants)) {
679
+ const templateRepository = templateRepositoryFactory(variantName);
680
+ const templateEntries = await templateRepository.listVariantEntries(variantName);
681
+ targets.push(...createManualRenderTargets({
682
+ config: coreConfig,
683
+ templateEntries,
684
+ environment: environmentName,
685
+ variant: variantName
686
+ }));
687
+ }
688
+ return uniqueTargets(targets);
689
+ }
690
+
691
+ if (event.type === "source-object" && event.key.startsWith("part/")) {
692
+ const dependencyId = event.key.slice("part/".length);
693
+ const dependentRecords = await dependencyStore.findDependentsByDependency({
694
+ kind: "partial",
695
+ id: dependencyId
696
+ });
697
+
698
+ if (dependentRecords.length === 0) {
699
+ const templateRepository = templateRepositoryFactory(event.variant);
700
+ const templateEntries = await templateRepository.listVariantEntries(event.variant);
701
+ return createManualRenderTargets({
702
+ config: coreConfig,
703
+ templateEntries,
704
+ environment: environmentName,
705
+ variant: event.variant
706
+ });
707
+ }
708
+
709
+ return uniqueTargets(dependentRecords
710
+ .filter((record) => record.environment === environmentName && record.variant === event.variant)
711
+ .map((record) => ({
712
+ environment: environmentName,
713
+ variant: record.variant,
714
+ language: record.language,
715
+ sourceKey: record.templateKey,
716
+ outputKey: defaultDeleteOutputKey(record.variant, record.templateKey),
717
+ baseUrl: environmentManifest.variants[record.variant].languages[record.language].baseUrl
718
+ })));
719
+ }
720
+
721
+ if (event.type === "source-object") {
722
+ return uniqueTargets(Object.keys(environmentManifest.variants[event.variant].languages).map((languageCode) => ({
723
+ environment: environmentName,
724
+ variant: event.variant,
725
+ language: languageCode,
726
+ sourceKey: event.key,
727
+ outputKey: defaultDeleteOutputKey(event.variant, event.key),
728
+ baseUrl: environmentManifest.variants[event.variant].languages[languageCode].baseUrl
729
+ })));
730
+ }
731
+
732
+ return [];
733
+ }
734
+
735
+ export async function deleteOutputsForTemplate({
736
+ event,
737
+ environmentName,
738
+ environmentManifest,
739
+ dependencyStore,
740
+ publisher,
741
+ invalidationScheduler,
742
+ buildId
743
+ }) {
744
+ const languages = Object.keys(environmentManifest.variants[event.variant].languages);
745
+ const deletedOutputs = [];
746
+
747
+ for (const languageCode of languages) {
748
+ const generated = await dependencyStore.findGeneratedOutputsByTemplate(event.key, {
749
+ environment: environmentName,
750
+ variant: event.variant,
751
+ language: languageCode
752
+ });
753
+ const outputs = generated.length > 0 ? generated : [{
754
+ environment: environmentName,
755
+ variant: event.variant,
756
+ language: languageCode,
757
+ templateKey: event.key,
758
+ outputKey: defaultDeleteOutputKey(event.variant, event.key)
759
+ }];
760
+
761
+ for (const output of outputs) {
762
+ await publisher.delete(output.outputKey, {
763
+ environment: environmentName,
764
+ variant: event.variant,
765
+ language: languageCode,
766
+ sourceKey: event.key,
767
+ outputKey: output.outputKey,
768
+ baseUrl: environmentManifest.variants[event.variant].languages[languageCode].baseUrl
769
+ });
770
+ await dependencyStore.deleteOutput(output);
771
+ await invalidationScheduler.enqueue(buildInvalidationRequest(
772
+ environmentName,
773
+ environmentManifest,
774
+ event.variant,
775
+ languageCode,
776
+ buildId
777
+ ));
778
+ deletedOutputs.push(output.outputKey);
779
+ }
780
+ }
781
+
782
+ return deletedOutputs;
783
+ }
784
+
785
+ export async function renderAndPublishTargets({
786
+ manifest,
787
+ environmentName,
788
+ environmentManifest,
789
+ contentRepository,
790
+ dependencyStore,
791
+ publisher,
792
+ invalidationScheduler,
793
+ targets,
794
+ buildId
795
+ }) {
796
+ const coreConfig = buildCoreConfigFromEnvironment(manifest, environmentName);
797
+ const rendered = [];
798
+ const deleted = [];
799
+ const warnings = [];
800
+
801
+ for (const target of targets) {
802
+ const templateRepository = new S3TemplateRepository({
803
+ s3: publisher.s3,
804
+ environmentManifest,
805
+ activeVariantName: target.variant
806
+ });
807
+
808
+ const previousOutputs = await dependencyStore.findGeneratedOutputsByTemplate(target.sourceKey, {
809
+ environment: environmentName,
810
+ variant: target.variant,
811
+ language: target.language
812
+ });
813
+
814
+ const results = await renderSourceTemplate({
815
+ config: coreConfig,
816
+ templateRepository,
817
+ contentRepository,
818
+ environment: environmentName,
819
+ variantName: target.variant,
820
+ languageCode: target.language,
821
+ sourceKey: target.sourceKey
822
+ });
823
+
824
+ const nextOutputKeys = new Set();
825
+
826
+ for (const result of results) {
827
+ nextOutputKeys.add(result.artifact.outputKey);
828
+ await publisher.put(result.artifact, result.target);
829
+ await dependencyStore.replaceSourceDependencies({
830
+ sourceId: buildSourceId(environmentName, result.target.variant, result.target.language, result.artifact.outputKey),
831
+ environment: environmentName,
832
+ variant: result.target.variant,
833
+ language: result.target.language,
834
+ templateKey: target.sourceKey,
835
+ outputKey: result.artifact.outputKey,
836
+ dependencies: result.dependencies
837
+ });
838
+ await invalidationScheduler.enqueue(buildInvalidationRequest(
839
+ environmentName,
840
+ environmentManifest,
841
+ result.target.variant,
842
+ result.target.language,
843
+ buildId,
844
+ result.invalidationPaths
845
+ ));
846
+ warnings.push(...result.warnings);
847
+ rendered.push(result.artifact.outputKey);
848
+ }
849
+
850
+ for (const previousOutput of previousOutputs) {
851
+ if (nextOutputKeys.has(previousOutput.outputKey)) {
852
+ continue;
853
+ }
854
+ await publisher.delete(previousOutput.outputKey, {
855
+ environment: environmentName,
856
+ variant: previousOutput.variant,
857
+ language: previousOutput.language,
858
+ sourceKey: previousOutput.templateKey,
859
+ outputKey: previousOutput.outputKey,
860
+ baseUrl: environmentManifest.variants[previousOutput.variant].languages[previousOutput.language].baseUrl
861
+ });
862
+ await dependencyStore.deleteOutput(previousOutput);
863
+ await invalidationScheduler.enqueue(buildInvalidationRequest(
864
+ environmentName,
865
+ environmentManifest,
866
+ previousOutput.variant,
867
+ previousOutput.language,
868
+ buildId
869
+ ));
870
+ deleted.push(previousOutput.outputKey);
871
+ }
872
+ }
873
+
874
+ return { rendered, deleted, warnings };
875
+ }
876
+
877
+ export function createRepositoriesAndPublishers({ clients, environmentManifest }) {
878
+ const languageLocaleMap = Object.fromEntries(Object.entries(environmentManifest.variants).flatMap(([, variantConfig]) => (
879
+ Object.entries(variantConfig.languages).map(([languageCode, languageConfig]) => [
880
+ languageCode,
881
+ languageConfig.webinyLocale ?? languageCode
882
+ ])
883
+ )));
884
+
885
+ return {
886
+ contentRepository: new DynamoContentRepository({
887
+ dynamo: clients.dynamo,
888
+ tableName: environmentManifest.tables.content,
889
+ indexName: environmentManifest.tables.contentIdIndexName ?? "contentid",
890
+ languageLocaleMap
891
+ }),
892
+ dependencyStore: new DynamoDependencyStore({
893
+ dynamo: clients.dynamo,
894
+ tableName: environmentManifest.tables.dependency
895
+ }),
896
+ publisher: new S3OutputPublisher({
897
+ s3: clients.s3,
898
+ environmentManifest
899
+ }),
900
+ invalidationScheduler: new LambdaInvalidationScheduler({
901
+ lambda: clients.lambda,
902
+ functionName: environmentManifest.functions.invalidationScheduler
903
+ })
904
+ };
905
+ }
906
+
907
+ export async function invokeLambdaEvent(lambda, functionName, payload) {
908
+ await lambda.invoke({
909
+ FunctionName: functionName,
910
+ InvocationType: "Event",
911
+ Payload: JSON.stringify(payload)
912
+ }).promise();
913
+ }
914
+
915
+ export function createBuildId(prefix = "build") {
916
+ return `${prefix}-${Date.now()}`;
917
+ }