@porulle/core 0.10.8 → 0.13.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/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 +13 -5
- 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 +17 -0
- 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 +33 -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 +9 -6
- 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/index.d.ts.map +1 -1
- package/dist/interfaces/rest/index.js +4 -0
- 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.d.ts +5 -0
- package/dist/interfaces/rest/routes/admin/custom-field-proposals.d.ts.map +1 -0
- package/dist/interfaces/rest/routes/admin/custom-field-proposals.js +22 -0
- package/dist/interfaces/rest/routes/admin/entity-field-definitions.d.ts +5 -0
- package/dist/interfaces/rest/routes/admin/entity-field-definitions.d.ts.map +1 -0
- package/dist/interfaces/rest/routes/admin/entity-field-definitions.js +39 -0
- 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 +47 -4
- 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 +14 -1
- package/dist/interfaces/rest/schemas/admin-custom-field-proposals.d.ts +134 -0
- package/dist/interfaces/rest/schemas/admin-custom-field-proposals.d.ts.map +1 -0
- package/dist/interfaces/rest/schemas/admin-custom-field-proposals.js +26 -0
- package/dist/interfaces/rest/schemas/admin-entity-field-definitions.d.ts +592 -0
- package/dist/interfaces/rest/schemas/admin-entity-field-definitions.d.ts.map +1 -0
- package/dist/interfaces/rest/schemas/admin-entity-field-definitions.js +109 -0
- 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/catalog.d.ts +1364 -401
- package/dist/interfaces/rest/schemas/catalog.d.ts.map +1 -1
- package/dist/interfaces/rest/schemas/catalog.js +86 -0
- 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/pricing.d.ts +1 -0
- package/dist/interfaces/rest/schemas/pricing.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/schemas/search.d.ts +1 -0
- package/dist/interfaces/rest/schemas/search.d.ts.map +1 -1
- package/dist/interfaces/rest/schemas/search.js +1 -0
- 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/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 +19 -10
- package/dist/modules/catalog/entity-service.d.ts.map +1 -1
- package/dist/modules/catalog/entity-service.js +180 -51
- package/dist/modules/catalog/module.d.ts +6 -1
- package/dist/modules/catalog/module.d.ts.map +1 -1
- package/dist/modules/catalog/module.js +6 -1
- package/dist/modules/catalog/ownership.d.ts +5 -0
- package/dist/modules/catalog/ownership.d.ts.map +1 -0
- package/dist/modules/catalog/ownership.js +11 -0
- 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 +55 -3
- package/dist/modules/catalog/repository/index.d.ts.map +1 -1
- package/dist/modules/catalog/repository/index.js +335 -6
- package/dist/modules/catalog/revision.d.ts +4 -0
- package/dist/modules/catalog/revision.d.ts.map +1 -0
- package/dist/modules/catalog/revision.js +33 -0
- package/dist/modules/catalog/schema.d.ts +979 -110
- package/dist/modules/catalog/schema.d.ts.map +1 -1
- package/dist/modules/catalog/schema.js +112 -2
- package/dist/modules/catalog/schemas.d.ts +2 -0
- package/dist/modules/catalog/schemas.d.ts.map +1 -1
- package/dist/modules/catalog/schemas.js +2 -0
- package/dist/modules/catalog/service.d.ts +80 -15
- package/dist/modules/catalog/service.d.ts.map +1 -1
- package/dist/modules/catalog/service.js +522 -14
- package/dist/modules/channels/adapter.d.ts +85 -0
- package/dist/modules/channels/adapter.d.ts.map +1 -1
- package/dist/modules/channels/adapter.js +9 -1
- 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/module.d.ts +68 -0
- package/dist/modules/media/module.d.ts.map +1 -1
- package/dist/modules/media/module.js +2 -0
- package/dist/modules/media/schema.d.ts +70 -0
- package/dist/modules/media/schema.d.ts.map +1 -1
- package/dist/modules/media/schema.js +20 -2
- package/dist/modules/media/service.d.ts +8 -1
- package/dist/modules/media/service.d.ts.map +1 -1
- package/dist/modules/media/service.js +71 -23
- 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/schema.d.ts +17 -0
- package/dist/modules/pricing/schema.d.ts.map +1 -1
- package/dist/modules/pricing/schema.js +1 -0
- package/dist/modules/pricing/schemas.d.ts +1 -0
- package/dist/modules/pricing/schemas.d.ts.map +1 -1
- package/dist/modules/pricing/schemas.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 +18 -12
- 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 +8 -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 +3 -0
- package/dist/modules/search/service.d.ts +5 -0
- package/dist/modules/search/service.d.ts.map +1 -1
- package/dist/modules/search/service.js +131 -11
- 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/kernel-modules.d.ts +73 -0
- package/dist/runtime/kernel-modules.d.ts.map +1 -1
- package/dist/runtime/server.d.ts.map +1 -1
- package/dist/runtime/server.js +10 -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/auth-schema-guard.ts +143 -0
- package/src/auth/auth-schema.ts +50 -19
- package/src/auth/middleware.ts +17 -5
- 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 +17 -0
- 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 +32 -4
- package/src/hooks/checkout.ts +19 -12
- package/src/index.ts +30 -6
- package/src/interfaces/rest/audit-middleware.ts +2 -0
- package/src/interfaces/rest/customer-portal.ts +31 -24
- package/src/interfaces/rest/index.ts +4 -0
- 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 +37 -0
- package/src/interfaces/rest/routes/admin/entity-field-definitions.ts +89 -0
- 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 +96 -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 +19 -1
- package/src/interfaces/rest/schemas/admin-custom-field-proposals.ts +29 -0
- package/src/interfaces/rest/schemas/admin-entity-field-definitions.ts +120 -0
- package/src/interfaces/rest/schemas/catalog.ts +92 -0
- package/src/interfaces/rest/schemas/search.ts +2 -0
- package/src/interfaces/rest/utils.ts +61 -2
- package/src/kernel/compensation/executor.ts +2 -2
- 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 +188 -53
- package/src/modules/catalog/module.ts +15 -0
- package/src/modules/catalog/ownership.ts +17 -0
- package/src/modules/catalog/read-policy.ts +16 -0
- package/src/modules/catalog/repository/index.ts +566 -4
- package/src/modules/catalog/revision.ts +35 -0
- package/src/modules/catalog/schema.ts +169 -1
- package/src/modules/catalog/schemas.ts +2 -0
- package/src/modules/catalog/service.ts +766 -26
- package/src/modules/channels/adapter.ts +107 -1
- 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/module.ts +2 -0
- package/src/modules/media/schema.ts +23 -2
- package/src/modules/media/service.ts +89 -28
- 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/schema.ts +1 -0
- package/src/modules/pricing/schemas.ts +1 -0
- package/src/modules/pricing/service.ts +21 -18
- package/src/modules/promotions/module.ts +2 -0
- package/src/modules/promotions/service.ts +25 -16
- package/src/modules/search/adapter.ts +8 -0
- package/src/modules/search/module.ts +3 -0
- package/src/modules/search/service.ts +142 -12
- 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 +16 -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,10 +1,14 @@
|
|
|
1
1
|
import type { Actor } from "../../auth/types.js";
|
|
2
|
+
import { resolveOrgIdForCommerce } from "../../auth/org.js";
|
|
3
|
+
import { assertPermission } from "../../auth/permissions.js";
|
|
2
4
|
import type { CommerceConfig } from "../../config/types.js";
|
|
5
|
+
import type { EntityFieldDefinition, FieldType } from "../../config/types.js";
|
|
3
6
|
import type { HookRegistry } from "../../kernel/hooks/registry.js";
|
|
4
|
-
import type
|
|
7
|
+
import { Err, Ok, type Result } from "../../kernel/result.js";
|
|
8
|
+
import { CommerceConflictError, CommerceNotFoundError, CommerceValidationError, toCommerceError } from "../../kernel/errors.js";
|
|
5
9
|
import type { Pagination } from "../../utils/pagination.js";
|
|
6
10
|
import type { DatabaseAdapter } from "../../kernel/database/adapter.js";
|
|
7
|
-
import type
|
|
11
|
+
import { createTxContext, type CatalogWriteContext, isWriteContextTransactional, resolveWriteContextHookContext, type TxContext } from "../../kernel/database/tx-context.js";
|
|
8
12
|
import {
|
|
9
13
|
CatalogRepository,
|
|
10
14
|
type SellableEntity,
|
|
@@ -15,7 +19,17 @@ import {
|
|
|
15
19
|
type OptionType,
|
|
16
20
|
type OptionValue,
|
|
17
21
|
type Variant,
|
|
22
|
+
type SellableEntityRevision,
|
|
23
|
+
type SellableEntityInsert,
|
|
24
|
+
type SellableAttributeInsert,
|
|
25
|
+
type SellableCustomField,
|
|
26
|
+
type SellableCustomFieldInsert,
|
|
27
|
+
type EntityFieldDefinitionRecord,
|
|
28
|
+
type CatalogFieldOwnership,
|
|
18
29
|
} from "./repository/index.js";
|
|
30
|
+
import type { SellableEntityRevisionReason, SellableEntityRevisionSnapshot } from "./schema.js";
|
|
31
|
+
import { isValidFieldPath, validateFieldPath, type FieldOwner, type FieldPath } from "./ownership.js";
|
|
32
|
+
import { comparableSerialize } from "./revision.js";
|
|
19
33
|
|
|
20
34
|
// ─── Re-exported schema-derived types ────────────────────────────────────────
|
|
21
35
|
export type {
|
|
@@ -87,6 +101,7 @@ export interface GetOptions {
|
|
|
87
101
|
includeMedia?: boolean;
|
|
88
102
|
includeCategories?: boolean;
|
|
89
103
|
includeBrands?: boolean;
|
|
104
|
+
includeCustomFields?: boolean;
|
|
90
105
|
}
|
|
91
106
|
|
|
92
107
|
export interface VariantMatrixRule {
|
|
@@ -99,8 +114,32 @@ export type VariantGenerationStrategy =
|
|
|
99
114
|
| { mode: "manual"; combinations: string[][] }
|
|
100
115
|
| { mode: "matrix"; matrix: VariantMatrixRule };
|
|
101
116
|
|
|
117
|
+
export type EntityFieldDefinitionResolver = (
|
|
118
|
+
entityType: string,
|
|
119
|
+
actorOrOrg?: Actor | string | null,
|
|
120
|
+
ctx?: TxContext,
|
|
121
|
+
) => Promise<EntityFieldDefinition[]>;
|
|
122
|
+
|
|
123
|
+
export type CreateEntityFieldDefinitionInput = {
|
|
124
|
+
entityType: string;
|
|
125
|
+
name: string;
|
|
126
|
+
type: FieldType;
|
|
127
|
+
unit?: string | null;
|
|
128
|
+
options?: string[] | null;
|
|
129
|
+
target?: string | null;
|
|
130
|
+
filterable?: boolean;
|
|
131
|
+
localized?: boolean;
|
|
132
|
+
sortOrder?: number;
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
export type UpdateEntityFieldDefinitionInput = Partial<Omit<CreateEntityFieldDefinitionInput, "entityType" | "name">> & {
|
|
136
|
+
entityType?: string;
|
|
137
|
+
name?: string;
|
|
138
|
+
};
|
|
139
|
+
|
|
102
140
|
export interface CatalogEntityHydrated extends SellableEntity {
|
|
103
141
|
attributes?: SellableAttribute[];
|
|
142
|
+
customFields?: SellableCustomField[];
|
|
104
143
|
variants?: Array<Variant & { optionValueIds: string[] }>;
|
|
105
144
|
optionTypes?: Array<OptionType & { values: OptionValue[] }>;
|
|
106
145
|
categories?: EntityCategory[];
|
|
@@ -120,7 +159,7 @@ export interface CatalogService {
|
|
|
120
159
|
id: string,
|
|
121
160
|
input: UpdateEntityInput,
|
|
122
161
|
actor: Actor | null,
|
|
123
|
-
ctx?:
|
|
162
|
+
ctx?: CatalogWriteContext,
|
|
124
163
|
): Promise<Result<CatalogEntityHydrated>>;
|
|
125
164
|
delete(
|
|
126
165
|
id: string,
|
|
@@ -129,17 +168,58 @@ export interface CatalogService {
|
|
|
129
168
|
): Promise<Result<void>>;
|
|
130
169
|
getById(
|
|
131
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,
|
|
132
178
|
options?: GetOptions,
|
|
133
|
-
actor?: Actor | null,
|
|
134
179
|
ctx?: TxContext,
|
|
135
180
|
): Promise<Result<CatalogEntityHydrated>>;
|
|
136
181
|
getBySlug(
|
|
137
182
|
slug: string,
|
|
138
|
-
options
|
|
139
|
-
actor
|
|
183
|
+
options: GetOptions | undefined,
|
|
184
|
+
actor: Actor | null,
|
|
140
185
|
ctx?: TxContext,
|
|
141
186
|
): Promise<Result<CatalogEntityHydrated>>;
|
|
142
|
-
list(params: ListParams, actor
|
|
187
|
+
list(params: ListParams, actor: Actor | null, ctx?: TxContext): Promise<Result<CatalogListResult>>;
|
|
188
|
+
/** Returns undefined when no concrete ownership row applies to the field. */
|
|
189
|
+
resolveFieldOwner(
|
|
190
|
+
entityId: string,
|
|
191
|
+
fieldPath: FieldPath,
|
|
192
|
+
storeId: string,
|
|
193
|
+
ctx?: TxContext,
|
|
194
|
+
variantId?: string | null,
|
|
195
|
+
): Promise<FieldOwner | undefined>;
|
|
196
|
+
/** Entity-level bulk resolution: variant-scoped ownership rows are not consulted. */
|
|
197
|
+
resolveFieldOwners(
|
|
198
|
+
entityId: string,
|
|
199
|
+
storeId: string,
|
|
200
|
+
ctx?: TxContext,
|
|
201
|
+
): Promise<Map<FieldPath, FieldOwner>>;
|
|
202
|
+
setFieldOwner(
|
|
203
|
+
entityId: string,
|
|
204
|
+
fieldPath: FieldPath,
|
|
205
|
+
storeId: string | null,
|
|
206
|
+
owner: FieldOwner,
|
|
207
|
+
actor: Actor | null,
|
|
208
|
+
ctx?: TxContext,
|
|
209
|
+
variantId?: string | null,
|
|
210
|
+
): Promise<Result<void>>;
|
|
211
|
+
listFieldOwnership(
|
|
212
|
+
entityId: string,
|
|
213
|
+
actor: Actor | null,
|
|
214
|
+
ctx?: TxContext,
|
|
215
|
+
storeId?: string,
|
|
216
|
+
): Promise<Result<CatalogFieldOwnership[]>>;
|
|
217
|
+
seedImportedFieldOwnership(
|
|
218
|
+
entityId: string,
|
|
219
|
+
storeId: string,
|
|
220
|
+
fieldPaths: FieldPath[],
|
|
221
|
+
ctx?: TxContext,
|
|
222
|
+
): Promise<Result<void>>;
|
|
143
223
|
publish(
|
|
144
224
|
id: string,
|
|
145
225
|
actor: Actor | null,
|
|
@@ -160,11 +240,26 @@ export interface CatalogService {
|
|
|
160
240
|
locale: string,
|
|
161
241
|
attrs: SetAttributesInput,
|
|
162
242
|
actor: Actor | null,
|
|
163
|
-
ctx?:
|
|
243
|
+
ctx?: CatalogWriteContext,
|
|
164
244
|
): Promise<Result<void>>;
|
|
245
|
+
approveCustomField(
|
|
246
|
+
entityId: string,
|
|
247
|
+
fieldName: string,
|
|
248
|
+
locale: string,
|
|
249
|
+
actor: Actor | null,
|
|
250
|
+
ctx?: TxContext,
|
|
251
|
+
): Promise<Result<SellableCustomField>>;
|
|
252
|
+
rejectCustomField(
|
|
253
|
+
entityId: string,
|
|
254
|
+
fieldName: string,
|
|
255
|
+
locale: string,
|
|
256
|
+
actor: Actor | null,
|
|
257
|
+
ctx?: TxContext,
|
|
258
|
+
): Promise<Result<SellableCustomField>>;
|
|
165
259
|
getAttributes(
|
|
166
260
|
entityId: string,
|
|
167
261
|
locale: string,
|
|
262
|
+
actor: Actor | null,
|
|
168
263
|
ctx?: TxContext,
|
|
169
264
|
): Promise<Result<SellableAttribute>>;
|
|
170
265
|
listCategories(
|
|
@@ -271,6 +366,45 @@ export interface CatalogService {
|
|
|
271
366
|
actor: Actor | null,
|
|
272
367
|
ctx?: TxContext,
|
|
273
368
|
): Promise<Result<{ created: Variant[]; skipped: number }>>;
|
|
369
|
+
recordEntityRevision(
|
|
370
|
+
entityId: string,
|
|
371
|
+
actor: Actor | null,
|
|
372
|
+
reason?: SellableEntityRevisionReason,
|
|
373
|
+
ctx?: TxContext,
|
|
374
|
+
): Promise<Result<SellableEntityRevision>>;
|
|
375
|
+
restoreEntityRevision(
|
|
376
|
+
entityId: string,
|
|
377
|
+
revisionId: string,
|
|
378
|
+
actor: Actor | null,
|
|
379
|
+
ctx?: TxContext,
|
|
380
|
+
): Promise<Result<CatalogEntityHydrated>>;
|
|
381
|
+
trimEntityRevisions(
|
|
382
|
+
actor: Actor | null,
|
|
383
|
+
olderThanDays?: number,
|
|
384
|
+
ctx?: TxContext,
|
|
385
|
+
): Promise<Result<number>>;
|
|
386
|
+
listEntityFieldDefinitions(
|
|
387
|
+
actor?: Actor | null,
|
|
388
|
+
entityType?: string,
|
|
389
|
+
ctx?: TxContext,
|
|
390
|
+
): Promise<Result<EntityFieldDefinitionRecord[]>>;
|
|
391
|
+
createEntityFieldDefinition(
|
|
392
|
+
input: CreateEntityFieldDefinitionInput,
|
|
393
|
+
actor: Actor | null,
|
|
394
|
+
ctx?: TxContext,
|
|
395
|
+
): Promise<Result<EntityFieldDefinitionRecord>>;
|
|
396
|
+
updateEntityFieldDefinition(
|
|
397
|
+
id: string,
|
|
398
|
+
input: UpdateEntityFieldDefinitionInput,
|
|
399
|
+
actor: Actor | null,
|
|
400
|
+
ctx?: TxContext,
|
|
401
|
+
): Promise<Result<EntityFieldDefinitionRecord>>;
|
|
402
|
+
archiveEntityFieldDefinition(
|
|
403
|
+
id: string,
|
|
404
|
+
actor: Actor | null,
|
|
405
|
+
ctx?: TxContext,
|
|
406
|
+
): Promise<Result<EntityFieldDefinitionRecord>>;
|
|
407
|
+
resolveEntityFieldDefinitions: EntityFieldDefinitionResolver;
|
|
274
408
|
}
|
|
275
409
|
|
|
276
410
|
export interface CatalogServiceDeps {
|
|
@@ -295,61 +429,651 @@ export type CategorySummary = {
|
|
|
295
429
|
metadata: Record<string, unknown>;
|
|
296
430
|
};
|
|
297
431
|
|
|
432
|
+
function isUniqueViolation(error: unknown): boolean {
|
|
433
|
+
if (error == null || typeof error !== "object") return false;
|
|
434
|
+
const value = error as { code?: unknown; cause?: unknown };
|
|
435
|
+
if (value.code === "23505") return true;
|
|
436
|
+
return isUniqueViolation(value.cause);
|
|
437
|
+
}
|
|
438
|
+
|
|
298
439
|
export class CatalogServiceImpl implements CatalogService {
|
|
299
440
|
readonly repository: CatalogRepository;
|
|
441
|
+
private readonly database: DatabaseAdapter;
|
|
442
|
+
private readonly config: CommerceConfig;
|
|
300
443
|
private readonly entities: EntityService;
|
|
301
444
|
private readonly categories: CategoryService;
|
|
302
445
|
private readonly brands: BrandService;
|
|
303
446
|
|
|
304
447
|
constructor(deps: CatalogServiceDeps) {
|
|
305
448
|
this.repository = deps.repository;
|
|
306
|
-
this.
|
|
449
|
+
this.database = deps.database;
|
|
450
|
+
this.config = deps.config;
|
|
451
|
+
this.entities = new EntityService(deps, this.resolveEntityFieldDefinitions.bind(this));
|
|
307
452
|
this.categories = new CategoryService(deps);
|
|
308
453
|
this.brands = new BrandService(deps);
|
|
309
454
|
}
|
|
310
455
|
|
|
456
|
+
private codeFieldDefinition(entityType: string, name: string): EntityFieldDefinition | undefined {
|
|
457
|
+
return this.config.entities?.[entityType]?.fields.find((field) => field.name === name);
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
private runtimeFieldDefinition(row: EntityFieldDefinitionRecord): EntityFieldDefinition {
|
|
461
|
+
return {
|
|
462
|
+
name: row.name,
|
|
463
|
+
type: row.type,
|
|
464
|
+
...(row.unit != null ? { unit: row.unit } : {}),
|
|
465
|
+
...(row.options != null ? { options: row.options } : {}),
|
|
466
|
+
...(row.target != null ? { target: row.target } : {}),
|
|
467
|
+
filterable: row.filterable,
|
|
468
|
+
localized: row.localized,
|
|
469
|
+
sortOrder: row.sortOrder,
|
|
470
|
+
};
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
async resolveEntityFieldDefinitions(
|
|
474
|
+
entityType: string,
|
|
475
|
+
actorOrOrg?: Actor | string | null,
|
|
476
|
+
ctx?: TxContext,
|
|
477
|
+
): Promise<EntityFieldDefinition[]> {
|
|
478
|
+
const orgId = typeof actorOrOrg === "string"
|
|
479
|
+
? actorOrOrg
|
|
480
|
+
: resolveOrgIdForCommerce(actorOrOrg ?? ctx?.actor ?? null, this.config);
|
|
481
|
+
const codeFields = this.config.entities?.[entityType]?.fields ?? [];
|
|
482
|
+
const merged = new Map(codeFields.map((field) => [field.name, { ...field }]));
|
|
483
|
+
const runtimeFields = await this.repository.findActiveEntityFieldDefinitions(orgId, entityType, ctx);
|
|
484
|
+
|
|
485
|
+
for (const row of runtimeFields) {
|
|
486
|
+
const codeField = merged.get(row.name);
|
|
487
|
+
if (!codeField) {
|
|
488
|
+
merged.set(row.name, this.runtimeFieldDefinition(row));
|
|
489
|
+
continue;
|
|
490
|
+
}
|
|
491
|
+
merged.set(row.name, {
|
|
492
|
+
...codeField,
|
|
493
|
+
...(row.options != null ? { options: row.options } : {}),
|
|
494
|
+
filterable: row.filterable,
|
|
495
|
+
localized: row.localized,
|
|
496
|
+
sortOrder: row.sortOrder,
|
|
497
|
+
});
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
return [...merged.values()].sort((first, second) =>
|
|
501
|
+
(first.sortOrder ?? 0) - (second.sortOrder ?? 0),
|
|
502
|
+
);
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
listEntityFieldDefinitions(
|
|
506
|
+
actor?: Actor | null,
|
|
507
|
+
entityType?: string,
|
|
508
|
+
ctx?: TxContext,
|
|
509
|
+
): Promise<Result<EntityFieldDefinitionRecord[]>> {
|
|
510
|
+
const orgId = resolveOrgIdForCommerce(actor ?? ctx?.actor ?? null, this.config);
|
|
511
|
+
return this.withMutationResult(actor ?? ctx?.actor ?? null, ctx, async (txCtx) =>
|
|
512
|
+
Ok(await this.repository.findEntityFieldDefinitions(orgId, entityType, txCtx)),
|
|
513
|
+
);
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
createEntityFieldDefinition(
|
|
517
|
+
input: CreateEntityFieldDefinitionInput,
|
|
518
|
+
actor: Actor | null,
|
|
519
|
+
ctx?: TxContext,
|
|
520
|
+
): Promise<Result<EntityFieldDefinitionRecord>> {
|
|
521
|
+
if (input.entityType.length === 0 || input.name.length === 0) {
|
|
522
|
+
return Promise.resolve(Err(new CommerceValidationError("Entity type and field name are required.")));
|
|
523
|
+
}
|
|
524
|
+
assertPermission(actor, "catalog:update");
|
|
525
|
+
const orgId = resolveOrgIdForCommerce(actor ?? ctx?.actor ?? null, this.config);
|
|
526
|
+
return this.withMutationResult(actor, ctx, async (txCtx) => {
|
|
527
|
+
const codeField = this.codeFieldDefinition(input.entityType, input.name);
|
|
528
|
+
if (codeField && input.type !== codeField.type) {
|
|
529
|
+
return Err(new CommerceValidationError("Code-defined fields only allow options, filterable, localized, and sortOrder overrides."));
|
|
530
|
+
}
|
|
531
|
+
return Ok(await this.repository.createEntityFieldDefinition({
|
|
532
|
+
organizationId: orgId,
|
|
533
|
+
entityType: input.entityType,
|
|
534
|
+
name: input.name,
|
|
535
|
+
type: input.type,
|
|
536
|
+
// A shadow row over a code field inherits the code values for anything
|
|
537
|
+
// omitted, so creating one never silently flips a code default.
|
|
538
|
+
...(codeField?.unit !== undefined || input.unit !== undefined
|
|
539
|
+
? { unit: codeField ? codeField.unit ?? null : input.unit }
|
|
540
|
+
: {}),
|
|
541
|
+
...(codeField?.target !== undefined || input.target !== undefined
|
|
542
|
+
? { target: codeField ? codeField.target ?? null : input.target }
|
|
543
|
+
: {}),
|
|
544
|
+
...(input.options !== undefined
|
|
545
|
+
? { options: input.options }
|
|
546
|
+
: codeField?.options !== undefined ? { options: codeField.options } : {}),
|
|
547
|
+
...(input.filterable !== undefined
|
|
548
|
+
? { filterable: input.filterable }
|
|
549
|
+
: codeField?.filterable !== undefined ? { filterable: codeField.filterable } : {}),
|
|
550
|
+
...(input.localized !== undefined ? { localized: input.localized } : {}),
|
|
551
|
+
...(input.sortOrder !== undefined ? { sortOrder: input.sortOrder } : {}),
|
|
552
|
+
}, txCtx));
|
|
553
|
+
});
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
updateEntityFieldDefinition(
|
|
557
|
+
id: string,
|
|
558
|
+
input: UpdateEntityFieldDefinitionInput,
|
|
559
|
+
actor: Actor | null,
|
|
560
|
+
ctx?: TxContext,
|
|
561
|
+
): Promise<Result<EntityFieldDefinitionRecord>> {
|
|
562
|
+
assertPermission(actor, "catalog:update");
|
|
563
|
+
const orgId = resolveOrgIdForCommerce(actor ?? ctx?.actor ?? null, this.config);
|
|
564
|
+
return this.withMutationResult(actor, ctx, async (txCtx) => {
|
|
565
|
+
const existing = await this.repository.findEntityFieldDefinitionById(orgId, id, txCtx);
|
|
566
|
+
if (!existing) return Err(new CommerceNotFoundError("Entity field definition not found."));
|
|
567
|
+
const codeField = this.codeFieldDefinition(existing.entityType, existing.name);
|
|
568
|
+
if (codeField && (input.entityType !== undefined || input.name !== undefined || input.type !== undefined || input.unit !== undefined || input.target !== undefined)) {
|
|
569
|
+
return Err(new CommerceValidationError("Code-defined fields only allow options, filterable, localized, and sortOrder updates."));
|
|
570
|
+
}
|
|
571
|
+
const data = {
|
|
572
|
+
...(input.entityType !== undefined ? { entityType: input.entityType } : {}),
|
|
573
|
+
...(input.name !== undefined ? { name: input.name } : {}),
|
|
574
|
+
...(input.type !== undefined ? { type: input.type } : {}),
|
|
575
|
+
...(input.unit !== undefined ? { unit: input.unit } : {}),
|
|
576
|
+
...(input.options !== undefined ? { options: input.options } : {}),
|
|
577
|
+
...(input.target !== undefined ? { target: input.target } : {}),
|
|
578
|
+
...(input.filterable !== undefined ? { filterable: input.filterable } : {}),
|
|
579
|
+
...(input.localized !== undefined ? { localized: input.localized } : {}),
|
|
580
|
+
...(input.sortOrder !== undefined ? { sortOrder: input.sortOrder } : {}),
|
|
581
|
+
};
|
|
582
|
+
const updated = await this.repository.updateEntityFieldDefinition(orgId, id, data, txCtx);
|
|
583
|
+
if (!updated) return Err(new CommerceNotFoundError("Entity field definition not found."));
|
|
584
|
+
return Ok(updated);
|
|
585
|
+
});
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
archiveEntityFieldDefinition(
|
|
589
|
+
id: string,
|
|
590
|
+
actor: Actor | null,
|
|
591
|
+
ctx?: TxContext,
|
|
592
|
+
): Promise<Result<EntityFieldDefinitionRecord>> {
|
|
593
|
+
assertPermission(actor, "catalog:update");
|
|
594
|
+
const orgId = resolveOrgIdForCommerce(actor ?? ctx?.actor ?? null, this.config);
|
|
595
|
+
return this.withMutationResult(actor, ctx, async (txCtx) => {
|
|
596
|
+
const existing = await this.repository.findEntityFieldDefinitionById(orgId, id, txCtx);
|
|
597
|
+
if (!existing) return Err(new CommerceNotFoundError("Entity field definition not found."));
|
|
598
|
+
if (this.codeFieldDefinition(existing.entityType, existing.name)) {
|
|
599
|
+
return Err(new CommerceValidationError("Code-defined fields cannot be archived."));
|
|
600
|
+
}
|
|
601
|
+
if (existing.status === "archived") return Ok(existing);
|
|
602
|
+
const updated = await this.repository.updateEntityFieldDefinition(orgId, id, { status: "archived" }, txCtx);
|
|
603
|
+
if (!updated) return Err(new CommerceNotFoundError("Entity field definition not found."));
|
|
604
|
+
return Ok(updated);
|
|
605
|
+
});
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
private async withMutation<T>(
|
|
609
|
+
actor: Actor | null,
|
|
610
|
+
ctx: CatalogWriteContext | undefined,
|
|
611
|
+
fn: (txCtx: TxContext) => Promise<T>,
|
|
612
|
+
): Promise<T> {
|
|
613
|
+
if (isWriteContextTransactional(ctx)) return fn(ctx);
|
|
614
|
+
const hookContext = resolveWriteContextHookContext(ctx);
|
|
615
|
+
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
616
|
+
try {
|
|
617
|
+
return await this.database.transaction(async (tx) => fn(createTxContext(tx, {
|
|
618
|
+
actor,
|
|
619
|
+
...(hookContext ? { hookContext } : {}),
|
|
620
|
+
})));
|
|
621
|
+
} catch (error) {
|
|
622
|
+
if (!isUniqueViolation(error) || attempt === 2) throw error;
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
throw new Error("Catalog mutation retry limit reached.");
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
private async withMutationResult<T>(
|
|
629
|
+
actor: Actor | null,
|
|
630
|
+
ctx: CatalogWriteContext | undefined,
|
|
631
|
+
fn: (txCtx: TxContext) => Promise<Result<T>>,
|
|
632
|
+
): Promise<Result<T>> {
|
|
633
|
+
try {
|
|
634
|
+
return await this.withMutation(actor, ctx, fn);
|
|
635
|
+
} catch (error) {
|
|
636
|
+
if (isUniqueViolation(error)) return Err(new CommerceConflictError("A resource with those values already exists."));
|
|
637
|
+
return Err(toCommerceError(error));
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
private async captureRevision(
|
|
642
|
+
entityId: string,
|
|
643
|
+
actor: Actor | null,
|
|
644
|
+
reason: SellableEntityRevisionReason,
|
|
645
|
+
ctx: TxContext,
|
|
646
|
+
force = false,
|
|
647
|
+
): Promise<SellableEntityRevision> {
|
|
648
|
+
const snapshot = await this.repository.snapshotEntity(entityId, ctx);
|
|
649
|
+
const latest = await this.repository.findLatestRevision(entityId, ctx);
|
|
650
|
+
if (!(force || reason === "push") && latest && comparableSerialize(latest.snapshot) === comparableSerialize(snapshot)) return latest;
|
|
651
|
+
const revisionActor = actor ?? ctx.actor;
|
|
652
|
+
const organizationId = typeof snapshot.entity.organizationId === "string"
|
|
653
|
+
? snapshot.entity.organizationId
|
|
654
|
+
: resolveOrgIdForCommerce(revisionActor, this.config);
|
|
655
|
+
return this.repository.createRevision({
|
|
656
|
+
organizationId,
|
|
657
|
+
entityId,
|
|
658
|
+
snapshot,
|
|
659
|
+
reason,
|
|
660
|
+
actorId: revisionActor?.userId ?? null,
|
|
661
|
+
actorType: revisionActor?.type ?? null,
|
|
662
|
+
requestId: ctx.requestId,
|
|
663
|
+
}, ctx);
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
async recordEntityRevision(
|
|
667
|
+
entityId: string,
|
|
668
|
+
actor: Actor | null,
|
|
669
|
+
reason: SellableEntityRevisionReason = "update",
|
|
670
|
+
ctx?: TxContext,
|
|
671
|
+
): Promise<Result<SellableEntityRevision>> {
|
|
672
|
+
try {
|
|
673
|
+
return await this.withMutation(actor, ctx, async (txCtx) => {
|
|
674
|
+
const revisionActor = actor ?? txCtx.actor;
|
|
675
|
+
assertPermission(revisionActor, "catalog:update");
|
|
676
|
+
const entity = await this.repository.findEntityById(entityId, txCtx);
|
|
677
|
+
if (!entity) return Err(new CommerceNotFoundError("Entity not found."));
|
|
678
|
+
if (entity.organizationId !== resolveOrgIdForCommerce(revisionActor, this.config)) {
|
|
679
|
+
return Err(new CommerceNotFoundError("Entity not found."));
|
|
680
|
+
}
|
|
681
|
+
return Ok(await this.captureRevision(entityId, revisionActor, reason, txCtx));
|
|
682
|
+
});
|
|
683
|
+
} catch (error) {
|
|
684
|
+
return Err(toCommerceError(error));
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
private restoreDate(value: unknown): Date | null {
|
|
689
|
+
if (value == null) return null;
|
|
690
|
+
return value instanceof Date ? value : new Date(String(value));
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
private async applyRevisionSnapshot(
|
|
694
|
+
entityId: string,
|
|
695
|
+
snapshot: SellableEntityRevisionSnapshot,
|
|
696
|
+
ctx: TxContext,
|
|
697
|
+
): Promise<void> {
|
|
698
|
+
const entity = snapshot.entity as unknown as SellableEntity;
|
|
699
|
+
const entityData: Partial<SellableEntityInsert> = {
|
|
700
|
+
sourceStoreId: entity.sourceStoreId,
|
|
701
|
+
type: entity.type,
|
|
702
|
+
slug: entity.slug,
|
|
703
|
+
status: entity.status,
|
|
704
|
+
isVisible: entity.isVisible,
|
|
705
|
+
taxClass: entity.taxClass,
|
|
706
|
+
metadata: entity.metadata,
|
|
707
|
+
publishedAt: this.restoreDate((snapshot.entity as Record<string, unknown>).publishedAt),
|
|
708
|
+
};
|
|
709
|
+
await this.repository.updateEntity(entityId, entityData, ctx);
|
|
710
|
+
|
|
711
|
+
await this.repository.deleteAttributesByEntityId(entityId, ctx);
|
|
712
|
+
for (const row of snapshot.attributes as unknown as Array<SellableAttributeInsert & { id: string }>) {
|
|
713
|
+
await this.repository.createAttribute({
|
|
714
|
+
id: row.id,
|
|
715
|
+
entityId,
|
|
716
|
+
locale: row.locale,
|
|
717
|
+
title: row.title,
|
|
718
|
+
subtitle: row.subtitle,
|
|
719
|
+
description: row.description,
|
|
720
|
+
richDescription: row.richDescription,
|
|
721
|
+
seoTitle: row.seoTitle,
|
|
722
|
+
seoDescription: row.seoDescription,
|
|
723
|
+
}, ctx);
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
await this.repository.deleteCustomFieldsByEntityId(entityId, ctx);
|
|
727
|
+
for (const row of snapshot.customFields as unknown as Array<SellableCustomFieldInsert & { id: string }>) {
|
|
728
|
+
await this.repository.createCustomField({
|
|
729
|
+
id: row.id,
|
|
730
|
+
entityId,
|
|
731
|
+
fieldName: row.fieldName,
|
|
732
|
+
fieldType: row.fieldType,
|
|
733
|
+
source: row.source,
|
|
734
|
+
status: row.status,
|
|
735
|
+
confidence: row.confidence,
|
|
736
|
+
evidence: row.evidence,
|
|
737
|
+
locale: row.locale,
|
|
738
|
+
approvedAt: this.restoreDate(row.approvedAt),
|
|
739
|
+
approvedBy: row.approvedBy,
|
|
740
|
+
textValue: row.textValue,
|
|
741
|
+
numberValue: row.numberValue,
|
|
742
|
+
booleanValue: row.booleanValue,
|
|
743
|
+
dateValue: this.restoreDate(row.dateValue),
|
|
744
|
+
jsonValue: row.jsonValue,
|
|
745
|
+
createdAt: this.restoreDate(row.createdAt) ?? new Date(),
|
|
746
|
+
updatedAt: this.restoreDate(row.updatedAt) ?? new Date(),
|
|
747
|
+
}, ctx);
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
await this.repository.deleteEntityCategoriesByEntityId(entityId, ctx);
|
|
751
|
+
for (const row of snapshot.categories as unknown as Array<{ categoryId: string; sortOrder: number }>) {
|
|
752
|
+
await this.repository.addEntityToCategory(entityId, row.categoryId, row.sortOrder, ctx);
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
await this.repository.deleteEntityBrandsByEntityId(entityId, ctx);
|
|
756
|
+
for (const row of snapshot.brands as unknown as Array<{ brandId: string; sortOrder: number }>) {
|
|
757
|
+
await this.repository.addEntityToBrand(entityId, row.brandId, row.sortOrder, ctx);
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
// Snapshots written before tags existed carry no key; restoring one must
|
|
761
|
+
// not delete tag links the snapshot never saw.
|
|
762
|
+
if (snapshot.tags) {
|
|
763
|
+
await this.repository.deleteEntityTagsByEntityId(entityId, ctx);
|
|
764
|
+
for (const row of snapshot.tags as unknown as Array<{ tagId: string }>) {
|
|
765
|
+
await this.repository.addEntityTag(entityId, row.tagId, ctx);
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
await this.repository.deleteEntityMediaByEntityId(entityId, ctx);
|
|
770
|
+
for (const row of snapshot.media as unknown as Array<{
|
|
771
|
+
mediaAssetId: string;
|
|
772
|
+
variantId: string | null;
|
|
773
|
+
role: "primary" | "gallery" | "thumbnail" | "video" | "document";
|
|
774
|
+
sortOrder: number;
|
|
775
|
+
}>) {
|
|
776
|
+
await this.repository.createEntityMedia({
|
|
777
|
+
entityId,
|
|
778
|
+
mediaAssetId: row.mediaAssetId,
|
|
779
|
+
role: row.role,
|
|
780
|
+
sortOrder: row.sortOrder,
|
|
781
|
+
variantId: row.variantId,
|
|
782
|
+
}, ctx);
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
async restoreEntityRevision(
|
|
787
|
+
entityId: string,
|
|
788
|
+
revisionId: string,
|
|
789
|
+
actor: Actor | null,
|
|
790
|
+
ctx?: TxContext,
|
|
791
|
+
): Promise<Result<CatalogEntityHydrated>> {
|
|
792
|
+
try {
|
|
793
|
+
return await this.withMutation(actor, ctx, async (txCtx) => {
|
|
794
|
+
const revisionActor = actor ?? txCtx.actor;
|
|
795
|
+
assertPermission(revisionActor, "catalog:update");
|
|
796
|
+
const entity = await this.repository.findEntityById(entityId, txCtx);
|
|
797
|
+
if (!entity) return Err(new CommerceNotFoundError("Entity not found."));
|
|
798
|
+
if (entity.organizationId !== resolveOrgIdForCommerce(revisionActor, this.config)) {
|
|
799
|
+
return Err(new CommerceNotFoundError("Entity not found."));
|
|
800
|
+
}
|
|
801
|
+
if (entity.sourceStoreId != null) assertPermission(revisionActor, "catalog:sync");
|
|
802
|
+
const revision = await this.repository.findRevisionById(entityId, revisionId, txCtx);
|
|
803
|
+
if (!revision) return Err(new CommerceNotFoundError("Revision not found."));
|
|
804
|
+
await this.applyRevisionSnapshot(entityId, revision.snapshot, txCtx);
|
|
805
|
+
const restored = await this.entities.getById(
|
|
806
|
+
entityId,
|
|
807
|
+
{ includeAttributes: true, includeCategories: true, includeBrands: true, includeMedia: true },
|
|
808
|
+
revisionActor,
|
|
809
|
+
txCtx,
|
|
810
|
+
);
|
|
811
|
+
if (!restored.ok) return restored;
|
|
812
|
+
await this.captureRevision(entityId, revisionActor, "restore", txCtx, true);
|
|
813
|
+
return restored;
|
|
814
|
+
});
|
|
815
|
+
} catch (error) {
|
|
816
|
+
return Err(toCommerceError(error));
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
async trimEntityRevisions(
|
|
821
|
+
actor: Actor | null,
|
|
822
|
+
olderThanDays = 90,
|
|
823
|
+
ctx?: TxContext,
|
|
824
|
+
): Promise<Result<number>> {
|
|
825
|
+
try {
|
|
826
|
+
return await this.withMutation(actor, ctx, async (txCtx) => {
|
|
827
|
+
const revisionActor = actor ?? txCtx.actor;
|
|
828
|
+
assertPermission(revisionActor, "catalog:update");
|
|
829
|
+
const cutoff = new Date(Date.now() - olderThanDays * 24 * 60 * 60 * 1000);
|
|
830
|
+
return Ok(await this.repository.deleteRevisionsOlderThan(resolveOrgIdForCommerce(revisionActor, this.config), cutoff, txCtx));
|
|
831
|
+
});
|
|
832
|
+
} catch (error) {
|
|
833
|
+
return Err(toCommerceError(error));
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
|
|
311
837
|
create(input: CreateEntityInput, actor: Actor | null, ctx?: TxContext): Promise<Result<CatalogEntityHydrated>> {
|
|
312
|
-
return this.
|
|
838
|
+
return this.withMutationResult(actor, ctx, async (txCtx) => {
|
|
839
|
+
const result = await this.entities.create(input, actor, txCtx);
|
|
840
|
+
if (result.ok) await this.captureRevision(result.value.id, actor, "create", txCtx);
|
|
841
|
+
return result;
|
|
842
|
+
});
|
|
313
843
|
}
|
|
314
844
|
|
|
315
|
-
update(id: string, input: UpdateEntityInput, actor: Actor | null, ctx?:
|
|
316
|
-
return this.
|
|
845
|
+
update(id: string, input: UpdateEntityInput, actor: Actor | null, ctx?: CatalogWriteContext): Promise<Result<CatalogEntityHydrated>> {
|
|
846
|
+
return this.withMutationResult(actor, ctx, async (txCtx) => {
|
|
847
|
+
const result = await this.entities.update(id, input, actor, txCtx);
|
|
848
|
+
if (result.ok) await this.captureRevision(id, actor, "update", txCtx);
|
|
849
|
+
return result;
|
|
850
|
+
});
|
|
317
851
|
}
|
|
318
852
|
|
|
319
853
|
delete(id: string, actor: Actor | null, ctx?: TxContext): Promise<Result<void>> {
|
|
320
854
|
return this.entities.delete(id, actor, ctx);
|
|
321
855
|
}
|
|
322
856
|
|
|
323
|
-
getById(id: string, options
|
|
857
|
+
getById(id: string, options: GetOptions | undefined, actor: Actor | null, ctx?: TxContext): Promise<Result<CatalogEntityHydrated>> {
|
|
324
858
|
return this.entities.getById(id, options, actor, ctx);
|
|
325
859
|
}
|
|
326
860
|
|
|
327
|
-
|
|
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>> {
|
|
328
866
|
return this.entities.getBySlug(slug, options, actor, ctx);
|
|
329
867
|
}
|
|
330
868
|
|
|
331
|
-
list(params: ListParams, actor
|
|
869
|
+
list(params: ListParams, actor: Actor | null, ctx?: TxContext): Promise<Result<CatalogListResult>> {
|
|
332
870
|
return this.entities.list(params, actor, ctx);
|
|
333
871
|
}
|
|
334
872
|
|
|
873
|
+
async resolveFieldOwner(
|
|
874
|
+
entityId: string,
|
|
875
|
+
fieldPath: FieldPath,
|
|
876
|
+
storeId: string,
|
|
877
|
+
ctx?: TxContext,
|
|
878
|
+
variantId?: string | null,
|
|
879
|
+
): Promise<FieldOwner | undefined> {
|
|
880
|
+
if (!isValidFieldPath(fieldPath)) return undefined;
|
|
881
|
+
const rows = await this.repository.findFieldOwnershipForResolution(entityId, storeId, variantId ?? null, ctx);
|
|
882
|
+
let selected: CatalogFieldOwnership | undefined;
|
|
883
|
+
let selectedPriority = -1;
|
|
884
|
+
for (const row of rows) {
|
|
885
|
+
if (row.fieldPath !== fieldPath) continue;
|
|
886
|
+
const priority = (row.variantId === variantId && variantId != null ? 2 : 0)
|
|
887
|
+
+ (row.storeId === storeId ? 1 : 0);
|
|
888
|
+
if (priority > selectedPriority) {
|
|
889
|
+
selected = row;
|
|
890
|
+
selectedPriority = priority;
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
return selected?.owner;
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
async resolveFieldOwners(
|
|
897
|
+
entityId: string,
|
|
898
|
+
storeId: string,
|
|
899
|
+
ctx?: TxContext,
|
|
900
|
+
): Promise<Map<FieldPath, FieldOwner>> {
|
|
901
|
+
const rows = await this.repository.findFieldOwnershipForResolution(entityId, storeId, null, ctx);
|
|
902
|
+
const resolved = new Map<FieldPath, { owner: FieldOwner; priority: number }>();
|
|
903
|
+
for (const row of rows) {
|
|
904
|
+
const priority = row.storeId === storeId ? 1 : 0;
|
|
905
|
+
const current = resolved.get(row.fieldPath);
|
|
906
|
+
if (!current || priority > current.priority) {
|
|
907
|
+
resolved.set(row.fieldPath, { owner: row.owner, priority });
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
return new Map([...resolved].map(([fieldPath, value]) => [fieldPath, value.owner]));
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
setFieldOwner(
|
|
914
|
+
entityId: string,
|
|
915
|
+
fieldPath: FieldPath,
|
|
916
|
+
storeId: string | null,
|
|
917
|
+
owner: FieldOwner,
|
|
918
|
+
actor: Actor | null,
|
|
919
|
+
ctx?: TxContext,
|
|
920
|
+
variantId?: string | null,
|
|
921
|
+
): Promise<Result<void>> {
|
|
922
|
+
return this.withMutationResult(actor, ctx, async (txCtx) => {
|
|
923
|
+
const setter = actor ?? txCtx.actor;
|
|
924
|
+
assertPermission(setter, "catalog:update");
|
|
925
|
+
validateFieldPath(fieldPath);
|
|
926
|
+
const entity = await this.repository.findEntityById(entityId, txCtx);
|
|
927
|
+
if (!entity) return Err(new CommerceNotFoundError("Entity not found."));
|
|
928
|
+
if (entity.organizationId !== resolveOrgIdForCommerce(setter, this.config)) {
|
|
929
|
+
return Err(new CommerceNotFoundError("Entity not found."));
|
|
930
|
+
}
|
|
931
|
+
if (entity.sourceStoreId != null) assertPermission(setter, "catalog:sync");
|
|
932
|
+
await this.repository.upsertFieldOwnership({
|
|
933
|
+
organizationId: entity.organizationId,
|
|
934
|
+
entityId,
|
|
935
|
+
...(variantId != null ? { variantId } : {}),
|
|
936
|
+
...(storeId != null ? { storeId } : {}),
|
|
937
|
+
fieldPath,
|
|
938
|
+
owner,
|
|
939
|
+
}, txCtx);
|
|
940
|
+
return Ok(undefined);
|
|
941
|
+
});
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
listFieldOwnership(
|
|
945
|
+
entityId: string,
|
|
946
|
+
actor: Actor | null,
|
|
947
|
+
ctx?: TxContext,
|
|
948
|
+
storeId?: string,
|
|
949
|
+
): Promise<Result<CatalogFieldOwnership[]>> {
|
|
950
|
+
return this.withMutationResult(actor, ctx, async (txCtx) => {
|
|
951
|
+
const reader = actor ?? txCtx.actor;
|
|
952
|
+
assertPermission(reader, "catalog:read");
|
|
953
|
+
const entity = await this.repository.findEntityById(entityId, txCtx);
|
|
954
|
+
if (!entity) return Err(new CommerceNotFoundError("Entity not found."));
|
|
955
|
+
if (entity.organizationId !== resolveOrgIdForCommerce(reader, this.config)) {
|
|
956
|
+
return Err(new CommerceNotFoundError("Entity not found."));
|
|
957
|
+
}
|
|
958
|
+
return Ok(await this.repository.findFieldOwnership(entityId, txCtx, storeId));
|
|
959
|
+
});
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
seedImportedFieldOwnership(
|
|
963
|
+
entityId: string,
|
|
964
|
+
storeId: string,
|
|
965
|
+
fieldPaths: FieldPath[],
|
|
966
|
+
ctx?: TxContext,
|
|
967
|
+
): Promise<Result<void>> {
|
|
968
|
+
return this.withMutationResult(null, ctx, async (txCtx) => {
|
|
969
|
+
const entity = await this.repository.findEntityById(entityId, txCtx);
|
|
970
|
+
if (!entity) return Err(new CommerceNotFoundError("Entity not found."));
|
|
971
|
+
const rows = fieldPaths.map((fieldPath) => ({
|
|
972
|
+
organizationId: entity.organizationId,
|
|
973
|
+
entityId,
|
|
974
|
+
storeId,
|
|
975
|
+
fieldPath: validateFieldPath(fieldPath),
|
|
976
|
+
owner: "store" as const,
|
|
977
|
+
}));
|
|
978
|
+
await this.repository.seedFieldOwnership(rows, txCtx);
|
|
979
|
+
return Ok(undefined);
|
|
980
|
+
});
|
|
981
|
+
}
|
|
982
|
+
|
|
335
983
|
publish(id: string, actor: Actor | null, ctx?: TxContext): Promise<Result<CatalogEntityHydrated>> {
|
|
336
|
-
return this.
|
|
984
|
+
return this.withMutationResult(actor, ctx, async (txCtx) => {
|
|
985
|
+
const result = await this.entities.publish(id, actor, txCtx);
|
|
986
|
+
if (result.ok) await this.captureRevision(id, actor, "update", txCtx);
|
|
987
|
+
return result;
|
|
988
|
+
});
|
|
337
989
|
}
|
|
338
990
|
|
|
339
991
|
archive(id: string, actor: Actor | null, ctx?: TxContext): Promise<Result<CatalogEntityHydrated>> {
|
|
340
|
-
return this.
|
|
992
|
+
return this.withMutationResult(actor, ctx, async (txCtx) => {
|
|
993
|
+
const result = await this.entities.archive(id, actor, txCtx);
|
|
994
|
+
if (result.ok) await this.captureRevision(id, actor, "update", txCtx);
|
|
995
|
+
return result;
|
|
996
|
+
});
|
|
341
997
|
}
|
|
342
998
|
|
|
343
999
|
discontinue(id: string, actor: Actor | null, ctx?: TxContext): Promise<Result<CatalogEntityHydrated>> {
|
|
344
|
-
return this.
|
|
1000
|
+
return this.withMutationResult(actor, ctx, async (txCtx) => {
|
|
1001
|
+
const result = await this.entities.discontinue(id, actor, txCtx);
|
|
1002
|
+
if (result.ok) await this.captureRevision(id, actor, "update", txCtx);
|
|
1003
|
+
return result;
|
|
1004
|
+
});
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
setAttributes(entityId: string, locale: string, attrs: SetAttributesInput, actor: Actor | null, ctx?: CatalogWriteContext): Promise<Result<void>> {
|
|
1008
|
+
return this.withMutationResult(actor, ctx, async (txCtx) => {
|
|
1009
|
+
const result = await this.entities.setAttributes(entityId, locale, attrs, actor, txCtx);
|
|
1010
|
+
if (result.ok) await this.captureRevision(entityId, actor, "update", txCtx);
|
|
1011
|
+
return result;
|
|
1012
|
+
});
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
private reviewCustomField(
|
|
1016
|
+
status: "approved" | "rejected",
|
|
1017
|
+
entityId: string,
|
|
1018
|
+
fieldName: string,
|
|
1019
|
+
locale: string,
|
|
1020
|
+
actor: Actor | null,
|
|
1021
|
+
ctx?: TxContext,
|
|
1022
|
+
): Promise<Result<SellableCustomField>> {
|
|
1023
|
+
return this.withMutationResult(actor, ctx, async (txCtx) => {
|
|
1024
|
+
const reviewer = actor ?? txCtx.actor;
|
|
1025
|
+
assertPermission(reviewer, "catalog:update");
|
|
1026
|
+
const entity = await this.repository.findEntityById(entityId, txCtx);
|
|
1027
|
+
if (!entity) return Err(new CommerceNotFoundError("Entity not found."));
|
|
1028
|
+
if (entity.organizationId !== resolveOrgIdForCommerce(reviewer, this.config)) {
|
|
1029
|
+
return Err(new CommerceNotFoundError("Entity not found."));
|
|
1030
|
+
}
|
|
1031
|
+
if (entity.sourceStoreId != null) assertPermission(reviewer, "catalog:sync");
|
|
1032
|
+
const proposal = await this.repository.findProposedCustomField(entityId, fieldName, locale, txCtx);
|
|
1033
|
+
if (!proposal) return Err(new CommerceNotFoundError("Custom field proposal not found."));
|
|
1034
|
+
if (status === "approved") {
|
|
1035
|
+
await this.repository.deleteCustomField(entityId, fieldName, locale, txCtx);
|
|
1036
|
+
}
|
|
1037
|
+
const updated = await this.repository.updateProposedCustomField(proposal.id, {
|
|
1038
|
+
status,
|
|
1039
|
+
...(status === "approved"
|
|
1040
|
+
? { approvedAt: new Date(), approvedBy: reviewer?.userId ?? null }
|
|
1041
|
+
: {}),
|
|
1042
|
+
}, txCtx);
|
|
1043
|
+
// Throw, never return Err, past this point: the approved-row delete above
|
|
1044
|
+
// must roll back when the proposal was already resolved by a concurrent
|
|
1045
|
+
// reviewer, or the live value is lost with no replacement.
|
|
1046
|
+
if (!updated) throw new CommerceNotFoundError("Custom field proposal not found.");
|
|
1047
|
+
if (status === "approved") {
|
|
1048
|
+
await this.repository.rejectOtherProposedCustomFields(entityId, fieldName, locale, updated.id, txCtx);
|
|
1049
|
+
}
|
|
1050
|
+
await this.captureRevision(entityId, reviewer, "update", txCtx);
|
|
1051
|
+
return Ok(updated);
|
|
1052
|
+
});
|
|
345
1053
|
}
|
|
346
1054
|
|
|
347
|
-
|
|
348
|
-
|
|
1055
|
+
approveCustomField(
|
|
1056
|
+
entityId: string,
|
|
1057
|
+
fieldName: string,
|
|
1058
|
+
locale: string,
|
|
1059
|
+
actor: Actor | null,
|
|
1060
|
+
ctx?: TxContext,
|
|
1061
|
+
): Promise<Result<SellableCustomField>> {
|
|
1062
|
+
return this.reviewCustomField("approved", entityId, fieldName, locale, actor, ctx);
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
rejectCustomField(
|
|
1066
|
+
entityId: string,
|
|
1067
|
+
fieldName: string,
|
|
1068
|
+
locale: string,
|
|
1069
|
+
actor: Actor | null,
|
|
1070
|
+
ctx?: TxContext,
|
|
1071
|
+
): Promise<Result<SellableCustomField>> {
|
|
1072
|
+
return this.reviewCustomField("rejected", entityId, fieldName, locale, actor, ctx);
|
|
349
1073
|
}
|
|
350
1074
|
|
|
351
|
-
getAttributes(entityId: string, locale: string, ctx?: TxContext): Promise<Result<SellableAttribute>> {
|
|
352
|
-
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);
|
|
353
1077
|
}
|
|
354
1078
|
|
|
355
1079
|
listCategories(ctx?: TxContext, opts?: { includeArchived?: boolean }): Promise<Result<CategorySummary[]>> {
|
|
@@ -377,11 +1101,19 @@ export class CatalogServiceImpl implements CatalogService {
|
|
|
377
1101
|
}
|
|
378
1102
|
|
|
379
1103
|
addToCategory(entityId: string, categoryId: string, actor: Actor | null, ctx?: TxContext): Promise<Result<void>> {
|
|
380
|
-
return this.
|
|
1104
|
+
return this.withMutationResult(actor, ctx, async (txCtx) => {
|
|
1105
|
+
const result = await this.categories.addToCategory(entityId, categoryId, actor, txCtx);
|
|
1106
|
+
if (result.ok) await this.captureRevision(entityId, actor, "update", txCtx);
|
|
1107
|
+
return result;
|
|
1108
|
+
});
|
|
381
1109
|
}
|
|
382
1110
|
|
|
383
1111
|
removeFromCategory(entityId: string, categoryId: string, actor: Actor | null, ctx?: TxContext): Promise<Result<void>> {
|
|
384
|
-
return this.
|
|
1112
|
+
return this.withMutationResult(actor, ctx, async (txCtx) => {
|
|
1113
|
+
const result = await this.categories.removeFromCategory(entityId, categoryId, actor, txCtx);
|
|
1114
|
+
if (result.ok) await this.captureRevision(entityId, actor, "update", txCtx);
|
|
1115
|
+
return result;
|
|
1116
|
+
});
|
|
385
1117
|
}
|
|
386
1118
|
|
|
387
1119
|
listBrands(ctx?: TxContext): Promise<Result<Brand[]>> {
|
|
@@ -401,11 +1133,19 @@ export class CatalogServiceImpl implements CatalogService {
|
|
|
401
1133
|
}
|
|
402
1134
|
|
|
403
1135
|
addToBrand(entityId: string, brandId: string, actor: Actor | null, ctx?: TxContext): Promise<Result<void>> {
|
|
404
|
-
return this.
|
|
1136
|
+
return this.withMutationResult(actor, ctx, async (txCtx) => {
|
|
1137
|
+
const result = await this.brands.addToBrand(entityId, brandId, actor, txCtx);
|
|
1138
|
+
if (result.ok) await this.captureRevision(entityId, actor, "update", txCtx);
|
|
1139
|
+
return result;
|
|
1140
|
+
});
|
|
405
1141
|
}
|
|
406
1142
|
|
|
407
1143
|
removeFromBrand(entityId: string, brandId: string, actor: Actor | null, ctx?: TxContext): Promise<Result<void>> {
|
|
408
|
-
return this.
|
|
1144
|
+
return this.withMutationResult(actor, ctx, async (txCtx) => {
|
|
1145
|
+
const result = await this.brands.removeFromBrand(entityId, brandId, actor, txCtx);
|
|
1146
|
+
if (result.ok) await this.captureRevision(entityId, actor, "update", txCtx);
|
|
1147
|
+
return result;
|
|
1148
|
+
});
|
|
409
1149
|
}
|
|
410
1150
|
|
|
411
1151
|
createOptionType(input: CreateOptionTypeInput, actor: Actor | null, ctx?: TxContext): Promise<Result<OptionType>> {
|