@porulle/core 0.11.0 → 0.14.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/auth/actor.d.ts +10 -0
- package/dist/auth/actor.d.ts.map +1 -0
- package/dist/auth/actor.js +91 -0
- package/dist/auth/auth-schema-guard.d.ts +2444 -0
- package/dist/auth/auth-schema-guard.d.ts.map +1 -0
- package/dist/auth/auth-schema-guard.js +113 -0
- package/dist/auth/auth-schema.d.ts +228 -0
- package/dist/auth/auth-schema.d.ts.map +1 -1
- package/dist/auth/auth-schema.js +24 -2
- package/dist/auth/middleware.d.ts +1 -0
- package/dist/auth/middleware.d.ts.map +1 -1
- package/dist/auth/middleware.js +16 -92
- package/dist/auth/org.d.ts +8 -2
- package/dist/auth/org.d.ts.map +1 -1
- package/dist/auth/org.js +12 -4
- package/dist/auth/organization-guard.d.ts +5 -0
- package/dist/auth/organization-guard.d.ts.map +1 -0
- package/dist/auth/organization-guard.js +124 -0
- package/dist/auth/permissions.d.ts +10 -0
- package/dist/auth/permissions.d.ts.map +1 -1
- package/dist/auth/permissions.js +31 -7
- package/dist/auth/role-authority.d.ts +44 -0
- package/dist/auth/role-authority.d.ts.map +1 -0
- package/dist/auth/role-authority.js +85 -0
- package/dist/auth/setup.d.ts.map +1 -1
- package/dist/auth/setup.js +19 -1
- package/dist/auth/strict-org-resolution.d.ts.map +1 -1
- package/dist/auth/strict-org-resolution.js +9 -1
- package/dist/auth/types.d.ts +1 -1
- package/dist/auth/types.d.ts.map +1 -1
- package/dist/config/defaults.d.ts.map +1 -1
- package/dist/config/defaults.js +2 -0
- package/dist/config/types.d.ts +32 -4
- package/dist/config/types.d.ts.map +1 -1
- package/dist/hooks/checkout.d.ts.map +1 -1
- package/dist/hooks/checkout.js +10 -9
- package/dist/index.d.ts +7 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -3
- package/dist/interfaces/rest/audit-middleware.d.ts.map +1 -1
- package/dist/interfaces/rest/audit-middleware.js +2 -0
- package/dist/interfaces/rest/customer-portal.d.ts.map +1 -1
- package/dist/interfaces/rest/customer-portal.js +9 -8
- package/dist/interfaces/rest/route-coverage.d.ts +14 -0
- package/dist/interfaces/rest/route-coverage.d.ts.map +1 -0
- package/dist/interfaces/rest/route-coverage.js +81 -0
- package/dist/interfaces/rest/router.d.ts +7 -2
- package/dist/interfaces/rest/router.d.ts.map +1 -1
- package/dist/interfaces/rest/router.js +8 -4
- package/dist/interfaces/rest/routes/admin/compensation-failures.d.ts.map +1 -1
- package/dist/interfaces/rest/routes/admin/compensation-failures.js +7 -5
- package/dist/interfaces/rest/routes/admin/custom-field-proposals.js +2 -2
- package/dist/interfaces/rest/routes/admin/permissions.d.ts +1 -1
- package/dist/interfaces/rest/routes/admin/permissions.d.ts.map +1 -1
- package/dist/interfaces/rest/routes/admin/permissions.js +2 -0
- package/dist/interfaces/rest/routes/admin/staff.d.ts.map +1 -1
- package/dist/interfaces/rest/routes/admin/staff.js +126 -66
- package/dist/interfaces/rest/routes/admin-jobs.js +3 -3
- package/dist/interfaces/rest/routes/audit.js +3 -3
- package/dist/interfaces/rest/routes/carts.d.ts.map +1 -1
- package/dist/interfaces/rest/routes/carts.js +13 -10
- package/dist/interfaces/rest/routes/catalog.d.ts.map +1 -1
- package/dist/interfaces/rest/routes/catalog.js +10 -2
- package/dist/interfaces/rest/routes/checkout.d.ts.map +1 -1
- package/dist/interfaces/rest/routes/checkout.js +30 -35
- package/dist/interfaces/rest/routes/documents.d.ts.map +1 -1
- package/dist/interfaces/rest/routes/documents.js +10 -5
- package/dist/interfaces/rest/routes/inventory.d.ts.map +1 -1
- package/dist/interfaces/rest/routes/inventory.js +2 -0
- package/dist/interfaces/rest/routes/media.d.ts.map +1 -1
- package/dist/interfaces/rest/routes/media.js +7 -3
- package/dist/interfaces/rest/routes/orders.d.ts.map +1 -1
- package/dist/interfaces/rest/routes/orders.js +26 -8
- package/dist/interfaces/rest/routes/pricing.js +2 -2
- package/dist/interfaces/rest/routes/promotions.d.ts.map +1 -1
- package/dist/interfaces/rest/routes/promotions.js +13 -10
- package/dist/interfaces/rest/routes/search.d.ts.map +1 -1
- package/dist/interfaces/rest/routes/search.js +2 -1
- package/dist/interfaces/rest/schemas/carts.d.ts +7 -0
- package/dist/interfaces/rest/schemas/carts.d.ts.map +1 -1
- package/dist/interfaces/rest/schemas/customer-portal.d.ts +35 -0
- package/dist/interfaces/rest/schemas/customer-portal.d.ts.map +1 -1
- package/dist/interfaces/rest/schemas/orders.d.ts +204 -0
- package/dist/interfaces/rest/schemas/orders.d.ts.map +1 -1
- package/dist/interfaces/rest/schemas/responses.d.ts +71 -0
- package/dist/interfaces/rest/schemas/responses.d.ts.map +1 -1
- package/dist/interfaces/rest/utils.d.ts +15 -0
- package/dist/interfaces/rest/utils.d.ts.map +1 -1
- package/dist/interfaces/rest/utils.js +45 -2
- package/dist/kernel/compensation/executor.js +2 -2
- package/dist/kernel/database/plugin-types.d.ts +11 -0
- package/dist/kernel/database/plugin-types.d.ts.map +1 -1
- package/dist/kernel/database/scoped-db.d.ts +12 -0
- package/dist/kernel/database/scoped-db.d.ts.map +1 -1
- package/dist/kernel/database/scoped-db.js +12 -0
- package/dist/kernel/database/tx-context.d.ts +8 -0
- package/dist/kernel/database/tx-context.d.ts.map +1 -1
- package/dist/kernel/database/tx-context.js +13 -0
- package/dist/kernel/hooks/create-context.d.ts +2 -0
- package/dist/kernel/hooks/create-context.d.ts.map +1 -1
- package/dist/kernel/hooks/create-context.js +1 -0
- package/dist/kernel/hooks/types.d.ts +2 -0
- package/dist/kernel/hooks/types.d.ts.map +1 -1
- package/dist/kernel/plugin/manifest.d.ts.map +1 -1
- package/dist/kernel/plugin/manifest.js +9 -5
- package/dist/modules/analytics/service.js +2 -2
- package/dist/modules/audit/service.js +3 -3
- package/dist/modules/cart/repository/index.d.ts +13 -7
- package/dist/modules/cart/repository/index.d.ts.map +1 -1
- package/dist/modules/cart/repository/index.js +16 -10
- package/dist/modules/cart/schema.d.ts +17 -0
- package/dist/modules/cart/schema.d.ts.map +1 -1
- package/dist/modules/cart/schema.js +4 -0
- package/dist/modules/cart/service.d.ts +22 -9
- package/dist/modules/cart/service.d.ts.map +1 -1
- package/dist/modules/cart/service.js +57 -33
- package/dist/modules/catalog/brand-service.js +7 -7
- package/dist/modules/catalog/category-service.js +7 -7
- package/dist/modules/catalog/entity-service.d.ts +14 -7
- package/dist/modules/catalog/entity-service.d.ts.map +1 -1
- package/dist/modules/catalog/entity-service.js +109 -31
- package/dist/modules/catalog/read-policy.d.ts +9 -0
- package/dist/modules/catalog/read-policy.d.ts.map +1 -0
- package/dist/modules/catalog/read-policy.js +8 -0
- package/dist/modules/catalog/repository/index.d.ts +3 -2
- package/dist/modules/catalog/repository/index.d.ts.map +1 -1
- package/dist/modules/catalog/repository/index.js +14 -4
- package/dist/modules/catalog/service.d.ts +15 -13
- package/dist/modules/catalog/service.d.ts.map +1 -1
- package/dist/modules/catalog/service.js +25 -18
- package/dist/modules/customers/module.d.ts.map +1 -1
- package/dist/modules/customers/module.js +1 -0
- package/dist/modules/customers/service.d.ts +2 -0
- package/dist/modules/customers/service.d.ts.map +1 -1
- package/dist/modules/customers/service.js +21 -20
- package/dist/modules/documents/module.d.ts.map +1 -1
- package/dist/modules/documents/module.js +1 -0
- package/dist/modules/documents/service.d.ts +18 -4
- package/dist/modules/documents/service.d.ts.map +1 -1
- package/dist/modules/documents/service.js +54 -16
- package/dist/modules/fulfillment/module.d.ts.map +1 -1
- package/dist/modules/fulfillment/module.js +1 -0
- package/dist/modules/fulfillment/service.d.ts +2 -0
- package/dist/modules/fulfillment/service.d.ts.map +1 -1
- package/dist/modules/fulfillment/service.js +15 -6
- package/dist/modules/inventory/service.d.ts.map +1 -1
- package/dist/modules/inventory/service.js +15 -14
- package/dist/modules/media/service.js +6 -6
- package/dist/modules/orders/guest-access.d.ts +27 -0
- package/dist/modules/orders/guest-access.d.ts.map +1 -0
- package/dist/modules/orders/guest-access.js +46 -0
- package/dist/modules/orders/module.d.ts.map +1 -1
- package/dist/modules/orders/module.js +1 -0
- package/dist/modules/orders/quote.d.ts.map +1 -1
- package/dist/modules/orders/quote.js +1 -0
- package/dist/modules/orders/repository/index.d.ts +1 -1
- package/dist/modules/orders/repository/index.d.ts.map +1 -1
- package/dist/modules/orders/repository/index.js +2 -2
- package/dist/modules/orders/schema.d.ts +17 -0
- package/dist/modules/orders/schema.d.ts.map +1 -1
- package/dist/modules/orders/schema.js +5 -3
- package/dist/modules/orders/service.d.ts +16 -7
- package/dist/modules/orders/service.d.ts.map +1 -1
- package/dist/modules/orders/service.js +184 -55
- package/dist/modules/pricing/module.d.ts.map +1 -1
- package/dist/modules/pricing/module.js +1 -0
- package/dist/modules/pricing/service.d.ts +2 -0
- package/dist/modules/pricing/service.d.ts.map +1 -1
- package/dist/modules/pricing/service.js +12 -11
- package/dist/modules/promotions/module.d.ts.map +1 -1
- package/dist/modules/promotions/module.js +1 -0
- package/dist/modules/promotions/service.d.ts +3 -0
- package/dist/modules/promotions/service.d.ts.map +1 -1
- package/dist/modules/promotions/service.js +19 -11
- package/dist/modules/search/adapter.d.ts +11 -0
- package/dist/modules/search/adapter.d.ts.map +1 -1
- package/dist/modules/search/module.d.ts.map +1 -1
- package/dist/modules/search/module.js +4 -1
- package/dist/modules/search/service.d.ts +1 -0
- package/dist/modules/search/service.d.ts.map +1 -1
- package/dist/modules/search/service.js +57 -6
- package/dist/modules/settings/module.d.ts.map +1 -1
- package/dist/modules/settings/module.js +1 -0
- package/dist/modules/settings/service.d.ts +3 -0
- package/dist/modules/settings/service.d.ts.map +1 -1
- package/dist/modules/settings/service.js +6 -4
- package/dist/modules/shipping/service.js +9 -9
- package/dist/modules/tax/module.d.ts.map +1 -1
- package/dist/modules/tax/module.js +1 -0
- package/dist/modules/tax/service.d.ts +3 -0
- package/dist/modules/tax/service.d.ts.map +1 -1
- package/dist/modules/tax/service.js +11 -9
- package/dist/modules/webhooks/hook.js +2 -2
- package/dist/modules/webhooks/module.d.ts.map +1 -1
- package/dist/modules/webhooks/module.js +1 -0
- package/dist/modules/webhooks/service.d.ts +2 -0
- package/dist/modules/webhooks/service.d.ts.map +1 -1
- package/dist/modules/webhooks/service.js +4 -4
- package/dist/runtime/server.d.ts.map +1 -1
- package/dist/runtime/server.js +28 -2
- package/dist/test-utils/create-pglite-adapter.d.ts.map +1 -1
- package/dist/test-utils/create-pglite-adapter.js +35 -13
- package/dist/test-utils/create-test-config.d.ts.map +1 -1
- package/dist/test-utils/create-test-config.js +4 -0
- package/dist/test-utils/rest-api-test-utils.d.ts.map +1 -1
- package/dist/test-utils/rest-api-test-utils.js +2 -0
- package/dist/test-utils/test-actors.js +2 -2
- package/dist/utils/id.d.ts +2 -0
- package/dist/utils/id.d.ts.map +1 -1
- package/dist/utils/id.js +11 -0
- package/package.json +6 -5
- package/src/auth/actor.ts +110 -0
- package/src/auth/auth-schema-guard.ts +143 -0
- package/src/auth/auth-schema.ts +50 -19
- package/src/auth/middleware.ts +25 -101
- package/src/auth/org.ts +15 -3
- package/src/auth/organization-guard.ts +158 -0
- package/src/auth/permissions.ts +31 -8
- package/src/auth/role-authority.ts +107 -0
- package/src/auth/setup.ts +19 -1
- package/src/auth/strict-org-resolution.ts +7 -1
- package/src/auth/types.ts +1 -1
- package/src/config/defaults.ts +2 -0
- package/src/config/types.ts +31 -4
- package/src/hooks/checkout.ts +19 -12
- package/src/index.ts +21 -6
- package/src/interfaces/rest/audit-middleware.ts +2 -0
- package/src/interfaces/rest/customer-portal.ts +31 -24
- package/src/interfaces/rest/route-coverage.ts +103 -0
- package/src/interfaces/rest/router.ts +16 -10
- package/src/interfaces/rest/routes/admin/compensation-failures.ts +8 -5
- package/src/interfaces/rest/routes/admin/custom-field-proposals.ts +2 -2
- package/src/interfaces/rest/routes/admin/permissions.ts +3 -1
- package/src/interfaces/rest/routes/admin/staff.ts +161 -72
- package/src/interfaces/rest/routes/admin-jobs.ts +3 -3
- package/src/interfaces/rest/routes/audit.ts +3 -3
- package/src/interfaces/rest/routes/carts.ts +17 -7
- package/src/interfaces/rest/routes/catalog.ts +11 -0
- package/src/interfaces/rest/routes/checkout.ts +41 -38
- package/src/interfaces/rest/routes/documents.ts +15 -1
- package/src/interfaces/rest/routes/inventory.ts +2 -0
- package/src/interfaces/rest/routes/media.ts +8 -3
- package/src/interfaces/rest/routes/orders.ts +27 -8
- package/src/interfaces/rest/routes/pricing.ts +2 -2
- package/src/interfaces/rest/routes/promotions.ts +12 -9
- package/src/interfaces/rest/routes/search.ts +3 -1
- package/src/interfaces/rest/utils.ts +61 -2
- package/src/kernel/compensation/executor.ts +2 -2
- package/src/kernel/database/plugin-types.ts +11 -0
- package/src/kernel/database/scoped-db.ts +12 -0
- package/src/kernel/database/tx-context.ts +25 -0
- package/src/kernel/hooks/create-context.ts +3 -0
- package/src/kernel/hooks/types.ts +2 -0
- package/src/kernel/plugin/manifest.ts +9 -5
- package/src/modules/analytics/service.ts +2 -2
- package/src/modules/audit/service.ts +3 -3
- package/src/modules/cart/repository/index.ts +22 -8
- package/src/modules/cart/schema.ts +4 -0
- package/src/modules/cart/service.ts +62 -27
- package/src/modules/catalog/brand-service.ts +7 -7
- package/src/modules/catalog/category-service.ts +7 -7
- package/src/modules/catalog/entity-service.ts +117 -35
- package/src/modules/catalog/read-policy.ts +16 -0
- package/src/modules/catalog/repository/index.ts +19 -3
- package/src/modules/catalog/service.ts +46 -31
- package/src/modules/customers/module.ts +2 -0
- package/src/modules/customers/service.ts +23 -22
- package/src/modules/documents/module.ts +2 -0
- package/src/modules/documents/service.ts +74 -11
- package/src/modules/fulfillment/module.ts +2 -0
- package/src/modules/fulfillment/service.ts +17 -6
- package/src/modules/inventory/service.ts +15 -24
- package/src/modules/media/service.ts +6 -6
- package/src/modules/orders/guest-access.ts +55 -0
- package/src/modules/orders/module.ts +1 -0
- package/src/modules/orders/quote.ts +1 -0
- package/src/modules/orders/repository/index.ts +2 -0
- package/src/modules/orders/schema.ts +5 -3
- package/src/modules/orders/service.ts +302 -52
- package/src/modules/pricing/module.ts +2 -0
- package/src/modules/pricing/service.ts +14 -16
- package/src/modules/promotions/module.ts +2 -0
- package/src/modules/promotions/service.ts +25 -16
- package/src/modules/search/adapter.ts +9 -0
- package/src/modules/search/module.ts +5 -1
- package/src/modules/search/service.ts +55 -6
- package/src/modules/settings/module.ts +2 -0
- package/src/modules/settings/service.ts +8 -4
- package/src/modules/shipping/service.ts +9 -9
- package/src/modules/tax/module.ts +1 -0
- package/src/modules/tax/service.ts +13 -9
- package/src/modules/webhooks/hook.ts +2 -2
- package/src/modules/webhooks/module.ts +2 -0
- package/src/modules/webhooks/service.ts +6 -4
- package/src/runtime/server.ts +41 -2
- package/src/test-utils/create-pglite-adapter.ts +34 -12
- package/src/test-utils/create-test-config.ts +10 -6
- package/src/test-utils/rest-api-test-utils.ts +2 -0
- package/src/test-utils/test-actors.ts +2 -2
- package/src/utils/id.ts +20 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { resolveOrgIdForCommerce } from "../../auth/org.js";
|
|
2
2
|
import { assertPermission } from "../../auth/permissions.js";
|
|
3
3
|
import type { Actor } from "../../auth/types.js";
|
|
4
4
|
import {
|
|
@@ -21,8 +21,12 @@ import type {
|
|
|
21
21
|
import { Err, Ok, type Result } from "../../kernel/result.js";
|
|
22
22
|
import { createLogger } from "../../utils/logger.js";
|
|
23
23
|
import { paginate } from "../../utils/pagination.js";
|
|
24
|
+
import type { JobsAdapter } from "../../kernel/jobs/adapter.js";
|
|
24
25
|
import type { PluginDb } from "../../kernel/database/plugin-types.js";
|
|
25
|
-
import type { TxContext } from "../../kernel/database/tx-context.js";
|
|
26
|
+
import type { CatalogWriteContext, TxContext } from "../../kernel/database/tx-context.js";
|
|
27
|
+
import { isWriteContextTransactional } from "../../kernel/database/tx-context.js";
|
|
28
|
+
import { canReadUnpublishedCatalog, isCatalogEntityVisible } from "./read-policy.js";
|
|
29
|
+
import { isValidFieldPath } from "./ownership.js";
|
|
26
30
|
import type {
|
|
27
31
|
CatalogServiceDeps,
|
|
28
32
|
CatalogEntityHydrated,
|
|
@@ -52,6 +56,26 @@ function hookDatabaseArg(database: CatalogServiceDeps["database"]): { database:
|
|
|
52
56
|
return { database: { db: database.db as PluginDb } };
|
|
53
57
|
}
|
|
54
58
|
|
|
59
|
+
const attributeFields = ["title", "subtitle", "description", "richDescription", "seoTitle", "seoDescription"] as const;
|
|
60
|
+
|
|
61
|
+
function catalogHookContext(
|
|
62
|
+
deps: CatalogServiceDeps,
|
|
63
|
+
actor: Actor | null,
|
|
64
|
+
ctx: CatalogWriteContext | undefined,
|
|
65
|
+
operation: string,
|
|
66
|
+
): HookContext {
|
|
67
|
+
return createHookContext({
|
|
68
|
+
actor,
|
|
69
|
+
tx: isWriteContextTransactional(ctx) ? ctx.tx : null,
|
|
70
|
+
logger: createLogger(`catalog.${operation}`),
|
|
71
|
+
services: deps.services,
|
|
72
|
+
...(deps.services.jobs ? { jobs: deps.services.jobs as JobsAdapter } : {}),
|
|
73
|
+
context: { moduleName: "catalog", ...(ctx?.hookContext ?? {}) },
|
|
74
|
+
...hookDatabaseArg(deps.database),
|
|
75
|
+
commerceConfig: deps.config,
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
55
79
|
type CatalogCreateBeforeHook = BeforeHook<CreateEntityInput>;
|
|
56
80
|
type CatalogCreateAfterHook = AfterHook<SellableEntity>;
|
|
57
81
|
type CatalogUpdateBeforeHook = BeforeHook<UpdateEntityInput>;
|
|
@@ -100,7 +124,7 @@ export class EntityService {
|
|
|
100
124
|
|
|
101
125
|
private assertSameOrg(resource: { organizationId?: string | null } | undefined, actor: Actor | null): void {
|
|
102
126
|
if (!resource) return;
|
|
103
|
-
const orgId =
|
|
127
|
+
const orgId = resolveOrgIdForCommerce(actor, this.deps.config);
|
|
104
128
|
if (resource.organizationId && resource.organizationId !== orgId) {
|
|
105
129
|
throw new CommerceNotFoundError("Entity not found.");
|
|
106
130
|
}
|
|
@@ -256,12 +280,12 @@ export class EntityService {
|
|
|
256
280
|
|
|
257
281
|
async create(input: CreateEntityInput, actor: Actor | null, ctx?: TxContext): Promise<Result<CatalogEntityHydrated>> { try {
|
|
258
282
|
assertPermission(actor, "catalog:create");
|
|
259
|
-
const orgId =
|
|
283
|
+
const orgId = resolveOrgIdForCommerce(actor, this.deps.config);
|
|
260
284
|
const existingBySlug = await this.repo.findEntityBySlug(orgId, input.slug, ctx);
|
|
261
285
|
if (existingBySlug) return Err(new CommerceConflictError(`Entity with slug ${input.slug} already exists.`));
|
|
262
286
|
const beforeHooks = this.deps.hooks.resolve("catalog.beforeCreate") as CatalogCreateBeforeHook[];
|
|
263
287
|
const afterHooks = this.deps.hooks.resolve("catalog.afterCreate") as CatalogCreateAfterHook[];
|
|
264
|
-
const context: HookContext = createHookContext({ actor, tx: ctx?.tx ?? null, logger: createLogger("catalog.create"), services: this.deps.services, context: { moduleName: "catalog" }, ...hookDatabaseArg(this.deps.database) });
|
|
288
|
+
const context: HookContext = createHookContext({ actor, tx: ctx?.tx ?? null, logger: createLogger("catalog.create"), services: this.deps.services, context: { moduleName: "catalog" }, ...hookDatabaseArg(this.deps.database), commerceConfig: this.deps.config });
|
|
265
289
|
const processedInput = await runBeforeHooks(beforeHooks, input, "create", context);
|
|
266
290
|
if (processedInput.sourceStoreId != null) assertPermission(actor, "catalog:sync");
|
|
267
291
|
const customFieldsResult = await this.validateCustomFields(processedInput.type, processedInput.customFields, actor, ctx);
|
|
@@ -278,23 +302,24 @@ export class EntityService {
|
|
|
278
302
|
return Ok(hydrated, hookReport.hasErrors ? { hookErrors: hookReport.errors } : undefined);
|
|
279
303
|
} catch (error) { return Err(toCommerceError(error)); } }
|
|
280
304
|
|
|
281
|
-
async update(id: string, input: UpdateEntityInput, actor: Actor | null, ctx?:
|
|
305
|
+
async update(id: string, input: UpdateEntityInput, actor: Actor | null, ctx?: CatalogWriteContext): Promise<Result<CatalogEntityHydrated>> { try {
|
|
306
|
+
const txCtx = isWriteContextTransactional(ctx) ? ctx : undefined;
|
|
282
307
|
assertPermission(actor, "catalog:update");
|
|
283
|
-
const existing = await this.repo.findEntityById(id,
|
|
308
|
+
const existing = await this.repo.findEntityById(id, txCtx);
|
|
284
309
|
if (!existing) return Err(new CommerceNotFoundError("Entity not found."));
|
|
285
310
|
this.assertSameOrg(existing, actor);
|
|
286
311
|
this.assertEntityWritable(existing, actor);
|
|
287
312
|
const beforeHooks = this.deps.hooks.resolve("catalog.beforeUpdate") as CatalogUpdateBeforeHook[];
|
|
288
313
|
const afterHooks = this.deps.hooks.resolve("catalog.afterUpdate") as CatalogUpdateAfterHook[];
|
|
289
|
-
const context: HookContext =
|
|
314
|
+
const context: HookContext = catalogHookContext(this.deps, actor, ctx, "update");
|
|
290
315
|
const processed = await runBeforeHooks(beforeHooks, input, "update", context);
|
|
291
|
-
const customFieldsResult = await this.validateCustomFields(existing.type, processed.customFields, actor,
|
|
316
|
+
const customFieldsResult = await this.validateCustomFields(existing.type, processed.customFields, actor, txCtx);
|
|
292
317
|
if (!customFieldsResult.ok) return customFieldsResult;
|
|
293
|
-
const updated = await this.repo.updateEntity(id, { ...(processed.slug !== undefined ? { slug: processed.slug } : {}), ...(processed.status !== undefined ? { status: processed.status as SellableEntity["status"] } : {}), ...(processed.taxClass !== undefined ? { taxClass: processed.taxClass } : {}), ...(processed.metadata !== undefined ? { metadata: processed.metadata } : {}), ...(processed.isVisible !== undefined ? { isVisible: processed.isVisible } : {}) },
|
|
318
|
+
const updated = await this.repo.updateEntity(id, { ...(processed.slug !== undefined ? { slug: processed.slug } : {}), ...(processed.status !== undefined ? { status: processed.status as SellableEntity["status"] } : {}), ...(processed.taxClass !== undefined ? { taxClass: processed.taxClass } : {}), ...(processed.metadata !== undefined ? { metadata: processed.metadata } : {}), ...(processed.isVisible !== undefined ? { isVisible: processed.isVisible } : {}) }, txCtx);
|
|
294
319
|
if (!updated) return Err(new CommerceNotFoundError("Entity not found."));
|
|
295
|
-
await this.writeCustomFields(existing.id, customFieldsResult.value,
|
|
320
|
+
await this.writeCustomFields(existing.id, customFieldsResult.value, txCtx, true);
|
|
296
321
|
const hookReport = await runAfterHooks(afterHooks, existing, updated, "update", context);
|
|
297
|
-
const hydrated = await this.hydrateEntity(updated, undefined,
|
|
322
|
+
const hydrated = await this.hydrateEntity(updated, undefined, txCtx);
|
|
298
323
|
return Ok(hydrated, hookReport.hasErrors ? { hookErrors: hookReport.errors } : undefined);
|
|
299
324
|
} catch (error) { return Err(toCommerceError(error)); } }
|
|
300
325
|
|
|
@@ -314,24 +339,27 @@ export class EntityService {
|
|
|
314
339
|
return Ok(undefined);
|
|
315
340
|
} catch (error) { return Err(toCommerceError(error)); } }
|
|
316
341
|
|
|
317
|
-
async getById(id: string, options
|
|
318
|
-
const
|
|
342
|
+
async getById(id: string, options: GetOptions | undefined, actor: Actor | null, ctx?: TxContext): Promise<Result<CatalogEntityHydrated>> {
|
|
343
|
+
const resolvedActor = actor ?? ctx?.actor ?? null;
|
|
344
|
+
const orgId = resolveOrgIdForCommerce(resolvedActor, this.deps.config);
|
|
345
|
+
const context: HookContext = createHookContext({ actor: resolvedActor, tx: ctx?.tx ?? null, logger: createLogger("catalog.read"), services: this.deps.services, context: { moduleName: "catalog" }, ...hookDatabaseArg(this.deps.database), commerceConfig: this.deps.config });
|
|
319
346
|
const globalBeforeHooks = this.deps.hooks.resolve("catalog.beforeRead") as CatalogReadBeforeHook[];
|
|
320
347
|
const globalAfterHooks = this.deps.hooks.resolve("catalog.afterRead") as CatalogReadAfterHook[];
|
|
321
348
|
let processed = await runBeforeHooks(globalBeforeHooks, { id, ...(options !== undefined ? { options } : {}) }, "read", context);
|
|
322
|
-
let entity = await this.repo.findEntityById(processed.id ?? id, ctx);
|
|
349
|
+
let entity = await this.repo.findEntityById(processed.id ?? id, ctx, orgId);
|
|
323
350
|
if (!entity) return Err(new CommerceNotFoundError("Entity not found."));
|
|
351
|
+
if (!isCatalogEntityVisible(entity, resolvedActor)) return Err(new CommerceNotFoundError("Entity not found."));
|
|
324
352
|
const entityBeforeHooks = this.deps.hooks.resolve(`catalog.${entity.type}.beforeRead`) as CatalogReadBeforeHook[];
|
|
325
353
|
if (entityBeforeHooks.length > 0) {
|
|
326
354
|
processed = await runBeforeHooks(entityBeforeHooks, { ...processed, id: entity.id }, "read", context);
|
|
327
355
|
const resolvedId = processed.id ?? entity.id;
|
|
328
356
|
if (resolvedId !== entity.id) {
|
|
329
|
-
const refetched = await this.repo.findEntityById(resolvedId, ctx);
|
|
357
|
+
const refetched = await this.repo.findEntityById(resolvedId, ctx, orgId);
|
|
330
358
|
if (!refetched) return Err(new CommerceNotFoundError("Entity not found."));
|
|
359
|
+
if (!isCatalogEntityVisible(refetched, resolvedActor)) return Err(new CommerceNotFoundError("Entity not found."));
|
|
331
360
|
entity = refetched;
|
|
332
361
|
}
|
|
333
362
|
}
|
|
334
|
-
const resolvedActor = actor ?? ctx?.actor ?? null;
|
|
335
363
|
try {
|
|
336
364
|
this.assertSameOrg(entity, resolvedActor);
|
|
337
365
|
} catch (error) {
|
|
@@ -343,28 +371,48 @@ export class EntityService {
|
|
|
343
371
|
return Ok(result, report.hasErrors ? { hookErrors: report.errors } : undefined);
|
|
344
372
|
}
|
|
345
373
|
|
|
346
|
-
|
|
347
|
-
|
|
374
|
+
/**
|
|
375
|
+
* Reads an entity for a trusted server pipeline after the request boundary
|
|
376
|
+
* has already authorized the caller. Organization scope is explicit; the
|
|
377
|
+
* storefront visibility policy is intentionally not applied, so an internal
|
|
378
|
+
* invariant check never depends on the caller's catalog permissions.
|
|
379
|
+
*/
|
|
380
|
+
async getByIdForInternalUse(
|
|
381
|
+
id: string,
|
|
382
|
+
organizationId: string,
|
|
383
|
+
options?: GetOptions,
|
|
384
|
+
ctx?: TxContext,
|
|
385
|
+
): Promise<Result<CatalogEntityHydrated>> {
|
|
386
|
+
const entity = await this.repo.findEntityById(id, ctx, organizationId);
|
|
387
|
+
if (!entity) return Err(new CommerceNotFoundError("Entity not found."));
|
|
388
|
+
return Ok(await this.hydrateEntity(entity, options, ctx));
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
async getBySlug(slug: string, options: GetOptions | undefined, actor: Actor | null, ctx?: TxContext): Promise<Result<CatalogEntityHydrated>> {
|
|
392
|
+
const context: HookContext = createHookContext({ actor: actor ?? null, tx: ctx?.tx ?? null, logger: createLogger("catalog.read"), services: this.deps.services, context: { moduleName: "catalog" }, ...hookDatabaseArg(this.deps.database), commerceConfig: this.deps.config });
|
|
348
393
|
const globalBeforeHooks = this.deps.hooks.resolve("catalog.beforeRead") as CatalogReadBeforeHook[];
|
|
349
394
|
const globalAfterHooks = this.deps.hooks.resolve("catalog.afterRead") as CatalogReadAfterHook[];
|
|
350
395
|
let processed = await runBeforeHooks(globalBeforeHooks, { slug, ...(options !== undefined ? { options } : {}) }, "read", context);
|
|
351
396
|
let resolvedSlug = processed.slug ?? slug;
|
|
352
|
-
const orgId =
|
|
397
|
+
const orgId = resolveOrgIdForCommerce(actor ?? null, this.deps.config);
|
|
353
398
|
let entity = await this.repo.findEntityBySlug(orgId, resolvedSlug, ctx);
|
|
354
399
|
if (!entity) return Err(new CommerceNotFoundError("Entity not found."));
|
|
400
|
+
if (!isCatalogEntityVisible(entity, actor ?? null)) return Err(new CommerceNotFoundError("Entity not found."));
|
|
355
401
|
const entityBeforeHooks = this.deps.hooks.resolve(`catalog.${entity.type}.beforeRead`) as CatalogReadBeforeHook[];
|
|
356
402
|
if (entityBeforeHooks.length > 0) {
|
|
357
403
|
processed = await runBeforeHooks(entityBeforeHooks, { ...processed, id: entity.id }, "read", context);
|
|
358
404
|
const nextId = processed.id ?? entity.id;
|
|
359
405
|
const nextSlug = processed.slug ?? resolvedSlug;
|
|
360
406
|
if (nextId !== entity.id) {
|
|
361
|
-
const refetched = await this.repo.findEntityById(nextId, ctx);
|
|
407
|
+
const refetched = await this.repo.findEntityById(nextId, ctx, orgId);
|
|
362
408
|
if (!refetched) return Err(new CommerceNotFoundError("Entity not found."));
|
|
409
|
+
if (!isCatalogEntityVisible(refetched, actor ?? null)) return Err(new CommerceNotFoundError("Entity not found."));
|
|
363
410
|
entity = refetched;
|
|
364
411
|
resolvedSlug = nextSlug;
|
|
365
412
|
} else if (nextSlug !== resolvedSlug) {
|
|
366
413
|
const refetched = await this.repo.findEntityBySlug(orgId, nextSlug, ctx);
|
|
367
414
|
if (!refetched) return Err(new CommerceNotFoundError("Entity not found."));
|
|
415
|
+
if (!isCatalogEntityVisible(refetched, actor ?? null)) return Err(new CommerceNotFoundError("Entity not found."));
|
|
368
416
|
entity = refetched;
|
|
369
417
|
}
|
|
370
418
|
}
|
|
@@ -374,9 +422,9 @@ export class EntityService {
|
|
|
374
422
|
return Ok(result, report.hasErrors ? { hookErrors: report.errors } : undefined);
|
|
375
423
|
}
|
|
376
424
|
|
|
377
|
-
async list(params: ListParams, actor
|
|
425
|
+
async list(params: ListParams, actor: Actor | null, ctx?: TxContext): Promise<Result<CatalogListResult>> {
|
|
378
426
|
const resolvedActor = actor ?? ctx?.actor ?? null;
|
|
379
|
-
const context: HookContext = createHookContext({ actor: resolvedActor, tx: ctx?.tx ?? null, logger: createLogger("catalog.list"), services: this.deps.services, context: { moduleName: "catalog" }, ...hookDatabaseArg(this.deps.database) });
|
|
427
|
+
const context: HookContext = createHookContext({ actor: resolvedActor, tx: ctx?.tx ?? null, logger: createLogger("catalog.list"), services: this.deps.services, context: { moduleName: "catalog" }, ...hookDatabaseArg(this.deps.database), commerceConfig: this.deps.config });
|
|
380
428
|
const globalBeforeHooks = this.deps.hooks.resolve("catalog.beforeList") as CatalogListBeforeHook[];
|
|
381
429
|
const globalAfterHooks = this.deps.hooks.resolve("catalog.afterList") as CatalogListAfterHook[];
|
|
382
430
|
let processed = await runBeforeHooks(globalBeforeHooks, params, "list", context);
|
|
@@ -385,20 +433,28 @@ export class EntityService {
|
|
|
385
433
|
const entityBeforeHooks = this.deps.hooks.resolve(`catalog.${initialTypeFilter}.beforeList`) as CatalogListBeforeHook[];
|
|
386
434
|
if (entityBeforeHooks.length > 0) processed = await runBeforeHooks(entityBeforeHooks, processed, "list", context);
|
|
387
435
|
}
|
|
388
|
-
const listOrgId =
|
|
389
|
-
|
|
436
|
+
const listOrgId = resolveOrgIdForCommerce(resolvedActor, this.deps.config);
|
|
437
|
+
const canReadUnpublished = canReadUnpublishedCatalog(resolvedActor);
|
|
438
|
+
let entities = await this.repo.findEntities(listOrgId, {
|
|
439
|
+
...(processed.filter?.type ? { type: processed.filter.type } : {}),
|
|
440
|
+
...(canReadUnpublished
|
|
441
|
+
? (processed.filter?.status ? { status: processed.filter.status } : {})
|
|
442
|
+
: { status: "active", isVisible: true }),
|
|
443
|
+
}, ctx);
|
|
444
|
+
const isUUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
390
445
|
if (processed.filter?.category) {
|
|
391
446
|
const catInput = processed.filter.category;
|
|
392
|
-
const isUUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
393
447
|
let category = await this.repo.findCategoryBySlug(listOrgId, catInput, ctx);
|
|
394
|
-
if (!category && isUUID.test(catInput)) category = await this.repo.findCategoryById(catInput, ctx);
|
|
448
|
+
if (!category && isUUID.test(catInput)) category = await this.repo.findCategoryById(catInput, ctx, listOrgId);
|
|
395
449
|
if (!category) return Err(new CommerceValidationError(`Category not found: "${catInput}".`));
|
|
396
450
|
const entityIds = await this.repo.findEntitiesByCategory(category.id, ctx);
|
|
397
451
|
entities = entities.filter((e) => new Set(entityIds).has(e.id));
|
|
398
452
|
}
|
|
399
453
|
if (processed.filter?.brand) {
|
|
400
454
|
let brand = await this.repo.findBrandBySlug(listOrgId, processed.filter.brand, ctx);
|
|
401
|
-
if (!brand
|
|
455
|
+
if (!brand && isUUID.test(processed.filter.brand)) {
|
|
456
|
+
brand = await this.repo.findBrandById(processed.filter.brand, ctx, listOrgId);
|
|
457
|
+
}
|
|
402
458
|
if (brand) {
|
|
403
459
|
const brandEntityIds: string[] = [];
|
|
404
460
|
for (const entity of entities) {
|
|
@@ -454,19 +510,45 @@ export class EntityService {
|
|
|
454
510
|
archive(id: string, actor: Actor | null, ctx?: TxContext): Promise<Result<CatalogEntityHydrated>> { return this.changeStatus(id, "archived", actor, ctx); }
|
|
455
511
|
discontinue(id: string, actor: Actor | null, ctx?: TxContext): Promise<Result<CatalogEntityHydrated>> { return this.changeStatus(id, "discontinued", actor, ctx); }
|
|
456
512
|
|
|
457
|
-
async setAttributes(entityId: string, locale: string, attrs: SetAttributesInput, actor: Actor | null, ctx?:
|
|
513
|
+
async setAttributes(entityId: string, locale: string, attrs: SetAttributesInput, actor: Actor | null, ctx?: CatalogWriteContext): Promise<Result<void>> {
|
|
458
514
|
try { assertPermission(actor, "catalog:update"); } catch (error) { return Err(toCommerceError(error)); }
|
|
459
|
-
const
|
|
515
|
+
const txCtx = isWriteContextTransactional(ctx) ? ctx : undefined;
|
|
516
|
+
const entity = await this.repo.findEntityById(entityId, txCtx);
|
|
460
517
|
if (!entity) return Err(new CommerceNotFoundError("Entity not found."));
|
|
461
518
|
try { this.assertSameOrg(entity, actor); } catch (error) { return Err(toCommerceError(error)); }
|
|
462
|
-
await this.repo.
|
|
519
|
+
const beforeAttr = await this.repo.findAttributeByLocale(entityId, locale, txCtx);
|
|
520
|
+
const changedFieldPaths: string[] = [];
|
|
521
|
+
for (const field of attributeFields) {
|
|
522
|
+
if (attrs[field] === undefined) continue;
|
|
523
|
+
const before = beforeAttr?.[field] ?? null;
|
|
524
|
+
const after = attrs[field] ?? null;
|
|
525
|
+
if (before !== after) {
|
|
526
|
+
const path = `attributes.${locale}.${field}`;
|
|
527
|
+
if (isValidFieldPath(path)) changedFieldPaths.push(path);
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
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);
|
|
531
|
+
if (changedFieldPaths.length > 0) {
|
|
532
|
+
const afterHooks = this.deps.hooks.resolve("catalog.afterUpdate") as CatalogUpdateAfterHook[];
|
|
533
|
+
const context = catalogHookContext(this.deps, actor, ctx, "update");
|
|
534
|
+
context.context.changedFieldPaths = changedFieldPaths;
|
|
535
|
+
await runAfterHooks(afterHooks, entity, entity, "update", context);
|
|
536
|
+
}
|
|
463
537
|
return Ok(undefined);
|
|
464
538
|
}
|
|
465
539
|
|
|
466
|
-
async getAttributes(entityId: string, locale: string, ctx?: TxContext): Promise<Result<SellableAttribute>> {
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
540
|
+
async getAttributes(entityId: string, locale: string, actor: Actor | null, ctx?: TxContext): Promise<Result<SellableAttribute>> {
|
|
541
|
+
try {
|
|
542
|
+
assertPermission(actor, "catalog:read");
|
|
543
|
+
const entity = await this.repo.findEntityById(entityId, ctx, resolveOrgIdForCommerce(actor, this.deps.config));
|
|
544
|
+
if (!entity) return Err(new CommerceNotFoundError("Entity not found."));
|
|
545
|
+
if (!isCatalogEntityVisible(entity, actor)) return Err(new CommerceNotFoundError("Entity not found."));
|
|
546
|
+
const attr = await this.repo.findAttributeByLocale(entity.id, locale, ctx);
|
|
547
|
+
if (!attr) return Err(new CommerceNotFoundError(`Attributes for locale ${locale} not found.`));
|
|
548
|
+
return Ok(attr);
|
|
549
|
+
} catch (error) {
|
|
550
|
+
return Err(toCommerceError(error));
|
|
551
|
+
}
|
|
470
552
|
}
|
|
471
553
|
|
|
472
554
|
async createOptionType(input: CreateOptionTypeInput, actor: Actor | null, ctx?: TxContext): Promise<Result<OptionType>> {
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { hasPermission } from "../../auth/permissions.js";
|
|
2
|
+
import type { Actor } from "../../auth/types.js";
|
|
3
|
+
|
|
4
|
+
type CatalogVisibility = {
|
|
5
|
+
status: string;
|
|
6
|
+
isVisible: boolean;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export function canReadUnpublishedCatalog(actor: Actor | null): boolean {
|
|
10
|
+
return hasPermission(actor, "catalog:read:unpublished");
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function isCatalogEntityVisible(entity: CatalogVisibility, actor: Actor | null): boolean {
|
|
14
|
+
return canReadUnpublishedCatalog(actor) ||
|
|
15
|
+
(entity.status === "active" && entity.isVisible);
|
|
16
|
+
}
|
|
@@ -124,6 +124,7 @@ export class CatalogRepository {
|
|
|
124
124
|
filter?: {
|
|
125
125
|
type?: string;
|
|
126
126
|
status?: string;
|
|
127
|
+
isVisible?: boolean;
|
|
127
128
|
ids?: string[];
|
|
128
129
|
},
|
|
129
130
|
ctx?: TxContext,
|
|
@@ -139,6 +140,9 @@ export class CatalogRepository {
|
|
|
139
140
|
eq(sellableEntities.status, filter.status as SellableEntity["status"]),
|
|
140
141
|
);
|
|
141
142
|
}
|
|
143
|
+
if (filter?.isVisible !== undefined) {
|
|
144
|
+
conditions.push(eq(sellableEntities.isVisible, filter.isVisible));
|
|
145
|
+
}
|
|
142
146
|
if (filter?.ids && filter.ids.length > 0) {
|
|
143
147
|
conditions.push(inArray(sellableEntities.id, filter.ids));
|
|
144
148
|
}
|
|
@@ -811,12 +815,17 @@ export class CatalogRepository {
|
|
|
811
815
|
async findCategoryById(
|
|
812
816
|
id: string,
|
|
813
817
|
ctx?: TxContext,
|
|
818
|
+
orgId?: string,
|
|
814
819
|
): Promise<Category | undefined> {
|
|
815
820
|
const db = this.getDb(ctx);
|
|
816
821
|
const rows = await db
|
|
817
822
|
.select()
|
|
818
823
|
.from(categories)
|
|
819
|
-
.where(
|
|
824
|
+
.where(
|
|
825
|
+
orgId
|
|
826
|
+
? and(eq(categories.id, id), eq(categories.organizationId, orgId))
|
|
827
|
+
: eq(categories.id, id),
|
|
828
|
+
);
|
|
820
829
|
return rows[0];
|
|
821
830
|
}
|
|
822
831
|
|
|
@@ -963,9 +972,16 @@ export class CatalogRepository {
|
|
|
963
972
|
// Brands
|
|
964
973
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
965
974
|
|
|
966
|
-
async findBrandById(id: string, ctx?: TxContext): Promise<Brand | undefined> {
|
|
975
|
+
async findBrandById(id: string, ctx?: TxContext, orgId?: string): Promise<Brand | undefined> {
|
|
967
976
|
const db = this.getDb(ctx);
|
|
968
|
-
const rows = await db
|
|
977
|
+
const rows = await db
|
|
978
|
+
.select()
|
|
979
|
+
.from(brands)
|
|
980
|
+
.where(
|
|
981
|
+
orgId
|
|
982
|
+
? and(eq(brands.id, id), eq(brands.organizationId, orgId))
|
|
983
|
+
: eq(brands.id, id),
|
|
984
|
+
);
|
|
969
985
|
return rows[0];
|
|
970
986
|
}
|
|
971
987
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Actor } from "../../auth/types.js";
|
|
2
|
-
import {
|
|
2
|
+
import { resolveOrgIdForCommerce } from "../../auth/org.js";
|
|
3
3
|
import { assertPermission } from "../../auth/permissions.js";
|
|
4
4
|
import type { CommerceConfig } from "../../config/types.js";
|
|
5
5
|
import type { EntityFieldDefinition, FieldType } from "../../config/types.js";
|
|
@@ -8,7 +8,7 @@ import { Err, Ok, type Result } from "../../kernel/result.js";
|
|
|
8
8
|
import { CommerceConflictError, CommerceNotFoundError, CommerceValidationError, toCommerceError } from "../../kernel/errors.js";
|
|
9
9
|
import type { Pagination } from "../../utils/pagination.js";
|
|
10
10
|
import type { DatabaseAdapter } from "../../kernel/database/adapter.js";
|
|
11
|
-
import { createTxContext, type TxContext } from "../../kernel/database/tx-context.js";
|
|
11
|
+
import { createTxContext, type CatalogWriteContext, isWriteContextTransactional, resolveWriteContextHookContext, type TxContext } from "../../kernel/database/tx-context.js";
|
|
12
12
|
import {
|
|
13
13
|
CatalogRepository,
|
|
14
14
|
type SellableEntity,
|
|
@@ -159,7 +159,7 @@ export interface CatalogService {
|
|
|
159
159
|
id: string,
|
|
160
160
|
input: UpdateEntityInput,
|
|
161
161
|
actor: Actor | null,
|
|
162
|
-
ctx?:
|
|
162
|
+
ctx?: CatalogWriteContext,
|
|
163
163
|
): Promise<Result<CatalogEntityHydrated>>;
|
|
164
164
|
delete(
|
|
165
165
|
id: string,
|
|
@@ -168,17 +168,23 @@ export interface CatalogService {
|
|
|
168
168
|
): Promise<Result<void>>;
|
|
169
169
|
getById(
|
|
170
170
|
id: string,
|
|
171
|
+
options: GetOptions | undefined,
|
|
172
|
+
actor: Actor | null,
|
|
173
|
+
ctx?: TxContext,
|
|
174
|
+
): Promise<Result<CatalogEntityHydrated>>;
|
|
175
|
+
getByIdForInternalUse(
|
|
176
|
+
id: string,
|
|
177
|
+
organizationId: string,
|
|
171
178
|
options?: GetOptions,
|
|
172
|
-
actor?: Actor | null,
|
|
173
179
|
ctx?: TxContext,
|
|
174
180
|
): Promise<Result<CatalogEntityHydrated>>;
|
|
175
181
|
getBySlug(
|
|
176
182
|
slug: string,
|
|
177
|
-
options
|
|
178
|
-
actor
|
|
183
|
+
options: GetOptions | undefined,
|
|
184
|
+
actor: Actor | null,
|
|
179
185
|
ctx?: TxContext,
|
|
180
186
|
): Promise<Result<CatalogEntityHydrated>>;
|
|
181
|
-
list(params: ListParams, actor
|
|
187
|
+
list(params: ListParams, actor: Actor | null, ctx?: TxContext): Promise<Result<CatalogListResult>>;
|
|
182
188
|
/** Returns undefined when no concrete ownership row applies to the field. */
|
|
183
189
|
resolveFieldOwner(
|
|
184
190
|
entityId: string,
|
|
@@ -234,7 +240,7 @@ export interface CatalogService {
|
|
|
234
240
|
locale: string,
|
|
235
241
|
attrs: SetAttributesInput,
|
|
236
242
|
actor: Actor | null,
|
|
237
|
-
ctx?:
|
|
243
|
+
ctx?: CatalogWriteContext,
|
|
238
244
|
): Promise<Result<void>>;
|
|
239
245
|
approveCustomField(
|
|
240
246
|
entityId: string,
|
|
@@ -253,6 +259,7 @@ export interface CatalogService {
|
|
|
253
259
|
getAttributes(
|
|
254
260
|
entityId: string,
|
|
255
261
|
locale: string,
|
|
262
|
+
actor: Actor | null,
|
|
256
263
|
ctx?: TxContext,
|
|
257
264
|
): Promise<Result<SellableAttribute>>;
|
|
258
265
|
listCategories(
|
|
@@ -470,7 +477,7 @@ export class CatalogServiceImpl implements CatalogService {
|
|
|
470
477
|
): Promise<EntityFieldDefinition[]> {
|
|
471
478
|
const orgId = typeof actorOrOrg === "string"
|
|
472
479
|
? actorOrOrg
|
|
473
|
-
:
|
|
480
|
+
: resolveOrgIdForCommerce(actorOrOrg ?? ctx?.actor ?? null, this.config);
|
|
474
481
|
const codeFields = this.config.entities?.[entityType]?.fields ?? [];
|
|
475
482
|
const merged = new Map(codeFields.map((field) => [field.name, { ...field }]));
|
|
476
483
|
const runtimeFields = await this.repository.findActiveEntityFieldDefinitions(orgId, entityType, ctx);
|
|
@@ -500,7 +507,7 @@ export class CatalogServiceImpl implements CatalogService {
|
|
|
500
507
|
entityType?: string,
|
|
501
508
|
ctx?: TxContext,
|
|
502
509
|
): Promise<Result<EntityFieldDefinitionRecord[]>> {
|
|
503
|
-
const orgId =
|
|
510
|
+
const orgId = resolveOrgIdForCommerce(actor ?? ctx?.actor ?? null, this.config);
|
|
504
511
|
return this.withMutationResult(actor ?? ctx?.actor ?? null, ctx, async (txCtx) =>
|
|
505
512
|
Ok(await this.repository.findEntityFieldDefinitions(orgId, entityType, txCtx)),
|
|
506
513
|
);
|
|
@@ -515,7 +522,7 @@ export class CatalogServiceImpl implements CatalogService {
|
|
|
515
522
|
return Promise.resolve(Err(new CommerceValidationError("Entity type and field name are required.")));
|
|
516
523
|
}
|
|
517
524
|
assertPermission(actor, "catalog:update");
|
|
518
|
-
const orgId =
|
|
525
|
+
const orgId = resolveOrgIdForCommerce(actor ?? ctx?.actor ?? null, this.config);
|
|
519
526
|
return this.withMutationResult(actor, ctx, async (txCtx) => {
|
|
520
527
|
const codeField = this.codeFieldDefinition(input.entityType, input.name);
|
|
521
528
|
if (codeField && input.type !== codeField.type) {
|
|
@@ -553,7 +560,7 @@ export class CatalogServiceImpl implements CatalogService {
|
|
|
553
560
|
ctx?: TxContext,
|
|
554
561
|
): Promise<Result<EntityFieldDefinitionRecord>> {
|
|
555
562
|
assertPermission(actor, "catalog:update");
|
|
556
|
-
const orgId =
|
|
563
|
+
const orgId = resolveOrgIdForCommerce(actor ?? ctx?.actor ?? null, this.config);
|
|
557
564
|
return this.withMutationResult(actor, ctx, async (txCtx) => {
|
|
558
565
|
const existing = await this.repository.findEntityFieldDefinitionById(orgId, id, txCtx);
|
|
559
566
|
if (!existing) return Err(new CommerceNotFoundError("Entity field definition not found."));
|
|
@@ -584,7 +591,7 @@ export class CatalogServiceImpl implements CatalogService {
|
|
|
584
591
|
ctx?: TxContext,
|
|
585
592
|
): Promise<Result<EntityFieldDefinitionRecord>> {
|
|
586
593
|
assertPermission(actor, "catalog:update");
|
|
587
|
-
const orgId =
|
|
594
|
+
const orgId = resolveOrgIdForCommerce(actor ?? ctx?.actor ?? null, this.config);
|
|
588
595
|
return this.withMutationResult(actor, ctx, async (txCtx) => {
|
|
589
596
|
const existing = await this.repository.findEntityFieldDefinitionById(orgId, id, txCtx);
|
|
590
597
|
if (!existing) return Err(new CommerceNotFoundError("Entity field definition not found."));
|
|
@@ -600,13 +607,17 @@ export class CatalogServiceImpl implements CatalogService {
|
|
|
600
607
|
|
|
601
608
|
private async withMutation<T>(
|
|
602
609
|
actor: Actor | null,
|
|
603
|
-
ctx:
|
|
610
|
+
ctx: CatalogWriteContext | undefined,
|
|
604
611
|
fn: (txCtx: TxContext) => Promise<T>,
|
|
605
612
|
): Promise<T> {
|
|
606
|
-
if (ctx
|
|
613
|
+
if (isWriteContextTransactional(ctx)) return fn(ctx);
|
|
614
|
+
const hookContext = resolveWriteContextHookContext(ctx);
|
|
607
615
|
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
608
616
|
try {
|
|
609
|
-
return await this.database.transaction(async (tx) => fn(createTxContext(tx, {
|
|
617
|
+
return await this.database.transaction(async (tx) => fn(createTxContext(tx, {
|
|
618
|
+
actor,
|
|
619
|
+
...(hookContext ? { hookContext } : {}),
|
|
620
|
+
})));
|
|
610
621
|
} catch (error) {
|
|
611
622
|
if (!isUniqueViolation(error) || attempt === 2) throw error;
|
|
612
623
|
}
|
|
@@ -616,7 +627,7 @@ export class CatalogServiceImpl implements CatalogService {
|
|
|
616
627
|
|
|
617
628
|
private async withMutationResult<T>(
|
|
618
629
|
actor: Actor | null,
|
|
619
|
-
ctx:
|
|
630
|
+
ctx: CatalogWriteContext | undefined,
|
|
620
631
|
fn: (txCtx: TxContext) => Promise<Result<T>>,
|
|
621
632
|
): Promise<Result<T>> {
|
|
622
633
|
try {
|
|
@@ -640,7 +651,7 @@ export class CatalogServiceImpl implements CatalogService {
|
|
|
640
651
|
const revisionActor = actor ?? ctx.actor;
|
|
641
652
|
const organizationId = typeof snapshot.entity.organizationId === "string"
|
|
642
653
|
? snapshot.entity.organizationId
|
|
643
|
-
:
|
|
654
|
+
: resolveOrgIdForCommerce(revisionActor, this.config);
|
|
644
655
|
return this.repository.createRevision({
|
|
645
656
|
organizationId,
|
|
646
657
|
entityId,
|
|
@@ -664,7 +675,7 @@ export class CatalogServiceImpl implements CatalogService {
|
|
|
664
675
|
assertPermission(revisionActor, "catalog:update");
|
|
665
676
|
const entity = await this.repository.findEntityById(entityId, txCtx);
|
|
666
677
|
if (!entity) return Err(new CommerceNotFoundError("Entity not found."));
|
|
667
|
-
if (entity.organizationId !==
|
|
678
|
+
if (entity.organizationId !== resolveOrgIdForCommerce(revisionActor, this.config)) {
|
|
668
679
|
return Err(new CommerceNotFoundError("Entity not found."));
|
|
669
680
|
}
|
|
670
681
|
return Ok(await this.captureRevision(entityId, revisionActor, reason, txCtx));
|
|
@@ -784,7 +795,7 @@ export class CatalogServiceImpl implements CatalogService {
|
|
|
784
795
|
assertPermission(revisionActor, "catalog:update");
|
|
785
796
|
const entity = await this.repository.findEntityById(entityId, txCtx);
|
|
786
797
|
if (!entity) return Err(new CommerceNotFoundError("Entity not found."));
|
|
787
|
-
if (entity.organizationId !==
|
|
798
|
+
if (entity.organizationId !== resolveOrgIdForCommerce(revisionActor, this.config)) {
|
|
788
799
|
return Err(new CommerceNotFoundError("Entity not found."));
|
|
789
800
|
}
|
|
790
801
|
if (entity.sourceStoreId != null) assertPermission(revisionActor, "catalog:sync");
|
|
@@ -816,7 +827,7 @@ export class CatalogServiceImpl implements CatalogService {
|
|
|
816
827
|
const revisionActor = actor ?? txCtx.actor;
|
|
817
828
|
assertPermission(revisionActor, "catalog:update");
|
|
818
829
|
const cutoff = new Date(Date.now() - olderThanDays * 24 * 60 * 60 * 1000);
|
|
819
|
-
return Ok(await this.repository.deleteRevisionsOlderThan(
|
|
830
|
+
return Ok(await this.repository.deleteRevisionsOlderThan(resolveOrgIdForCommerce(revisionActor, this.config), cutoff, txCtx));
|
|
820
831
|
});
|
|
821
832
|
} catch (error) {
|
|
822
833
|
return Err(toCommerceError(error));
|
|
@@ -831,7 +842,7 @@ export class CatalogServiceImpl implements CatalogService {
|
|
|
831
842
|
});
|
|
832
843
|
}
|
|
833
844
|
|
|
834
|
-
update(id: string, input: UpdateEntityInput, actor: Actor | null, ctx?:
|
|
845
|
+
update(id: string, input: UpdateEntityInput, actor: Actor | null, ctx?: CatalogWriteContext): Promise<Result<CatalogEntityHydrated>> {
|
|
835
846
|
return this.withMutationResult(actor, ctx, async (txCtx) => {
|
|
836
847
|
const result = await this.entities.update(id, input, actor, txCtx);
|
|
837
848
|
if (result.ok) await this.captureRevision(id, actor, "update", txCtx);
|
|
@@ -843,15 +854,19 @@ export class CatalogServiceImpl implements CatalogService {
|
|
|
843
854
|
return this.entities.delete(id, actor, ctx);
|
|
844
855
|
}
|
|
845
856
|
|
|
846
|
-
getById(id: string, options
|
|
857
|
+
getById(id: string, options: GetOptions | undefined, actor: Actor | null, ctx?: TxContext): Promise<Result<CatalogEntityHydrated>> {
|
|
847
858
|
return this.entities.getById(id, options, actor, ctx);
|
|
848
859
|
}
|
|
849
860
|
|
|
850
|
-
|
|
861
|
+
getByIdForInternalUse(id: string, organizationId: string, options?: GetOptions, ctx?: TxContext): Promise<Result<CatalogEntityHydrated>> {
|
|
862
|
+
return this.entities.getByIdForInternalUse(id, organizationId, options, ctx);
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
getBySlug(slug: string, options: GetOptions | undefined, actor: Actor | null, ctx?: TxContext): Promise<Result<CatalogEntityHydrated>> {
|
|
851
866
|
return this.entities.getBySlug(slug, options, actor, ctx);
|
|
852
867
|
}
|
|
853
868
|
|
|
854
|
-
list(params: ListParams, actor
|
|
869
|
+
list(params: ListParams, actor: Actor | null, ctx?: TxContext): Promise<Result<CatalogListResult>> {
|
|
855
870
|
return this.entities.list(params, actor, ctx);
|
|
856
871
|
}
|
|
857
872
|
|
|
@@ -910,7 +925,7 @@ export class CatalogServiceImpl implements CatalogService {
|
|
|
910
925
|
validateFieldPath(fieldPath);
|
|
911
926
|
const entity = await this.repository.findEntityById(entityId, txCtx);
|
|
912
927
|
if (!entity) return Err(new CommerceNotFoundError("Entity not found."));
|
|
913
|
-
if (entity.organizationId !==
|
|
928
|
+
if (entity.organizationId !== resolveOrgIdForCommerce(setter, this.config)) {
|
|
914
929
|
return Err(new CommerceNotFoundError("Entity not found."));
|
|
915
930
|
}
|
|
916
931
|
if (entity.sourceStoreId != null) assertPermission(setter, "catalog:sync");
|
|
@@ -937,7 +952,7 @@ export class CatalogServiceImpl implements CatalogService {
|
|
|
937
952
|
assertPermission(reader, "catalog:read");
|
|
938
953
|
const entity = await this.repository.findEntityById(entityId, txCtx);
|
|
939
954
|
if (!entity) return Err(new CommerceNotFoundError("Entity not found."));
|
|
940
|
-
if (entity.organizationId !==
|
|
955
|
+
if (entity.organizationId !== resolveOrgIdForCommerce(reader, this.config)) {
|
|
941
956
|
return Err(new CommerceNotFoundError("Entity not found."));
|
|
942
957
|
}
|
|
943
958
|
return Ok(await this.repository.findFieldOwnership(entityId, txCtx, storeId));
|
|
@@ -989,7 +1004,7 @@ export class CatalogServiceImpl implements CatalogService {
|
|
|
989
1004
|
});
|
|
990
1005
|
}
|
|
991
1006
|
|
|
992
|
-
setAttributes(entityId: string, locale: string, attrs: SetAttributesInput, actor: Actor | null, ctx?:
|
|
1007
|
+
setAttributes(entityId: string, locale: string, attrs: SetAttributesInput, actor: Actor | null, ctx?: CatalogWriteContext): Promise<Result<void>> {
|
|
993
1008
|
return this.withMutationResult(actor, ctx, async (txCtx) => {
|
|
994
1009
|
const result = await this.entities.setAttributes(entityId, locale, attrs, actor, txCtx);
|
|
995
1010
|
if (result.ok) await this.captureRevision(entityId, actor, "update", txCtx);
|
|
@@ -1010,7 +1025,7 @@ export class CatalogServiceImpl implements CatalogService {
|
|
|
1010
1025
|
assertPermission(reviewer, "catalog:update");
|
|
1011
1026
|
const entity = await this.repository.findEntityById(entityId, txCtx);
|
|
1012
1027
|
if (!entity) return Err(new CommerceNotFoundError("Entity not found."));
|
|
1013
|
-
if (entity.organizationId !==
|
|
1028
|
+
if (entity.organizationId !== resolveOrgIdForCommerce(reviewer, this.config)) {
|
|
1014
1029
|
return Err(new CommerceNotFoundError("Entity not found."));
|
|
1015
1030
|
}
|
|
1016
1031
|
if (entity.sourceStoreId != null) assertPermission(reviewer, "catalog:sync");
|
|
@@ -1057,8 +1072,8 @@ export class CatalogServiceImpl implements CatalogService {
|
|
|
1057
1072
|
return this.reviewCustomField("rejected", entityId, fieldName, locale, actor, ctx);
|
|
1058
1073
|
}
|
|
1059
1074
|
|
|
1060
|
-
getAttributes(entityId: string, locale: string, ctx?: TxContext): Promise<Result<SellableAttribute>> {
|
|
1061
|
-
return this.entities.getAttributes(entityId, locale, ctx);
|
|
1075
|
+
getAttributes(entityId: string, locale: string, actor: Actor | null, ctx?: TxContext): Promise<Result<SellableAttribute>> {
|
|
1076
|
+
return this.entities.getAttributes(entityId, locale, actor, ctx);
|
|
1062
1077
|
}
|
|
1063
1078
|
|
|
1064
1079
|
listCategories(ctx?: TxContext, opts?: { includeArchived?: boolean }): Promise<Result<CategorySummary[]>> {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { CommerceConfig } from "../../config/types.js";
|
|
1
2
|
import type { DrizzleDatabase } from "../../kernel/database/drizzle-db.js";
|
|
2
3
|
import { defineModule } from "../../kernel/module/index.js";
|
|
3
4
|
import { CustomersRepository } from "./repository/index.js";
|
|
@@ -21,6 +22,7 @@ export const customersModule = defineModule({
|
|
|
21
22
|
new CustomerService({
|
|
22
23
|
repository: new CustomersRepository(deps.db.db as DrizzleDatabase),
|
|
23
24
|
hooks: deps.hooks,
|
|
25
|
+
config: deps.config as CommerceConfig,
|
|
24
26
|
services: deps.services,
|
|
25
27
|
database: deps.db,
|
|
26
28
|
}),
|