@plusscommunities/pluss-core-aws 2.0.25-beta.6 → 2.0.25-beta.8
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/helper/hqPublishing.js +105 -26
- package/package.json +1 -1
package/helper/hqPublishing.js
CHANGED
|
@@ -144,17 +144,43 @@ const fanOutHqSource = async (hqSourceRow, tableName) => {
|
|
|
144
144
|
if (targets.length === 0) {
|
|
145
145
|
return [];
|
|
146
146
|
}
|
|
147
|
+
return writeCopiesForTargets(hqSourceRow, targets, tableName);
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Write one community-copy row per site in `targetSites` (all of the source's
|
|
152
|
+
* PublishedToSites on INSERT fan-out, or just the newly-added subset on a
|
|
153
|
+
* MODIFY target-set change). Shared by fanOutHqSource and syncHqTargets. The
|
|
154
|
+
* deterministic RowId makes each write idempotent AND makes re-adding a
|
|
155
|
+
* previously-removed (soft-deleted) site a lossless resurrection — the PUT
|
|
156
|
+
* overwrites the row and drops the Deleted flag.
|
|
157
|
+
*
|
|
158
|
+
* @param {object} hqSourceRow — the HQ-source row.
|
|
159
|
+
* @param {string[]} targetSites — sites to write copies for.
|
|
160
|
+
* @param {string} tableName — entity's table name.
|
|
161
|
+
* @returns {Promise<object[]>} the copy rows written.
|
|
162
|
+
*/
|
|
163
|
+
const writeCopiesForTargets = (hqSourceRow, targetSites, tableName) => {
|
|
147
164
|
const copyId = `hqcopy-${hqSourceRow.Id}`;
|
|
148
|
-
//
|
|
149
|
-
//
|
|
150
|
-
//
|
|
151
|
-
//
|
|
152
|
-
//
|
|
153
|
-
//
|
|
154
|
-
// the
|
|
165
|
+
// Preserve the HQ-source row's own UnixTimestamp on each copy rather than
|
|
166
|
+
// stamping "now". For an immediate publish the source timestamp already IS
|
|
167
|
+
// ~now, so feed ranking is unchanged; for a SCHEDULED publish (future
|
|
168
|
+
// timestamp set via the compose "Schedule Post" tab) preserving it keeps the
|
|
169
|
+
// copies hidden until the scheduled moment — BOTH consumers key off it: the
|
|
170
|
+
// noFuture feed filter, and the deferred-notification scheduler
|
|
171
|
+
// (watchNewsletter / the INSERT-path checkSendNotifications both gate on
|
|
172
|
+
// `UnixTimestamp < now`). Stamping "now" here published scheduled HQ posts
|
|
173
|
+
// (feed + push) immediately, ignoring the schedule. Fall back to now only if
|
|
174
|
+
// the source somehow lacks a timestamp.
|
|
175
|
+
//
|
|
176
|
+
// IMPORTANT: milliseconds (.valueOf), not seconds (.unix) — parity with the
|
|
177
|
+
// addNewsletterEntry UnixTimestamp convention. UnixTimestampReverse-keyed
|
|
178
|
+
// feed queries would otherwise mis-rank an ms value against a seconds value
|
|
179
|
+
// (MAX_SAFE_INTEGER minus 10^9 vs minus 10^12).
|
|
155
180
|
const now = moment.utc().valueOf();
|
|
181
|
+
const publishTs = hqSourceRow.UnixTimestamp || now;
|
|
156
182
|
|
|
157
|
-
const writes =
|
|
183
|
+
const writes = targetSites.map((targetSiteId) => {
|
|
158
184
|
// Spread + override pattern: keep all content fields, swap identity
|
|
159
185
|
// fields, drop HQ-only fields. Spread is shallow — nested objects
|
|
160
186
|
// (Author, etc.) share references with the source row, which is fine
|
|
@@ -164,8 +190,8 @@ const fanOutHqSource = async (hqSourceRow, tableName) => {
|
|
|
164
190
|
copy.Site = targetSiteId;
|
|
165
191
|
copy.RowId = `${targetSiteId}_${copyId}`;
|
|
166
192
|
copy.HqSourceId = hqSourceRow.Id;
|
|
167
|
-
copy.UnixTimestamp =
|
|
168
|
-
copy.UnixTimestampReverse = Number.MAX_SAFE_INTEGER -
|
|
193
|
+
copy.UnixTimestamp = publishTs;
|
|
194
|
+
copy.UnixTimestampReverse = Number.MAX_SAFE_INTEGER - publishTs;
|
|
169
195
|
// Each copy's own INSERT fires publishNotifications via the existing
|
|
170
196
|
// pipeline. Setting UnsentNotification: "X" matches the convention
|
|
171
197
|
// used by addNewsletterEntry when caller requested a notification.
|
|
@@ -173,11 +199,73 @@ const fanOutHqSource = async (hqSourceRow, tableName) => {
|
|
|
173
199
|
// HQ-only fields don't travel:
|
|
174
200
|
delete copy.PublishedToSites;
|
|
175
201
|
delete copy.Deleted; // copies start undeleted; retract cascade is separate
|
|
202
|
+
// Tagged references HQ-site users; on a community copy those IDs are
|
|
203
|
+
// orphaned (cosmetic "tagged people" display + a createUserEntries write
|
|
204
|
+
// keyed on non-members in newsletterChanged's INSERT branch). Audience
|
|
205
|
+
// targeting (All/Category) already governs who sees the copy.
|
|
206
|
+
delete copy.Tagged;
|
|
176
207
|
return updateRef(tableName, copy).then(() => copy);
|
|
177
208
|
});
|
|
178
209
|
return Promise.all(writes);
|
|
179
210
|
};
|
|
180
211
|
|
|
212
|
+
/**
|
|
213
|
+
* Reconcile community copies when an HQ-source MODIFY changes PublishedToSites:
|
|
214
|
+
* fan out copies for newly-ADDED sites, soft-delete copies for REMOVED sites.
|
|
215
|
+
* Content propagation to surviving copies is propagateHqEdit's job; this only
|
|
216
|
+
* reconciles the target SET. Re-adding a removed site resurrects its copy
|
|
217
|
+
* losslessly (deterministic RowId + PUT — see writeCopiesForTargets).
|
|
218
|
+
*
|
|
219
|
+
* MUST run sequentially relative to propagateHqEdit (never concurrently): both
|
|
220
|
+
* use editRef (get-merge-put), and two concurrent writes to the same removed-
|
|
221
|
+
* copy RowId would race (one clobbering the other's Deleted flag).
|
|
222
|
+
*
|
|
223
|
+
* @param {object} prevRow — OldImage of the HQ-source row.
|
|
224
|
+
* @param {object} nextRow — NewImage of the HQ-source row.
|
|
225
|
+
* @param {string} tableName — entity's table name.
|
|
226
|
+
* @param {object} [options]
|
|
227
|
+
* @param {string} [options.indexName="NewsletterEntriesHqSourceIdIndex"]
|
|
228
|
+
* @returns {Promise<{added: number, removed: number}>}
|
|
229
|
+
*/
|
|
230
|
+
const syncHqTargets = async (prevRow, nextRow, tableName, options = {}) => {
|
|
231
|
+
const indexName = options.indexName || "NewsletterEntriesHqSourceIdIndex";
|
|
232
|
+
if (!nextRow || nextRow.Site !== "hq" || nextRow.Deleted) {
|
|
233
|
+
return { added: 0, removed: 0 };
|
|
234
|
+
}
|
|
235
|
+
const prev = Array.isArray(prevRow?.PublishedToSites)
|
|
236
|
+
? prevRow.PublishedToSites
|
|
237
|
+
: [];
|
|
238
|
+
const next = Array.isArray(nextRow.PublishedToSites)
|
|
239
|
+
? nextRow.PublishedToSites
|
|
240
|
+
: [];
|
|
241
|
+
const added = next.filter((s) => !prev.includes(s));
|
|
242
|
+
const removed = prev.filter((s) => !next.includes(s));
|
|
243
|
+
|
|
244
|
+
if (added.length > 0) {
|
|
245
|
+
await writeCopiesForTargets(nextRow, added, tableName);
|
|
246
|
+
}
|
|
247
|
+
if (removed.length > 0) {
|
|
248
|
+
const copies = await findCopiesByHqSourceId(
|
|
249
|
+
nextRow.Id,
|
|
250
|
+
tableName,
|
|
251
|
+
indexName,
|
|
252
|
+
);
|
|
253
|
+
const toRemove = copies.filter((c) => removed.includes(c.Site));
|
|
254
|
+
// Per-call fresh payload — editRef sync-mutates updates[keyCol]; sharing
|
|
255
|
+
// across Promise.all(map) would leak RowId between iterations (same
|
|
256
|
+
// rationale as propagateHqEdit / cascadeHqRetract).
|
|
257
|
+
await Promise.all(
|
|
258
|
+
toRemove.map((c) =>
|
|
259
|
+
editRef(tableName, "RowId", c.RowId, {
|
|
260
|
+
Deleted: true,
|
|
261
|
+
Changed: moment.utc().unix(),
|
|
262
|
+
}),
|
|
263
|
+
),
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
return { added: added.length, removed: removed.length };
|
|
267
|
+
};
|
|
268
|
+
|
|
181
269
|
/**
|
|
182
270
|
* Propagate an HQ-source MODIFY to every community copy via the GSI lookup.
|
|
183
271
|
* Skips the retract case (where `Deleted: true`) — that flows through
|
|
@@ -312,8 +400,8 @@ const cascadeHqRetract = async (hqSourceRow, tableName, options = {}) => {
|
|
|
312
400
|
* @param {string} templateId — the `Id` of the contentLibrary row.
|
|
313
401
|
* @param {string} contentLibraryTable — entity-agnostic; caller passes the
|
|
314
402
|
* table name (without prefix).
|
|
315
|
-
* @returns {Promise<boolean>} `true` if increment applied; `false`
|
|
316
|
-
*
|
|
403
|
+
* @returns {Promise<boolean>} `true` if increment applied; `false` if table
|
|
404
|
+
* doesn't exist (graceful skip).
|
|
317
405
|
*/
|
|
318
406
|
const incrementTemplateUseCount = async (templateId, contentLibraryTable) => {
|
|
319
407
|
if (!templateId || !contentLibraryTable) {
|
|
@@ -322,16 +410,11 @@ const incrementTemplateUseCount = async (templateId, contentLibraryTable) => {
|
|
|
322
410
|
const params = {
|
|
323
411
|
TableName: `${process.env.tablePrefix}${contentLibraryTable}`,
|
|
324
412
|
Key: { Id: templateId },
|
|
325
|
-
UpdateExpression:
|
|
326
|
-
|
|
327
|
-
// it with UseCount=1. Only increment a template that actually exists; a
|
|
328
|
-
// stale/bogus TemplateId is skipped (ConditionalCheckFailed below).
|
|
329
|
-
ConditionExpression: "attribute_exists(Id)",
|
|
413
|
+
UpdateExpression:
|
|
414
|
+
"ADD UseCount :inc SET LastUsedTimestamp = :now",
|
|
330
415
|
ExpressionAttributeValues: {
|
|
331
416
|
":inc": 1,
|
|
332
|
-
|
|
333
|
-
// UpdatedAt). Was .unix() (seconds); reconciled to .valueOf().
|
|
334
|
-
":now": moment().utc().valueOf(),
|
|
417
|
+
":now": moment.utc().unix(),
|
|
335
418
|
},
|
|
336
419
|
};
|
|
337
420
|
try {
|
|
@@ -349,12 +432,6 @@ const incrementTemplateUseCount = async (templateId, contentLibraryTable) => {
|
|
|
349
432
|
);
|
|
350
433
|
return false;
|
|
351
434
|
}
|
|
352
|
-
if (error?.code === "ConditionalCheckFailedException") {
|
|
353
|
-
// TemplateId references no existing template — skip rather than
|
|
354
|
-
// create a phantom row. Not an error; continue the stream handler.
|
|
355
|
-
log("incrementTemplateUseCount", "NoSuchTemplate", { templateId });
|
|
356
|
-
return false;
|
|
357
|
-
}
|
|
358
435
|
// Anything else is genuinely unexpected — surface it.
|
|
359
436
|
log("incrementTemplateUseCount", "Error", error);
|
|
360
437
|
throw error;
|
|
@@ -367,6 +444,8 @@ module.exports = {
|
|
|
367
444
|
// Story B
|
|
368
445
|
findCopiesByHqSourceId,
|
|
369
446
|
fanOutHqSource,
|
|
447
|
+
writeCopiesForTargets,
|
|
448
|
+
syncHqTargets,
|
|
370
449
|
propagateHqEdit,
|
|
371
450
|
cascadeHqRetract,
|
|
372
451
|
incrementTemplateUseCount,
|
package/package.json
CHANGED