@porulle/core 0.53.1 → 0.54.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.53.1",
3
+ "version": "0.54.0",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "exports": {
@@ -62,8 +62,8 @@
62
62
  "eslint": "^9.39.1",
63
63
  "typescript": "5.9.2",
64
64
  "vitest": "^3.2.4",
65
- "@porulle/typescript-config": "0.1.0",
66
- "@porulle/eslint-config": "0.1.0"
65
+ "@porulle/eslint-config": "0.1.0",
66
+ "@porulle/typescript-config": "0.1.0"
67
67
  },
68
68
  "publishConfig": {
69
69
  "access": "public"
@@ -24,7 +24,7 @@ import { paginate } from "../../utils/pagination.js";
24
24
  import type { JobsAdapter } from "../../kernel/jobs/adapter.js";
25
25
  import type { PluginDb } from "../../kernel/database/plugin-types.js";
26
26
  import type { CatalogWriteContext, TxContext } from "../../kernel/database/tx-context.js";
27
- import { isWriteContextTransactional } from "../../kernel/database/tx-context.js";
27
+ import { createTxContext, isWriteContextTransactional } from "../../kernel/database/tx-context.js";
28
28
  import { canReadUnpublishedCatalog, isCatalogEntityVisible } from "./read-policy.js";
29
29
  import { isValidFieldPath } from "./ownership.js";
30
30
  import type {
@@ -519,21 +519,36 @@ export class EntityService {
519
519
  try { this.assertSameOrg(entity, actor); } catch (error) { return Err(toCommerceError(error)); }
520
520
  const beforeAttr = await this.repo.findAttributeByLocale(entityId, locale, txCtx);
521
521
  const changedFieldPaths: string[] = [];
522
+ let changed = false;
522
523
  for (const field of attributeFields) {
523
524
  if (attrs[field] === undefined) continue;
524
525
  const before = beforeAttr?.[field] ?? null;
525
526
  const after = attrs[field] ?? null;
526
527
  if (before !== after) {
528
+ changed = true;
527
529
  const path = `attributes.${locale}.${field}`;
528
530
  if (isValidFieldPath(path)) changedFieldPaths.push(path);
529
531
  }
530
532
  }
531
- await this.repo.upsertAttribute(entityId, locale, { title: attrs.title, subtitle: attrs.subtitle, description: attrs.description, richDescription: attrs.richDescription, seoTitle: attrs.seoTitle, seoDescription: attrs.seoDescription }, txCtx);
533
+ // An identical re-set writes nothing and moves nothing: an idempotent save must not look like an
534
+ // edit to anything that versions the product.
535
+ if (!changed && beforeAttr) return Ok(undefined);
536
+
537
+ // The attribute write and the entity's `updated_at` bump commit together. Consumers version a
538
+ // product from `sellable_entities.updated_at`; an attribute change that left it unmoved was
539
+ // invisible to them — a changed title never re-indexed (sim, 2026-09-24).
540
+ const write = async (writeCtx: TxContext): Promise<SellableEntity | undefined> => {
541
+ await this.repo.upsertAttribute(entityId, locale, { title: attrs.title, subtitle: attrs.subtitle, description: attrs.description, richDescription: attrs.richDescription, seoTitle: attrs.seoTitle, seoDescription: attrs.seoDescription }, writeCtx);
542
+ return changed ? this.repo.updateEntity(entityId, {}, writeCtx) : undefined;
543
+ };
544
+ const updated = txCtx
545
+ ? await write(txCtx)
546
+ : await this.deps.database.transaction((tx) => write(createTxContext(tx, { actor })));
532
547
  if (changedFieldPaths.length > 0) {
533
548
  const afterHooks = this.deps.hooks.resolve("catalog.afterUpdate") as CatalogUpdateAfterHook[];
534
549
  const context = catalogHookContext(this.deps, actor, ctx, "update");
535
550
  context.context.changedFieldPaths = changedFieldPaths;
536
- await runAfterHooks(afterHooks, entity, entity, "update", context, (hook) => this.deps.hooks.runsInTransaction(hook));
551
+ await runAfterHooks(afterHooks, entity, updated ?? entity, "update", context, (hook) => this.deps.hooks.runsInTransaction(hook));
537
552
  }
538
553
  return Ok(undefined);
539
554
  }
@@ -563,10 +578,13 @@ export class EntityService {
563
578
  const txCtx = isWriteContextTransactional(ctx) ? ctx : undefined;
564
579
  const entity = await this.repo.findEntityById(entityId, txCtx);
565
580
  if (!entity) return;
581
+ // The related rows changed, so the entity's `updated_at` moves with them (in the caller's
582
+ // transaction when it has one) — the same versioning contract as `setAttributes`.
583
+ const updated = await this.repo.updateEntity(entityId, {}, txCtx);
566
584
  const afterHooks = this.deps.hooks.resolve("catalog.afterUpdate") as CatalogUpdateAfterHook[];
567
585
  const context = catalogHookContext(this.deps, actor, ctx, "update");
568
586
  context.context.changedFieldPaths = [...changedFieldPaths];
569
- await runAfterHooks(afterHooks, entity, entity, "update", context, (hook) => this.deps.hooks.runsInTransaction(hook));
587
+ await runAfterHooks(afterHooks, entity, updated ?? entity, "update", context, (hook) => this.deps.hooks.runsInTransaction(hook));
570
588
  }
571
589
 
572
590
  async getAttributes(entityId: string, locale: string, actor: Actor | null, ctx?: TxContext): Promise<Result<SellableAttribute>> {