@porulle/core 0.45.0 → 0.46.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@porulle/core",
3
- "version": "0.45.0",
3
+ "version": "0.46.0",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "exports": {
@@ -538,6 +538,37 @@ export class EntityService {
538
538
  return Ok(undefined);
539
539
  }
540
540
 
541
+ /**
542
+ * Fire `catalog.afterUpdate` for an entity whose RELATED rows changed while the entity row did
543
+ * not. Same before/after pair as `setAttributes` above, for the same reason: every consumer of
544
+ * this hook keys on the entity, not on the row that actually moved.
545
+ *
546
+ * It exists because `CatalogService.reviewCustomField` wrote an approved custom field straight
547
+ * through the repository and ran no hooks at all. `create`, `update` and `setAttributes` each
548
+ * resolve and run their after-hooks; that one method did not, so approving a proposal notified
549
+ * nothing. Anything downstream that keeps a derived copy of an entity's custom fields — a search
550
+ * projection, a feed, a cache — had no way to learn that a value a person had just approved was
551
+ * now the live one, and went on serving the old one until something else happened to touch the
552
+ * entity.
553
+ *
554
+ * Callers pass the paths they changed so a handler can narrow. A missing entity is not an error:
555
+ * the caller has already established it, and racing a delete is not this method's problem.
556
+ */
557
+ async notifyEntityUpdated(
558
+ entityId: string,
559
+ changedFieldPaths: readonly string[],
560
+ actor: Actor | null,
561
+ ctx?: CatalogWriteContext,
562
+ ): Promise<void> {
563
+ const txCtx = isWriteContextTransactional(ctx) ? ctx : undefined;
564
+ const entity = await this.repo.findEntityById(entityId, txCtx);
565
+ if (!entity) return;
566
+ const afterHooks = this.deps.hooks.resolve("catalog.afterUpdate") as CatalogUpdateAfterHook[];
567
+ const context = catalogHookContext(this.deps, actor, ctx, "update");
568
+ context.context.changedFieldPaths = [...changedFieldPaths];
569
+ await runAfterHooks(afterHooks, entity, entity, "update", context, (hook) => this.deps.hooks.runsInTransaction(hook));
570
+ }
571
+
541
572
  async getAttributes(entityId: string, locale: string, actor: Actor | null, ctx?: TxContext): Promise<Result<SellableAttribute>> {
542
573
  try {
543
574
  assertPermission(actor, "catalog:read");
@@ -1050,6 +1050,13 @@ export class CatalogServiceImpl implements CatalogService {
1050
1050
  await this.repository.rejectOtherProposedCustomFields(entityId, fieldName, locale, updated.id, txCtx);
1051
1051
  }
1052
1052
  await this.captureRevision(entityId, reviewer, "update", txCtx);
1053
+ // An approval REPLACES the live value: the approved row was deleted above and this proposal
1054
+ // takes its place. That is an entity change to every consumer of `catalog.afterUpdate`, and
1055
+ // until now it fired none, so an approved fact reached nothing downstream. A rejection is
1056
+ // not: the live row is untouched and there is nothing for a consumer to re-read.
1057
+ if (status === "approved") {
1058
+ await this.entities.notifyEntityUpdated(entityId, [`customFields.${locale}.${fieldName}`], reviewer, txCtx);
1059
+ }
1053
1060
  return Ok(updated);
1054
1061
  });
1055
1062
  }