@projectdochelp/s3te 3.3.1 → 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
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
BatchWriteCommand,
|
|
18
18
|
DeleteCommand,
|
|
19
19
|
DynamoDBDocumentClient,
|
|
20
|
+
GetCommand,
|
|
20
21
|
PutCommand,
|
|
21
22
|
QueryCommand,
|
|
22
23
|
ScanCommand
|
|
@@ -75,8 +76,37 @@ function localeMatchScore(itemLocale, language, languageLocaleMap) {
|
|
|
75
76
|
return 0;
|
|
76
77
|
}
|
|
77
78
|
|
|
78
|
-
function
|
|
79
|
-
|
|
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 ?? ""));
|
|
80
110
|
}
|
|
81
111
|
|
|
82
112
|
function filterItemsByRequestedLocale(items, language, languageLocaleMap) {
|
|
@@ -102,7 +132,11 @@ function filterItemsByRequestedLocale(items, language, languageLocaleMap) {
|
|
|
102
132
|
}
|
|
103
133
|
|
|
104
134
|
const bestScore = Math.max(...scored.map((entry) => entry.score));
|
|
105
|
-
return scored
|
|
135
|
+
return scored
|
|
136
|
+
.filter((entry) => entry.score === bestScore)
|
|
137
|
+
.map((entry) => entry.item)
|
|
138
|
+
.sort(compareContentFreshness)
|
|
139
|
+
.slice(0, 1);
|
|
106
140
|
});
|
|
107
141
|
}
|
|
108
142
|
|
|
@@ -143,6 +177,7 @@ export function createAwsClients(region = process.env.AWS_REGION) {
|
|
|
143
177
|
invoke: InvokeCommand
|
|
144
178
|
}),
|
|
145
179
|
dynamo: wrapCommandClient(dynamoDocumentClient, {
|
|
180
|
+
get: GetCommand,
|
|
146
181
|
query: QueryCommand,
|
|
147
182
|
scan: ScanCommand,
|
|
148
183
|
batchWrite: BatchWriteCommand,
|
|
@@ -247,7 +282,7 @@ export function buildCoreConfigFromEnvironment(manifest, environmentName) {
|
|
|
247
282
|
debounceSeconds: 60
|
|
248
283
|
},
|
|
249
284
|
lambda: {
|
|
250
|
-
runtime: "
|
|
285
|
+
runtime: "nodejs24.x",
|
|
251
286
|
architecture: "arm64"
|
|
252
287
|
}
|
|
253
288
|
},
|
|
@@ -388,14 +423,7 @@ export class DynamoContentRepository {
|
|
|
388
423
|
}
|
|
389
424
|
}).promise();
|
|
390
425
|
const items = response.Items ?? [];
|
|
391
|
-
|
|
392
|
-
.map((item) => ({
|
|
393
|
-
item,
|
|
394
|
-
score: localeMatchScore(item.locale, language, this.languageLocaleMap)
|
|
395
|
-
}))
|
|
396
|
-
.filter((entry) => entry.score > 0)
|
|
397
|
-
.sort((left, right) => right.score - left.score || String(left.item.id).localeCompare(String(right.item.id)));
|
|
398
|
-
return candidates[0]?.item ?? null;
|
|
426
|
+
return filterItemsByRequestedLocale(items, language, this.languageLocaleMap)[0] ?? null;
|
|
399
427
|
}
|
|
400
428
|
|
|
401
429
|
async query(query, language) {
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { gunzipSync } from "node:zlib";
|
|
2
|
+
|
|
1
3
|
import { createAwsClients, invokeLambdaEvent } from "./common.mjs";
|
|
2
4
|
|
|
3
5
|
function escapeHtml(value) {
|
|
@@ -134,10 +136,118 @@ function toSimpleValue(value) {
|
|
|
134
136
|
return String(value);
|
|
135
137
|
}
|
|
136
138
|
|
|
137
|
-
function
|
|
138
|
-
|
|
139
|
-
? item.
|
|
140
|
-
:
|
|
139
|
+
function getItemRoot(item) {
|
|
140
|
+
return hasNestedEntryEnvelope(item)
|
|
141
|
+
? item.data
|
|
142
|
+
: item;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function hasNestedEntryEnvelope(item) {
|
|
146
|
+
if (!item?.data || typeof item.data !== "object" || Array.isArray(item.data)) {
|
|
147
|
+
return false;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return [
|
|
151
|
+
"id",
|
|
152
|
+
"entryId",
|
|
153
|
+
"model",
|
|
154
|
+
"modelId",
|
|
155
|
+
"tenant",
|
|
156
|
+
"tenantId",
|
|
157
|
+
"status",
|
|
158
|
+
"values",
|
|
159
|
+
"createdOn",
|
|
160
|
+
"savedOn",
|
|
161
|
+
"publishedOn",
|
|
162
|
+
"lastPublishedOn"
|
|
163
|
+
].some((key) => Object.prototype.hasOwnProperty.call(item.data, key));
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function decodeCompressedValue(value) {
|
|
167
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
168
|
+
return value;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (typeof value.compression !== "string" || typeof value.value !== "string") {
|
|
172
|
+
return value;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
try {
|
|
176
|
+
const compressed = Buffer.from(value.value, "base64");
|
|
177
|
+
if (value.compression === "gzip") {
|
|
178
|
+
const inflated = gunzipSync(compressed).toString("utf8");
|
|
179
|
+
if (value.isArray) {
|
|
180
|
+
return JSON.parse(inflated);
|
|
181
|
+
}
|
|
182
|
+
return inflated;
|
|
183
|
+
}
|
|
184
|
+
} catch {
|
|
185
|
+
return "";
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return value.value;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function getFieldIdentifiers(field) {
|
|
192
|
+
return [field?.storageId, field?.fieldId].filter(Boolean);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function findFieldValue(rawValues, field) {
|
|
196
|
+
for (const identifier of getFieldIdentifiers(field)) {
|
|
197
|
+
if (Object.prototype.hasOwnProperty.call(rawValues, identifier)) {
|
|
198
|
+
return rawValues[identifier];
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
return undefined;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function normalizeFieldValue(rawValue, field) {
|
|
205
|
+
const decodedValue = decodeCompressedValue(rawValue);
|
|
206
|
+
|
|
207
|
+
if (field?.type === "object") {
|
|
208
|
+
const nestedFields = Array.isArray(field.settings?.fields) ? field.settings.fields : [];
|
|
209
|
+
if (Array.isArray(decodedValue)) {
|
|
210
|
+
return decodedValue.map((entry) => normalizeFieldValue(entry, {
|
|
211
|
+
...field,
|
|
212
|
+
list: false
|
|
213
|
+
}));
|
|
214
|
+
}
|
|
215
|
+
if (decodedValue && typeof decodedValue === "object") {
|
|
216
|
+
return normalizeMappedValues(decodedValue, nestedFields);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (Array.isArray(decodedValue)) {
|
|
221
|
+
return decodedValue.map((entry) => toSimpleValue(entry));
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return toSimpleValue(decodedValue);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function normalizeMappedValues(rawValues, fields = []) {
|
|
228
|
+
const values = {};
|
|
229
|
+
for (const field of fields) {
|
|
230
|
+
const rawValue = findFieldValue(rawValues, field);
|
|
231
|
+
if (rawValue === undefined) {
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
234
|
+
values[field.fieldId] = normalizeFieldValue(rawValue, field);
|
|
235
|
+
}
|
|
236
|
+
return values;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function normalizeValues(item, modelFields = []) {
|
|
240
|
+
const root = getItemRoot(item);
|
|
241
|
+
const valueSource = root?.values && typeof root.values === "object"
|
|
242
|
+
? root.values
|
|
243
|
+
: (!hasNestedEntryEnvelope(item) && item?.data && typeof item.data === "object" && !Array.isArray(item.data) ? item.data : null);
|
|
244
|
+
|
|
245
|
+
if (valueSource && Array.isArray(modelFields) && modelFields.length > 0) {
|
|
246
|
+
const mappedValues = normalizeMappedValues(valueSource, modelFields);
|
|
247
|
+
if (Object.keys(mappedValues).length > 0) {
|
|
248
|
+
return mappedValues;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
141
251
|
|
|
142
252
|
if (valueSource) {
|
|
143
253
|
return Object.fromEntries(Object.entries(valueSource).map(([key, value]) => [key, toSimpleValue(value)]));
|
|
@@ -178,44 +288,62 @@ function normalizeValues(item) {
|
|
|
178
288
|
}
|
|
179
289
|
|
|
180
290
|
function isPublished(item) {
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
||
|
|
184
|
-
||
|
|
291
|
+
const root = getItemRoot(item);
|
|
292
|
+
return root.status === "published"
|
|
293
|
+
|| root.published === true
|
|
294
|
+
|| root.isPublished === true
|
|
295
|
+
|| root.publishedOn != null
|
|
296
|
+
|| root.firstPublishedOn != null
|
|
297
|
+
|| root.lastPublishedOn != null;
|
|
185
298
|
}
|
|
186
299
|
|
|
187
300
|
function extractWebinyLocale(item) {
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
??
|
|
191
|
-
??
|
|
301
|
+
const root = getItemRoot(item);
|
|
302
|
+
return root.locale
|
|
303
|
+
?? root.localeCode
|
|
304
|
+
?? root.i18n?.locale?.code
|
|
305
|
+
?? root.i18n?.localeCode
|
|
192
306
|
?? null;
|
|
193
307
|
}
|
|
194
308
|
|
|
195
309
|
function extractWebinyTenant(item) {
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
??
|
|
310
|
+
const root = getItemRoot(item);
|
|
311
|
+
return root.tenant
|
|
312
|
+
?? root.tenantId
|
|
313
|
+
?? root.createdBy?.tenant
|
|
199
314
|
?? null;
|
|
200
315
|
}
|
|
201
316
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
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
|
+
|
|
328
|
+
export function normalizeContentItem(item, options = {}) {
|
|
329
|
+
const root = getItemRoot(item);
|
|
330
|
+
const values = normalizeValues(item, options.modelFields);
|
|
331
|
+
const model = root.model
|
|
332
|
+
?? root.modelId
|
|
333
|
+
?? root.__typename
|
|
334
|
+
?? root.contentModel?.modelId
|
|
207
335
|
?? null;
|
|
208
336
|
return {
|
|
209
|
-
id: item.id,
|
|
210
|
-
contentId:
|
|
337
|
+
id: root.id ?? item.id,
|
|
338
|
+
contentId: values.contentId ?? values.contentid ?? root.contentId ?? root.contentid ?? root.entryId ?? root.id ?? item.id,
|
|
211
339
|
model,
|
|
212
340
|
locale: extractWebinyLocale(item) ?? undefined,
|
|
213
341
|
tenant: extractWebinyTenant(item) ?? undefined,
|
|
214
|
-
values
|
|
215
|
-
createdAt:
|
|
216
|
-
updatedAt:
|
|
217
|
-
version:
|
|
218
|
-
lastChangedAt:
|
|
342
|
+
values,
|
|
343
|
+
createdAt: root.createdAt ?? root.createdOn,
|
|
344
|
+
updatedAt: root.updatedAt ?? root.savedOn ?? root.publishedOn ?? root.lastPublishedOn,
|
|
345
|
+
version: root._version ?? root.version,
|
|
346
|
+
lastChangedAt: root._lastChangedAt ?? root.lastChangedAt
|
|
219
347
|
};
|
|
220
348
|
}
|
|
221
349
|
|
|
@@ -224,20 +352,87 @@ export function matchesConfiguredTenant(item, configuredTenant) {
|
|
|
224
352
|
return true;
|
|
225
353
|
}
|
|
226
354
|
|
|
227
|
-
const
|
|
355
|
+
const root = getItemRoot(item);
|
|
356
|
+
const tenant = root.tenant ?? root.tenantId ?? root.createdBy?.tenant ?? null;
|
|
228
357
|
return tenant != null && String(tenant) === String(configuredTenant);
|
|
229
358
|
}
|
|
230
359
|
|
|
360
|
+
async function loadModelFields(clients, sourceTableName, tenant, modelId, cache) {
|
|
361
|
+
if (!sourceTableName || !tenant || !modelId) {
|
|
362
|
+
return [];
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
const cacheKey = `${tenant}#${modelId}`;
|
|
366
|
+
if (cache.has(cacheKey)) {
|
|
367
|
+
return cache.get(cacheKey);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
const response = await clients.dynamo.get({
|
|
371
|
+
TableName: sourceTableName,
|
|
372
|
+
Key: {
|
|
373
|
+
PK: `T#${tenant}#CMS#CM`,
|
|
374
|
+
SK: modelId
|
|
375
|
+
}
|
|
376
|
+
}).promise();
|
|
377
|
+
const model = response.Item?.data ?? response.Item ?? null;
|
|
378
|
+
const fields = Array.isArray(model?.fields) ? model.fields : [];
|
|
379
|
+
cache.set(cacheKey, fields);
|
|
380
|
+
return fields;
|
|
381
|
+
}
|
|
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
|
+
|
|
231
423
|
export async function handler(event) {
|
|
232
424
|
const clients = createAwsClients();
|
|
233
425
|
const tableName = process.env.S3TE_CONTENT_TABLE;
|
|
234
426
|
const renderWorkerName = process.env.S3TE_RENDER_WORKER_NAME;
|
|
235
427
|
const environmentName = process.env.S3TE_ENVIRONMENT;
|
|
428
|
+
const sourceTableName = process.env.S3TE_WEBINY_SOURCE_TABLE;
|
|
429
|
+
const contentIndexName = process.env.S3TE_CONTENT_ID_INDEX_NAME ?? "contentid";
|
|
236
430
|
const configuredTenant = String(process.env.S3TE_WEBINY_TENANT ?? "").trim();
|
|
237
431
|
const relevantModels = new Set(String(process.env.S3TE_RELEVANT_MODELS ?? "")
|
|
238
432
|
.split(",")
|
|
239
433
|
.map((entry) => entry.trim())
|
|
240
434
|
.filter(Boolean));
|
|
435
|
+
const modelFieldCache = new Map();
|
|
241
436
|
|
|
242
437
|
let mirrored = 0;
|
|
243
438
|
let deleted = 0;
|
|
@@ -253,20 +448,22 @@ export async function handler(event) {
|
|
|
253
448
|
continue;
|
|
254
449
|
}
|
|
255
450
|
|
|
256
|
-
const
|
|
451
|
+
const itemRoot = getItemRoot(item);
|
|
452
|
+
const modelFields = await loadModelFields(
|
|
453
|
+
clients,
|
|
454
|
+
sourceTableName,
|
|
455
|
+
extractWebinyTenant(item),
|
|
456
|
+
itemRoot?.modelId ?? itemRoot?.model,
|
|
457
|
+
modelFieldCache
|
|
458
|
+
);
|
|
459
|
+
const contentItem = normalizeContentItem(item, { modelFields });
|
|
257
460
|
if (!contentItem.id || !contentItem.model || (relevantModels.size > 0 && !relevantModels.has(contentItem.model))) {
|
|
258
461
|
continue;
|
|
259
462
|
}
|
|
260
463
|
|
|
261
464
|
const shouldDelete = record.eventName === "REMOVE" || !isPublished(item);
|
|
262
465
|
if (shouldDelete) {
|
|
263
|
-
await clients
|
|
264
|
-
TableName: tableName,
|
|
265
|
-
Key: {
|
|
266
|
-
id: contentItem.id
|
|
267
|
-
}
|
|
268
|
-
}).promise();
|
|
269
|
-
deleted += 1;
|
|
466
|
+
deleted += await removeMirroredContentRevisions(clients, tableName, contentIndexName, contentItem);
|
|
270
467
|
await invokeLambdaEvent(clients.lambda, renderWorkerName, {
|
|
271
468
|
type: "content-item",
|
|
272
469
|
action: "delete",
|
|
@@ -278,6 +475,7 @@ export async function handler(event) {
|
|
|
278
475
|
continue;
|
|
279
476
|
}
|
|
280
477
|
|
|
478
|
+
await removeMirroredContentRevisions(clients, tableName, contentIndexName, contentItem, contentItem.id);
|
|
281
479
|
await clients.dynamo.put({
|
|
282
480
|
TableName: tableName,
|
|
283
481
|
Item: contentItem
|
|
@@ -631,7 +631,9 @@ 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(","),
|
|
636
|
+
S3TE_WEBINY_SOURCE_TABLE: runtimeConfig.integrations.webiny.sourceTableName,
|
|
635
637
|
S3TE_WEBINY_TENANT: runtimeConfig.integrations.webiny.tenant ?? "",
|
|
636
638
|
S3TE_RENDER_WORKER_NAME: functionNames.renderWorker
|
|
637
639
|
}
|
|
@@ -277,7 +277,7 @@ export function resolveProjectConfig(projectConfig) {
|
|
|
277
277
|
debounceSeconds: projectConfig.aws?.invalidationStore?.debounceSeconds ?? 60
|
|
278
278
|
},
|
|
279
279
|
lambda: {
|
|
280
|
-
runtime: projectConfig.aws?.lambda?.runtime ?? "
|
|
280
|
+
runtime: projectConfig.aws?.lambda?.runtime ?? "nodejs24.x",
|
|
281
281
|
architecture: projectConfig.aws?.lambda?.architecture ?? "arm64"
|
|
282
282
|
}
|
|
283
283
|
};
|