@porulle/core 0.53.0 → 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/dist/modules/catalog/entity-service.d.ts.map +1 -1
- package/dist/modules/catalog/entity-service.js +22 -4
- package/dist/modules/customers/repository/index.d.ts +6 -0
- package/dist/modules/customers/repository/index.d.ts.map +1 -1
- package/dist/modules/customers/repository/index.js +18 -0
- package/dist/modules/customers/service.d.ts.map +1 -1
- package/dist/modules/customers/service.js +11 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/src/modules/catalog/entity-service.ts +22 -4
- package/src/modules/customers/repository/index.ts +22 -0
- package/src/modules/customers/service.ts +9 -1
package/package.json
CHANGED
|
@@ -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
|
-
|
|
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>> {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { eq, and, desc } from "drizzle-orm";
|
|
2
2
|
import type { TxContext } from "../../../kernel/database/tx-context.js";
|
|
3
|
+
import { user } from "../../../auth/auth-schema.js";
|
|
3
4
|
import type {
|
|
4
5
|
DrizzleDatabase,
|
|
5
6
|
DbOrTx,
|
|
@@ -51,6 +52,27 @@ export class CustomersRepository {
|
|
|
51
52
|
return rows[0];
|
|
52
53
|
}
|
|
53
54
|
|
|
55
|
+
/**
|
|
56
|
+
* The auth user's email, when no OTHER customer profile in this organization already holds it
|
|
57
|
+
* (`customers` is unique on organization + email). Undefined otherwise — the caller then leaves the
|
|
58
|
+
* profile's email as it is rather than fail the whole profile read on the unique index.
|
|
59
|
+
*/
|
|
60
|
+
async claimableUserEmail(
|
|
61
|
+
orgId: string,
|
|
62
|
+
userId: string,
|
|
63
|
+
ctx?: TxContext,
|
|
64
|
+
): Promise<string | undefined> {
|
|
65
|
+
const db = this.getDb(ctx);
|
|
66
|
+
const [account] = await db.select({ email: user.email }).from(user).where(eq(user.id, userId));
|
|
67
|
+
if (!account?.email) return undefined;
|
|
68
|
+
const [holder] = await db
|
|
69
|
+
.select({ userId: customers.userId })
|
|
70
|
+
.from(customers)
|
|
71
|
+
.where(and(eq(customers.organizationId, orgId), eq(customers.email, account.email)))
|
|
72
|
+
.limit(1);
|
|
73
|
+
return holder === undefined || holder.userId === userId ? account.email : undefined;
|
|
74
|
+
}
|
|
75
|
+
|
|
54
76
|
async findByUserId(
|
|
55
77
|
orgId: string,
|
|
56
78
|
userId: string,
|
|
@@ -177,16 +177,24 @@ export class CustomerService {
|
|
|
177
177
|
actor: Actor | null,
|
|
178
178
|
ctx?: TxContext,
|
|
179
179
|
): Promise<Customer> {
|
|
180
|
+
// The profile carries the shopper's email: it is the key channel redaction uses, so a profile
|
|
181
|
+
// without one cannot be erased once its orders have been exported. Filled on creation, and on
|
|
182
|
+
// the next access of a profile created before this (never overwriting an email it has).
|
|
180
183
|
const existing = await this.repo.findByUserId(orgId, userId, ctx);
|
|
181
184
|
if (existing) {
|
|
182
|
-
return existing;
|
|
185
|
+
if (existing.email) return existing;
|
|
186
|
+
const email = await this.repo.claimableUserEmail(orgId, userId, ctx);
|
|
187
|
+
if (!email) return existing;
|
|
188
|
+
return (await this.repo.update(existing.id, { email }, ctx)) ?? existing;
|
|
183
189
|
}
|
|
184
190
|
|
|
191
|
+
const email = await this.repo.claimableUserEmail(orgId, userId, ctx);
|
|
185
192
|
const customer = await this.repo.create(
|
|
186
193
|
{
|
|
187
194
|
organizationId: orgId,
|
|
188
195
|
userId,
|
|
189
196
|
metadata: {},
|
|
197
|
+
...(email !== undefined ? { email } : {}),
|
|
190
198
|
},
|
|
191
199
|
ctx,
|
|
192
200
|
);
|