@projectdochelp/s3te 3.3.2 → 3.3.3

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@projectdochelp/s3te",
3
- "version": "3.3.2",
3
+ "version": "3.3.3",
4
4
  "description": "CLI, render core, AWS adapter, and testkit for S3TemplateEngine projects",
5
5
  "repository": {
6
6
  "type": "git",
@@ -76,8 +76,37 @@ function localeMatchScore(itemLocale, language, languageLocaleMap) {
76
76
  return 0;
77
77
  }
78
78
 
79
- function matchesRequestedLocale(item, language, languageLocaleMap) {
80
- return localeMatchScore(item?.locale, language, languageLocaleMap) > 0;
79
+ function comparableTimestamp(value) {
80
+ if (typeof value === "number" && Number.isFinite(value)) {
81
+ return value;
82
+ }
83
+
84
+ const timestamp = Date.parse(String(value ?? ""));
85
+ return Number.isFinite(timestamp) ? timestamp : -1;
86
+ }
87
+
88
+ function compareContentFreshness(left, right) {
89
+ const updatedDiff = comparableTimestamp(right.updatedAt) - comparableTimestamp(left.updatedAt);
90
+ if (updatedDiff !== 0) {
91
+ return updatedDiff;
92
+ }
93
+
94
+ const changedDiff = comparableTimestamp(right.lastChangedAt) - comparableTimestamp(left.lastChangedAt);
95
+ if (changedDiff !== 0) {
96
+ return changedDiff;
97
+ }
98
+
99
+ const createdDiff = comparableTimestamp(right.createdAt) - comparableTimestamp(left.createdAt);
100
+ if (createdDiff !== 0) {
101
+ return createdDiff;
102
+ }
103
+
104
+ const versionDiff = Number(right.version ?? -1) - Number(left.version ?? -1);
105
+ if (versionDiff !== 0) {
106
+ return versionDiff;
107
+ }
108
+
109
+ return String(right.id ?? "").localeCompare(String(left.id ?? ""));
81
110
  }
82
111
 
83
112
  function filterItemsByRequestedLocale(items, language, languageLocaleMap) {
@@ -103,7 +132,11 @@ function filterItemsByRequestedLocale(items, language, languageLocaleMap) {
103
132
  }
104
133
 
105
134
  const bestScore = Math.max(...scored.map((entry) => entry.score));
106
- return scored.filter((entry) => entry.score === bestScore).map((entry) => entry.item);
135
+ return scored
136
+ .filter((entry) => entry.score === bestScore)
137
+ .map((entry) => entry.item)
138
+ .sort(compareContentFreshness)
139
+ .slice(0, 1);
107
140
  });
108
141
  }
109
142
 
@@ -390,14 +423,7 @@ export class DynamoContentRepository {
390
423
  }
391
424
  }).promise();
392
425
  const items = response.Items ?? [];
393
- const candidates = items
394
- .map((item) => ({
395
- item,
396
- score: localeMatchScore(item.locale, language, this.languageLocaleMap)
397
- }))
398
- .filter((entry) => entry.score > 0)
399
- .sort((left, right) => right.score - left.score || String(left.item.id).localeCompare(String(right.item.id)));
400
- return candidates[0]?.item ?? null;
426
+ return filterItemsByRequestedLocale(items, language, this.languageLocaleMap)[0] ?? null;
401
427
  }
402
428
 
403
429
  async query(query, language) {
@@ -314,6 +314,17 @@ function extractWebinyTenant(item) {
314
314
  ?? null;
315
315
  }
316
316
 
317
+ function normalizeMirrorLocale(value) {
318
+ return value == null ? "" : String(value).trim().toLowerCase();
319
+ }
320
+
321
+ function isSameMirroredContentIdentity(existingItem, contentItem) {
322
+ return String(existingItem.contentId ?? "") === String(contentItem.contentId ?? "")
323
+ && String(existingItem.model ?? "") === String(contentItem.model ?? "")
324
+ && String(existingItem.tenant ?? "") === String(contentItem.tenant ?? "")
325
+ && normalizeMirrorLocale(existingItem.locale) === normalizeMirrorLocale(contentItem.locale);
326
+ }
327
+
317
328
  export function normalizeContentItem(item, options = {}) {
318
329
  const root = getItemRoot(item);
319
330
  const values = normalizeValues(item, options.modelFields);
@@ -369,12 +380,53 @@ async function loadModelFields(clients, sourceTableName, tenant, modelId, cache)
369
380
  return fields;
370
381
  }
371
382
 
383
+ async function listMirroredContentItems(clients, tableName, indexName, contentId) {
384
+ const items = [];
385
+ let lastEvaluatedKey;
386
+
387
+ do {
388
+ const response = await clients.dynamo.query({
389
+ TableName: tableName,
390
+ IndexName: indexName,
391
+ KeyConditionExpression: "contentId = :contentId",
392
+ ExpressionAttributeValues: {
393
+ ":contentId": contentId
394
+ },
395
+ ExclusiveStartKey: lastEvaluatedKey
396
+ }).promise();
397
+ items.push(...(response.Items ?? []));
398
+ lastEvaluatedKey = response.LastEvaluatedKey;
399
+ } while (lastEvaluatedKey);
400
+
401
+ return items;
402
+ }
403
+
404
+ async function removeMirroredContentRevisions(clients, tableName, indexName, contentItem, excludeId = null) {
405
+ const mirroredItems = await listMirroredContentItems(clients, tableName, indexName, contentItem.contentId);
406
+ const removals = mirroredItems.filter((existingItem) => (
407
+ isSameMirroredContentIdentity(existingItem, contentItem)
408
+ && existingItem.id !== excludeId
409
+ ));
410
+
411
+ for (const existingItem of removals) {
412
+ await clients.dynamo.delete({
413
+ TableName: tableName,
414
+ Key: {
415
+ id: existingItem.id
416
+ }
417
+ }).promise();
418
+ }
419
+
420
+ return removals.length;
421
+ }
422
+
372
423
  export async function handler(event) {
373
424
  const clients = createAwsClients();
374
425
  const tableName = process.env.S3TE_CONTENT_TABLE;
375
426
  const renderWorkerName = process.env.S3TE_RENDER_WORKER_NAME;
376
427
  const environmentName = process.env.S3TE_ENVIRONMENT;
377
428
  const sourceTableName = process.env.S3TE_WEBINY_SOURCE_TABLE;
429
+ const contentIndexName = process.env.S3TE_CONTENT_ID_INDEX_NAME ?? "contentid";
378
430
  const configuredTenant = String(process.env.S3TE_WEBINY_TENANT ?? "").trim();
379
431
  const relevantModels = new Set(String(process.env.S3TE_RELEVANT_MODELS ?? "")
380
432
  .split(",")
@@ -411,13 +463,7 @@ export async function handler(event) {
411
463
 
412
464
  const shouldDelete = record.eventName === "REMOVE" || !isPublished(item);
413
465
  if (shouldDelete) {
414
- await clients.dynamo.delete({
415
- TableName: tableName,
416
- Key: {
417
- id: contentItem.id
418
- }
419
- }).promise();
420
- deleted += 1;
466
+ deleted += await removeMirroredContentRevisions(clients, tableName, contentIndexName, contentItem);
421
467
  await invokeLambdaEvent(clients.lambda, renderWorkerName, {
422
468
  type: "content-item",
423
469
  action: "delete",
@@ -429,6 +475,7 @@ export async function handler(event) {
429
475
  continue;
430
476
  }
431
477
 
478
+ await removeMirroredContentRevisions(clients, tableName, contentIndexName, contentItem, contentItem.id);
432
479
  await clients.dynamo.put({
433
480
  TableName: tableName,
434
481
  Item: contentItem
@@ -631,6 +631,7 @@ export function buildWebinyCloudFormationTemplate({ config, environment }) {
631
631
  Variables: {
632
632
  S3TE_ENVIRONMENT: environment,
633
633
  S3TE_CONTENT_TABLE: runtimeConfig.tables.content,
634
+ S3TE_CONTENT_ID_INDEX_NAME: config.aws.contentStore.contentIdIndexName,
634
635
  S3TE_RELEVANT_MODELS: runtimeConfig.integrations.webiny.relevantModels.join(","),
635
636
  S3TE_WEBINY_SOURCE_TABLE: runtimeConfig.integrations.webiny.sourceTableName,
636
637
  S3TE_WEBINY_TENANT: runtimeConfig.integrations.webiny.tenant ?? "",