@projectdochelp/s3te 3.3.1 → 3.3.2

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.1",
3
+ "version": "3.3.2",
4
4
  "description": "CLI, render core, AWS adapter, and testkit for S3TemplateEngine projects",
5
5
  "repository": {
6
6
  "type": "git",
@@ -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
@@ -143,6 +144,7 @@ export function createAwsClients(region = process.env.AWS_REGION) {
143
144
  invoke: InvokeCommand
144
145
  }),
145
146
  dynamo: wrapCommandClient(dynamoDocumentClient, {
147
+ get: GetCommand,
146
148
  query: QueryCommand,
147
149
  scan: ScanCommand,
148
150
  batchWrite: BatchWriteCommand,
@@ -247,7 +249,7 @@ export function buildCoreConfigFromEnvironment(manifest, environmentName) {
247
249
  debounceSeconds: 60
248
250
  },
249
251
  lambda: {
250
- runtime: "nodejs22.x",
252
+ runtime: "nodejs24.x",
251
253
  architecture: "arm64"
252
254
  }
253
255
  },
@@ -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 normalizeValues(item) {
138
- const valueSource = item.values && typeof item.values === "object"
139
- ? item.values
140
- : (item.data && typeof item.data === "object" && !Array.isArray(item.data) ? item.data : null);
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,51 @@ function normalizeValues(item) {
178
288
  }
179
289
 
180
290
  function isPublished(item) {
181
- return item.status === "published"
182
- || item.published === true
183
- || item.isPublished === true
184
- || item.publishedOn != null;
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
- return item.locale
189
- ?? item.localeCode
190
- ?? item.i18n?.locale?.code
191
- ?? item.i18n?.localeCode
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
- return item.tenant
197
- ?? item.tenantId
198
- ?? item.createdBy?.tenant
310
+ const root = getItemRoot(item);
311
+ return root.tenant
312
+ ?? root.tenantId
313
+ ?? root.createdBy?.tenant
199
314
  ?? null;
200
315
  }
201
316
 
202
- export function normalizeContentItem(item) {
203
- const model = item.model
204
- ?? item.modelId
205
- ?? item.__typename
206
- ?? item.contentModel?.modelId
317
+ export function normalizeContentItem(item, options = {}) {
318
+ const root = getItemRoot(item);
319
+ const values = normalizeValues(item, options.modelFields);
320
+ const model = root.model
321
+ ?? root.modelId
322
+ ?? root.__typename
323
+ ?? root.contentModel?.modelId
207
324
  ?? null;
208
325
  return {
209
- id: item.id,
210
- contentId: item.contentId ?? item.contentid ?? item.entryId ?? item.id,
326
+ id: root.id ?? item.id,
327
+ contentId: values.contentId ?? values.contentid ?? root.contentId ?? root.contentid ?? root.entryId ?? root.id ?? item.id,
211
328
  model,
212
329
  locale: extractWebinyLocale(item) ?? undefined,
213
330
  tenant: extractWebinyTenant(item) ?? undefined,
214
- values: normalizeValues(item),
215
- createdAt: item.createdAt ?? item.createdOn,
216
- updatedAt: item.updatedAt ?? item.savedOn ?? item.publishedOn,
217
- version: item._version ?? item.version,
218
- lastChangedAt: item._lastChangedAt ?? item.lastChangedAt
331
+ values,
332
+ createdAt: root.createdAt ?? root.createdOn,
333
+ updatedAt: root.updatedAt ?? root.savedOn ?? root.publishedOn ?? root.lastPublishedOn,
334
+ version: root._version ?? root.version,
335
+ lastChangedAt: root._lastChangedAt ?? root.lastChangedAt
219
336
  };
220
337
  }
221
338
 
@@ -224,20 +341,46 @@ export function matchesConfiguredTenant(item, configuredTenant) {
224
341
  return true;
225
342
  }
226
343
 
227
- const tenant = item.tenant ?? item.tenantId ?? item.createdBy?.tenant ?? null;
344
+ const root = getItemRoot(item);
345
+ const tenant = root.tenant ?? root.tenantId ?? root.createdBy?.tenant ?? null;
228
346
  return tenant != null && String(tenant) === String(configuredTenant);
229
347
  }
230
348
 
349
+ async function loadModelFields(clients, sourceTableName, tenant, modelId, cache) {
350
+ if (!sourceTableName || !tenant || !modelId) {
351
+ return [];
352
+ }
353
+
354
+ const cacheKey = `${tenant}#${modelId}`;
355
+ if (cache.has(cacheKey)) {
356
+ return cache.get(cacheKey);
357
+ }
358
+
359
+ const response = await clients.dynamo.get({
360
+ TableName: sourceTableName,
361
+ Key: {
362
+ PK: `T#${tenant}#CMS#CM`,
363
+ SK: modelId
364
+ }
365
+ }).promise();
366
+ const model = response.Item?.data ?? response.Item ?? null;
367
+ const fields = Array.isArray(model?.fields) ? model.fields : [];
368
+ cache.set(cacheKey, fields);
369
+ return fields;
370
+ }
371
+
231
372
  export async function handler(event) {
232
373
  const clients = createAwsClients();
233
374
  const tableName = process.env.S3TE_CONTENT_TABLE;
234
375
  const renderWorkerName = process.env.S3TE_RENDER_WORKER_NAME;
235
376
  const environmentName = process.env.S3TE_ENVIRONMENT;
377
+ const sourceTableName = process.env.S3TE_WEBINY_SOURCE_TABLE;
236
378
  const configuredTenant = String(process.env.S3TE_WEBINY_TENANT ?? "").trim();
237
379
  const relevantModels = new Set(String(process.env.S3TE_RELEVANT_MODELS ?? "")
238
380
  .split(",")
239
381
  .map((entry) => entry.trim())
240
382
  .filter(Boolean));
383
+ const modelFieldCache = new Map();
241
384
 
242
385
  let mirrored = 0;
243
386
  let deleted = 0;
@@ -253,7 +396,15 @@ export async function handler(event) {
253
396
  continue;
254
397
  }
255
398
 
256
- const contentItem = normalizeContentItem(item);
399
+ const itemRoot = getItemRoot(item);
400
+ const modelFields = await loadModelFields(
401
+ clients,
402
+ sourceTableName,
403
+ extractWebinyTenant(item),
404
+ itemRoot?.modelId ?? itemRoot?.model,
405
+ modelFieldCache
406
+ );
407
+ const contentItem = normalizeContentItem(item, { modelFields });
257
408
  if (!contentItem.id || !contentItem.model || (relevantModels.size > 0 && !relevantModels.has(contentItem.model))) {
258
409
  continue;
259
410
  }
@@ -632,6 +632,7 @@ export function buildWebinyCloudFormationTemplate({ config, environment }) {
632
632
  S3TE_ENVIRONMENT: environment,
633
633
  S3TE_CONTENT_TABLE: runtimeConfig.tables.content,
634
634
  S3TE_RELEVANT_MODELS: runtimeConfig.integrations.webiny.relevantModels.join(","),
635
+ S3TE_WEBINY_SOURCE_TABLE: runtimeConfig.integrations.webiny.sourceTableName,
635
636
  S3TE_WEBINY_TENANT: runtimeConfig.integrations.webiny.tenant ?? "",
636
637
  S3TE_RENDER_WORKER_NAME: functionNames.renderWorker
637
638
  }
@@ -291,7 +291,7 @@ function schemaTemplate() {
291
291
  properties: {
292
292
  runtime: {
293
293
  type: "string",
294
- enum: ["nodejs22.x"]
294
+ enum: ["nodejs24.x"]
295
295
  },
296
296
  architecture: {
297
297
  type: "string",
@@ -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 ?? "nodejs22.x",
280
+ runtime: projectConfig.aws?.lambda?.runtime ?? "nodejs24.x",
281
281
  architecture: projectConfig.aws?.lambda?.architecture ?? "arm64"
282
282
  }
283
283
  };