@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,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { resolveOrgIdForCommerce } from "../../auth/org.js";
|
|
2
2
|
import { assertPermission } from "../../auth/permissions.js";
|
|
3
3
|
import { CommerceConflictError, CommerceNotFoundError, CommerceValidationError, toCommerceError, } from "../../kernel/errors.js";
|
|
4
4
|
import { mergeHookReports, runAfterHooks, runBeforeHooks, } from "../../kernel/hooks/executor.js";
|
|
@@ -6,9 +6,25 @@ import { createHookContext } from "../../kernel/hooks/create-context.js";
|
|
|
6
6
|
import { Err, Ok } from "../../kernel/result.js";
|
|
7
7
|
import { createLogger } from "../../utils/logger.js";
|
|
8
8
|
import { paginate } from "../../utils/pagination.js";
|
|
9
|
+
import { isWriteContextTransactional } from "../../kernel/database/tx-context.js";
|
|
10
|
+
import { canReadUnpublishedCatalog, isCatalogEntityVisible } from "./read-policy.js";
|
|
11
|
+
import { isValidFieldPath } from "./ownership.js";
|
|
9
12
|
function hookDatabaseArg(database) {
|
|
10
13
|
return { database: { db: database.db } };
|
|
11
14
|
}
|
|
15
|
+
const attributeFields = ["title", "subtitle", "description", "richDescription", "seoTitle", "seoDescription"];
|
|
16
|
+
function catalogHookContext(deps, actor, ctx, operation) {
|
|
17
|
+
return createHookContext({
|
|
18
|
+
actor,
|
|
19
|
+
tx: isWriteContextTransactional(ctx) ? ctx.tx : null,
|
|
20
|
+
logger: createLogger(`catalog.${operation}`),
|
|
21
|
+
services: deps.services,
|
|
22
|
+
...(deps.services.jobs ? { jobs: deps.services.jobs } : {}),
|
|
23
|
+
context: { moduleName: "catalog", ...(ctx?.hookContext ?? {}) },
|
|
24
|
+
...hookDatabaseArg(deps.database),
|
|
25
|
+
commerceConfig: deps.config,
|
|
26
|
+
});
|
|
27
|
+
}
|
|
12
28
|
function cartesian(arrays) {
|
|
13
29
|
return arrays.reduce((acc, values) => acc.flatMap((entry) => values.map((value) => [...entry, value])), [[]]);
|
|
14
30
|
}
|
|
@@ -16,6 +32,7 @@ function getCustomFieldValue(field) {
|
|
|
16
32
|
switch (field.fieldType) {
|
|
17
33
|
case "text":
|
|
18
34
|
case "relation":
|
|
35
|
+
case "select":
|
|
19
36
|
return field.textValue;
|
|
20
37
|
case "number":
|
|
21
38
|
return field.numberValue;
|
|
@@ -31,8 +48,10 @@ function getCustomFieldValue(field) {
|
|
|
31
48
|
}
|
|
32
49
|
export class EntityService {
|
|
33
50
|
deps;
|
|
34
|
-
|
|
51
|
+
resolveEntityFieldDefinitions;
|
|
52
|
+
constructor(deps, resolveEntityFieldDefinitions) {
|
|
35
53
|
this.deps = deps;
|
|
54
|
+
this.resolveEntityFieldDefinitions = resolveEntityFieldDefinitions;
|
|
36
55
|
}
|
|
37
56
|
get repo() {
|
|
38
57
|
return this.deps.repository;
|
|
@@ -40,7 +59,7 @@ export class EntityService {
|
|
|
40
59
|
assertSameOrg(resource, actor) {
|
|
41
60
|
if (!resource)
|
|
42
61
|
return;
|
|
43
|
-
const orgId =
|
|
62
|
+
const orgId = resolveOrgIdForCommerce(actor, this.deps.config);
|
|
44
63
|
if (resource.organizationId && resource.organizationId !== orgId) {
|
|
45
64
|
throw new CommerceNotFoundError("Entity not found.");
|
|
46
65
|
}
|
|
@@ -49,25 +68,42 @@ export class EntityService {
|
|
|
49
68
|
if (entity.sourceStoreId != null)
|
|
50
69
|
assertPermission(actor, "catalog:sync");
|
|
51
70
|
}
|
|
52
|
-
async
|
|
71
|
+
async validateCustomFields(entityType, customFields, actor, ctx) {
|
|
53
72
|
if (!customFields)
|
|
54
|
-
return Ok(
|
|
73
|
+
return Ok([]);
|
|
55
74
|
const entityConfig = this.deps.config.entities?.[entityType];
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
75
|
+
const definitions = await this.resolveEntityFieldDefinitions(entityType, actor ?? ctx?.actor ?? null, ctx);
|
|
76
|
+
if (!entityConfig && definitions.length === 0)
|
|
77
|
+
return Ok([]);
|
|
78
|
+
const definitionMap = new Map(definitions.map((f) => [f.name, f]));
|
|
79
|
+
const validated = [];
|
|
59
80
|
for (const [name, value] of Object.entries(customFields)) {
|
|
60
81
|
const def = definitionMap.get(name);
|
|
61
82
|
if (!def)
|
|
62
83
|
return Err(new CommerceValidationError(`Unknown custom field: ${name}`));
|
|
84
|
+
if (value === null) {
|
|
85
|
+
validated.push({ fieldName: name, locale: "en", data: null });
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
63
88
|
const type = def.type;
|
|
89
|
+
let normalized = value;
|
|
64
90
|
let valid = false;
|
|
65
91
|
switch (type) {
|
|
66
92
|
case "text":
|
|
67
93
|
case "relation":
|
|
68
|
-
case "select":
|
|
69
94
|
valid = typeof value === "string";
|
|
70
95
|
break;
|
|
96
|
+
case "select": {
|
|
97
|
+
if (typeof value !== "string")
|
|
98
|
+
break;
|
|
99
|
+
const trimmed = value.trim();
|
|
100
|
+
if (def.options && !def.options.includes(trimmed)) {
|
|
101
|
+
return Err(new CommerceValidationError(`Value "${trimmed}" is not permitted for field ${name}. Allowed: ${def.options.join(", ")}`));
|
|
102
|
+
}
|
|
103
|
+
normalized = trimmed;
|
|
104
|
+
valid = true;
|
|
105
|
+
break;
|
|
106
|
+
}
|
|
71
107
|
case "number":
|
|
72
108
|
valid = typeof value === "number";
|
|
73
109
|
break;
|
|
@@ -85,29 +121,51 @@ export class EntityService {
|
|
|
85
121
|
}
|
|
86
122
|
if (!valid)
|
|
87
123
|
return Err(new CommerceValidationError(`Custom field ${name} expected type ${type}.`));
|
|
88
|
-
const fieldType =
|
|
89
|
-
const insertData = {
|
|
124
|
+
const fieldType = type;
|
|
125
|
+
const insertData = {
|
|
126
|
+
fieldType,
|
|
127
|
+
source: "merchant",
|
|
128
|
+
status: "approved",
|
|
129
|
+
confidence: null,
|
|
130
|
+
evidence: null,
|
|
131
|
+
approvedAt: null,
|
|
132
|
+
approvedBy: null,
|
|
133
|
+
};
|
|
90
134
|
switch (fieldType) {
|
|
91
135
|
case "text":
|
|
92
136
|
case "relation":
|
|
93
|
-
|
|
137
|
+
case "select":
|
|
138
|
+
insertData.textValue = normalized;
|
|
94
139
|
break;
|
|
95
140
|
case "number":
|
|
96
|
-
insertData.numberValue =
|
|
141
|
+
insertData.numberValue = normalized;
|
|
97
142
|
break;
|
|
98
143
|
case "boolean":
|
|
99
|
-
insertData.booleanValue =
|
|
144
|
+
insertData.booleanValue = normalized;
|
|
100
145
|
break;
|
|
101
146
|
case "date":
|
|
102
|
-
insertData.dateValue =
|
|
147
|
+
insertData.dateValue = normalized instanceof Date ? normalized : new Date(normalized);
|
|
103
148
|
break;
|
|
104
149
|
case "json":
|
|
105
|
-
insertData.jsonValue =
|
|
150
|
+
insertData.jsonValue = normalized;
|
|
106
151
|
break;
|
|
107
152
|
}
|
|
108
|
-
|
|
153
|
+
validated.push({ fieldName: name, locale: "en", data: insertData });
|
|
154
|
+
}
|
|
155
|
+
return Ok(validated);
|
|
156
|
+
}
|
|
157
|
+
async writeCustomFields(entityId, customFields, ctx, upsert = false) {
|
|
158
|
+
for (const customField of customFields) {
|
|
159
|
+
if (customField.data === null) {
|
|
160
|
+
await this.repo.deleteCustomField(entityId, customField.fieldName, customField.locale, ctx);
|
|
161
|
+
}
|
|
162
|
+
else if (upsert) {
|
|
163
|
+
await this.repo.upsertCustomField(entityId, customField.fieldName, customField.locale, customField.data, ctx);
|
|
164
|
+
}
|
|
165
|
+
else {
|
|
166
|
+
await this.repo.createCustomField({ ...customField.data, entityId, fieldName: customField.fieldName, locale: customField.locale }, ctx);
|
|
167
|
+
}
|
|
109
168
|
}
|
|
110
|
-
return Ok(undefined);
|
|
111
169
|
}
|
|
112
170
|
async hydrateEntity(entity, options, ctx) {
|
|
113
171
|
const hydrated = { ...entity };
|
|
@@ -138,9 +196,11 @@ export class EntityService {
|
|
|
138
196
|
hydrated.categories = await this.repo.findEntityCategories(entity.id, ctx);
|
|
139
197
|
if (options?.includeBrands)
|
|
140
198
|
hydrated.brands = await this.repo.findEntityBrands(entity.id, ctx);
|
|
199
|
+
if (options?.includeCustomFields)
|
|
200
|
+
hydrated.customFields = await this.repo.findCustomFieldsByEntityId(entity.id, ctx);
|
|
141
201
|
if (options?.includeMedia) {
|
|
142
202
|
const mediaService = this.deps.services.media;
|
|
143
|
-
const mediaResult = await mediaService?.listEntityMedia?.(entity.id, { orgId: entity.organizationId });
|
|
203
|
+
const mediaResult = await mediaService?.listEntityMedia?.(entity.id, { orgId: entity.organizationId }, ctx);
|
|
144
204
|
hydrated.media = mediaResult?.ok && mediaResult.value ? mediaResult.value : [];
|
|
145
205
|
}
|
|
146
206
|
if (options?.includePricing) {
|
|
@@ -158,23 +218,26 @@ export class EntityService {
|
|
|
158
218
|
async create(input, actor, ctx) {
|
|
159
219
|
try {
|
|
160
220
|
assertPermission(actor, "catalog:create");
|
|
161
|
-
const orgId =
|
|
221
|
+
const orgId = resolveOrgIdForCommerce(actor, this.deps.config);
|
|
162
222
|
const existingBySlug = await this.repo.findEntityBySlug(orgId, input.slug, ctx);
|
|
163
223
|
if (existingBySlug)
|
|
164
224
|
return Err(new CommerceConflictError(`Entity with slug ${input.slug} already exists.`));
|
|
165
225
|
const beforeHooks = this.deps.hooks.resolve("catalog.beforeCreate");
|
|
166
226
|
const afterHooks = this.deps.hooks.resolve("catalog.afterCreate");
|
|
167
|
-
const context = createHookContext({ actor, tx: ctx?.tx ?? null, logger: createLogger("catalog.create"), services: this.deps.services, context: { moduleName: "catalog" }, ...hookDatabaseArg(this.deps.database) });
|
|
227
|
+
const context = 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 });
|
|
168
228
|
const processedInput = await runBeforeHooks(beforeHooks, input, "create", context);
|
|
169
229
|
if (processedInput.sourceStoreId != null)
|
|
170
230
|
assertPermission(actor, "catalog:sync");
|
|
171
|
-
const
|
|
231
|
+
const customFieldsResult = await this.validateCustomFields(processedInput.type, processedInput.customFields, actor, ctx);
|
|
232
|
+
if (!customFieldsResult.ok)
|
|
233
|
+
return customFieldsResult;
|
|
234
|
+
const statuses = ["draft", "active", "archived", "discontinued"];
|
|
235
|
+
const status = statuses.find((value) => value === processedInput.status) ?? "draft";
|
|
236
|
+
const entity = await this.repo.createEntity({ organizationId: orgId, type: processedInput.type, slug: processedInput.slug, status, isVisible: status === "active", ...(processedInput.sourceStoreId !== undefined ? { sourceStoreId: processedInput.sourceStoreId } : {}), ...(processedInput.taxClass !== undefined ? { taxClass: processedInput.taxClass } : {}), metadata: processedInput.metadata ?? {} }, ctx);
|
|
172
237
|
if (processedInput.attributes) {
|
|
173
238
|
await this.repo.createAttribute({ entityId: entity.id, locale: processedInput.attributes.locale ?? "en", title: processedInput.attributes.title, subtitle: processedInput.attributes.subtitle, description: processedInput.attributes.description, richDescription: processedInput.attributes.richDescription, seoTitle: processedInput.attributes.seoTitle, seoDescription: processedInput.attributes.seoDescription }, ctx);
|
|
174
239
|
}
|
|
175
|
-
|
|
176
|
-
if (!customFieldsResult.ok)
|
|
177
|
-
return customFieldsResult;
|
|
240
|
+
await this.writeCustomFields(entity.id, customFieldsResult.value, ctx);
|
|
178
241
|
const hookReport = await runAfterHooks(afterHooks, null, entity, "create", context);
|
|
179
242
|
const hydrated = await this.hydrateEntity(entity, undefined, ctx);
|
|
180
243
|
return Ok(hydrated, hookReport.hasErrors ? { hookErrors: hookReport.errors } : undefined);
|
|
@@ -185,21 +248,26 @@ export class EntityService {
|
|
|
185
248
|
}
|
|
186
249
|
async update(id, input, actor, ctx) {
|
|
187
250
|
try {
|
|
251
|
+
const txCtx = isWriteContextTransactional(ctx) ? ctx : undefined;
|
|
188
252
|
assertPermission(actor, "catalog:update");
|
|
189
|
-
const existing = await this.repo.findEntityById(id,
|
|
253
|
+
const existing = await this.repo.findEntityById(id, txCtx);
|
|
190
254
|
if (!existing)
|
|
191
255
|
return Err(new CommerceNotFoundError("Entity not found."));
|
|
192
256
|
this.assertSameOrg(existing, actor);
|
|
193
257
|
this.assertEntityWritable(existing, actor);
|
|
194
258
|
const beforeHooks = this.deps.hooks.resolve("catalog.beforeUpdate");
|
|
195
259
|
const afterHooks = this.deps.hooks.resolve("catalog.afterUpdate");
|
|
196
|
-
const context =
|
|
260
|
+
const context = catalogHookContext(this.deps, actor, ctx, "update");
|
|
197
261
|
const processed = await runBeforeHooks(beforeHooks, input, "update", context);
|
|
198
|
-
const
|
|
262
|
+
const customFieldsResult = await this.validateCustomFields(existing.type, processed.customFields, actor, txCtx);
|
|
263
|
+
if (!customFieldsResult.ok)
|
|
264
|
+
return customFieldsResult;
|
|
265
|
+
const updated = await this.repo.updateEntity(id, { ...(processed.slug !== undefined ? { slug: processed.slug } : {}), ...(processed.status !== undefined ? { status: processed.status } : {}), ...(processed.taxClass !== undefined ? { taxClass: processed.taxClass } : {}), ...(processed.metadata !== undefined ? { metadata: processed.metadata } : {}), ...(processed.isVisible !== undefined ? { isVisible: processed.isVisible } : {}) }, txCtx);
|
|
199
266
|
if (!updated)
|
|
200
267
|
return Err(new CommerceNotFoundError("Entity not found."));
|
|
268
|
+
await this.writeCustomFields(existing.id, customFieldsResult.value, txCtx, true);
|
|
201
269
|
const hookReport = await runAfterHooks(afterHooks, existing, updated, "update", context);
|
|
202
|
-
const hydrated = await this.hydrateEntity(updated, undefined,
|
|
270
|
+
const hydrated = await this.hydrateEntity(updated, undefined, txCtx);
|
|
203
271
|
return Ok(hydrated, hookReport.hasErrors ? { hookErrors: hookReport.errors } : undefined);
|
|
204
272
|
}
|
|
205
273
|
catch (error) {
|
|
@@ -228,25 +296,30 @@ export class EntityService {
|
|
|
228
296
|
}
|
|
229
297
|
}
|
|
230
298
|
async getById(id, options, actor, ctx) {
|
|
231
|
-
const
|
|
299
|
+
const resolvedActor = actor ?? ctx?.actor ?? null;
|
|
300
|
+
const orgId = resolveOrgIdForCommerce(resolvedActor, this.deps.config);
|
|
301
|
+
const context = 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 });
|
|
232
302
|
const globalBeforeHooks = this.deps.hooks.resolve("catalog.beforeRead");
|
|
233
303
|
const globalAfterHooks = this.deps.hooks.resolve("catalog.afterRead");
|
|
234
304
|
let processed = await runBeforeHooks(globalBeforeHooks, { id, ...(options !== undefined ? { options } : {}) }, "read", context);
|
|
235
|
-
let entity = await this.repo.findEntityById(processed.id ?? id, ctx);
|
|
305
|
+
let entity = await this.repo.findEntityById(processed.id ?? id, ctx, orgId);
|
|
236
306
|
if (!entity)
|
|
237
307
|
return Err(new CommerceNotFoundError("Entity not found."));
|
|
308
|
+
if (!isCatalogEntityVisible(entity, resolvedActor))
|
|
309
|
+
return Err(new CommerceNotFoundError("Entity not found."));
|
|
238
310
|
const entityBeforeHooks = this.deps.hooks.resolve(`catalog.${entity.type}.beforeRead`);
|
|
239
311
|
if (entityBeforeHooks.length > 0) {
|
|
240
312
|
processed = await runBeforeHooks(entityBeforeHooks, { ...processed, id: entity.id }, "read", context);
|
|
241
313
|
const resolvedId = processed.id ?? entity.id;
|
|
242
314
|
if (resolvedId !== entity.id) {
|
|
243
|
-
const refetched = await this.repo.findEntityById(resolvedId, ctx);
|
|
315
|
+
const refetched = await this.repo.findEntityById(resolvedId, ctx, orgId);
|
|
244
316
|
if (!refetched)
|
|
245
317
|
return Err(new CommerceNotFoundError("Entity not found."));
|
|
318
|
+
if (!isCatalogEntityVisible(refetched, resolvedActor))
|
|
319
|
+
return Err(new CommerceNotFoundError("Entity not found."));
|
|
246
320
|
entity = refetched;
|
|
247
321
|
}
|
|
248
322
|
}
|
|
249
|
-
const resolvedActor = actor ?? ctx?.actor ?? null;
|
|
250
323
|
try {
|
|
251
324
|
this.assertSameOrg(entity, resolvedActor);
|
|
252
325
|
}
|
|
@@ -258,25 +331,41 @@ export class EntityService {
|
|
|
258
331
|
const report = mergeHookReports(await runAfterHooks(globalAfterHooks, null, result, "read", context), await runAfterHooks(entityAfterHooks, null, result, "read", context));
|
|
259
332
|
return Ok(result, report.hasErrors ? { hookErrors: report.errors } : undefined);
|
|
260
333
|
}
|
|
334
|
+
/**
|
|
335
|
+
* Reads an entity for a trusted server pipeline after the request boundary
|
|
336
|
+
* has already authorized the caller. Organization scope is explicit; the
|
|
337
|
+
* storefront visibility policy is intentionally not applied, so an internal
|
|
338
|
+
* invariant check never depends on the caller's catalog permissions.
|
|
339
|
+
*/
|
|
340
|
+
async getByIdForInternalUse(id, organizationId, options, ctx) {
|
|
341
|
+
const entity = await this.repo.findEntityById(id, ctx, organizationId);
|
|
342
|
+
if (!entity)
|
|
343
|
+
return Err(new CommerceNotFoundError("Entity not found."));
|
|
344
|
+
return Ok(await this.hydrateEntity(entity, options, ctx));
|
|
345
|
+
}
|
|
261
346
|
async getBySlug(slug, options, actor, ctx) {
|
|
262
|
-
const context = createHookContext({ actor: actor ?? null, tx: ctx?.tx ?? null, logger: createLogger("catalog.read"), services: this.deps.services, context: { moduleName: "catalog" }, ...hookDatabaseArg(this.deps.database) });
|
|
347
|
+
const context = 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 });
|
|
263
348
|
const globalBeforeHooks = this.deps.hooks.resolve("catalog.beforeRead");
|
|
264
349
|
const globalAfterHooks = this.deps.hooks.resolve("catalog.afterRead");
|
|
265
350
|
let processed = await runBeforeHooks(globalBeforeHooks, { slug, ...(options !== undefined ? { options } : {}) }, "read", context);
|
|
266
351
|
let resolvedSlug = processed.slug ?? slug;
|
|
267
|
-
const orgId =
|
|
352
|
+
const orgId = resolveOrgIdForCommerce(actor ?? null, this.deps.config);
|
|
268
353
|
let entity = await this.repo.findEntityBySlug(orgId, resolvedSlug, ctx);
|
|
269
354
|
if (!entity)
|
|
270
355
|
return Err(new CommerceNotFoundError("Entity not found."));
|
|
356
|
+
if (!isCatalogEntityVisible(entity, actor ?? null))
|
|
357
|
+
return Err(new CommerceNotFoundError("Entity not found."));
|
|
271
358
|
const entityBeforeHooks = this.deps.hooks.resolve(`catalog.${entity.type}.beforeRead`);
|
|
272
359
|
if (entityBeforeHooks.length > 0) {
|
|
273
360
|
processed = await runBeforeHooks(entityBeforeHooks, { ...processed, id: entity.id }, "read", context);
|
|
274
361
|
const nextId = processed.id ?? entity.id;
|
|
275
362
|
const nextSlug = processed.slug ?? resolvedSlug;
|
|
276
363
|
if (nextId !== entity.id) {
|
|
277
|
-
const refetched = await this.repo.findEntityById(nextId, ctx);
|
|
364
|
+
const refetched = await this.repo.findEntityById(nextId, ctx, orgId);
|
|
278
365
|
if (!refetched)
|
|
279
366
|
return Err(new CommerceNotFoundError("Entity not found."));
|
|
367
|
+
if (!isCatalogEntityVisible(refetched, actor ?? null))
|
|
368
|
+
return Err(new CommerceNotFoundError("Entity not found."));
|
|
280
369
|
entity = refetched;
|
|
281
370
|
resolvedSlug = nextSlug;
|
|
282
371
|
}
|
|
@@ -284,6 +373,8 @@ export class EntityService {
|
|
|
284
373
|
const refetched = await this.repo.findEntityBySlug(orgId, nextSlug, ctx);
|
|
285
374
|
if (!refetched)
|
|
286
375
|
return Err(new CommerceNotFoundError("Entity not found."));
|
|
376
|
+
if (!isCatalogEntityVisible(refetched, actor ?? null))
|
|
377
|
+
return Err(new CommerceNotFoundError("Entity not found."));
|
|
287
378
|
entity = refetched;
|
|
288
379
|
}
|
|
289
380
|
}
|
|
@@ -294,7 +385,7 @@ export class EntityService {
|
|
|
294
385
|
}
|
|
295
386
|
async list(params, actor, ctx) {
|
|
296
387
|
const resolvedActor = actor ?? ctx?.actor ?? null;
|
|
297
|
-
const context = createHookContext({ actor: resolvedActor, tx: ctx?.tx ?? null, logger: createLogger("catalog.list"), services: this.deps.services, context: { moduleName: "catalog" }, ...hookDatabaseArg(this.deps.database) });
|
|
388
|
+
const context = 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 });
|
|
298
389
|
const globalBeforeHooks = this.deps.hooks.resolve("catalog.beforeList");
|
|
299
390
|
const globalAfterHooks = this.deps.hooks.resolve("catalog.afterList");
|
|
300
391
|
let processed = await runBeforeHooks(globalBeforeHooks, params, "list", context);
|
|
@@ -304,14 +395,20 @@ export class EntityService {
|
|
|
304
395
|
if (entityBeforeHooks.length > 0)
|
|
305
396
|
processed = await runBeforeHooks(entityBeforeHooks, processed, "list", context);
|
|
306
397
|
}
|
|
307
|
-
const listOrgId =
|
|
308
|
-
|
|
398
|
+
const listOrgId = resolveOrgIdForCommerce(resolvedActor, this.deps.config);
|
|
399
|
+
const canReadUnpublished = canReadUnpublishedCatalog(resolvedActor);
|
|
400
|
+
let entities = await this.repo.findEntities(listOrgId, {
|
|
401
|
+
...(processed.filter?.type ? { type: processed.filter.type } : {}),
|
|
402
|
+
...(canReadUnpublished
|
|
403
|
+
? (processed.filter?.status ? { status: processed.filter.status } : {})
|
|
404
|
+
: { status: "active", isVisible: true }),
|
|
405
|
+
}, ctx);
|
|
406
|
+
const isUUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
309
407
|
if (processed.filter?.category) {
|
|
310
408
|
const catInput = processed.filter.category;
|
|
311
|
-
const isUUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
312
409
|
let category = await this.repo.findCategoryBySlug(listOrgId, catInput, ctx);
|
|
313
410
|
if (!category && isUUID.test(catInput))
|
|
314
|
-
category = await this.repo.findCategoryById(catInput, ctx);
|
|
411
|
+
category = await this.repo.findCategoryById(catInput, ctx, listOrgId);
|
|
315
412
|
if (!category)
|
|
316
413
|
return Err(new CommerceValidationError(`Category not found: "${catInput}".`));
|
|
317
414
|
const entityIds = await this.repo.findEntitiesByCategory(category.id, ctx);
|
|
@@ -319,8 +416,9 @@ export class EntityService {
|
|
|
319
416
|
}
|
|
320
417
|
if (processed.filter?.brand) {
|
|
321
418
|
let brand = await this.repo.findBrandBySlug(listOrgId, processed.filter.brand, ctx);
|
|
322
|
-
if (!brand)
|
|
323
|
-
brand = await this.repo.findBrandById(processed.filter.brand, ctx);
|
|
419
|
+
if (!brand && isUUID.test(processed.filter.brand)) {
|
|
420
|
+
brand = await this.repo.findBrandById(processed.filter.brand, ctx, listOrgId);
|
|
421
|
+
}
|
|
324
422
|
if (brand) {
|
|
325
423
|
const brandEntityIds = [];
|
|
326
424
|
for (const entity of entities) {
|
|
@@ -403,7 +501,8 @@ export class EntityService {
|
|
|
403
501
|
catch (error) {
|
|
404
502
|
return Err(toCommerceError(error));
|
|
405
503
|
}
|
|
406
|
-
const
|
|
504
|
+
const txCtx = isWriteContextTransactional(ctx) ? ctx : undefined;
|
|
505
|
+
const entity = await this.repo.findEntityById(entityId, txCtx);
|
|
407
506
|
if (!entity)
|
|
408
507
|
return Err(new CommerceNotFoundError("Entity not found."));
|
|
409
508
|
try {
|
|
@@ -412,14 +511,44 @@ export class EntityService {
|
|
|
412
511
|
catch (error) {
|
|
413
512
|
return Err(toCommerceError(error));
|
|
414
513
|
}
|
|
415
|
-
await this.repo.
|
|
514
|
+
const beforeAttr = await this.repo.findAttributeByLocale(entityId, locale, txCtx);
|
|
515
|
+
const changedFieldPaths = [];
|
|
516
|
+
for (const field of attributeFields) {
|
|
517
|
+
if (attrs[field] === undefined)
|
|
518
|
+
continue;
|
|
519
|
+
const before = beforeAttr?.[field] ?? null;
|
|
520
|
+
const after = attrs[field] ?? null;
|
|
521
|
+
if (before !== after) {
|
|
522
|
+
const path = `attributes.${locale}.${field}`;
|
|
523
|
+
if (isValidFieldPath(path))
|
|
524
|
+
changedFieldPaths.push(path);
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
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);
|
|
528
|
+
if (changedFieldPaths.length > 0) {
|
|
529
|
+
const afterHooks = this.deps.hooks.resolve("catalog.afterUpdate");
|
|
530
|
+
const context = catalogHookContext(this.deps, actor, ctx, "update");
|
|
531
|
+
context.context.changedFieldPaths = changedFieldPaths;
|
|
532
|
+
await runAfterHooks(afterHooks, entity, entity, "update", context);
|
|
533
|
+
}
|
|
416
534
|
return Ok(undefined);
|
|
417
535
|
}
|
|
418
|
-
async getAttributes(entityId, locale, ctx) {
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
536
|
+
async getAttributes(entityId, locale, actor, ctx) {
|
|
537
|
+
try {
|
|
538
|
+
assertPermission(actor, "catalog:read");
|
|
539
|
+
const entity = await this.repo.findEntityById(entityId, ctx, resolveOrgIdForCommerce(actor, this.deps.config));
|
|
540
|
+
if (!entity)
|
|
541
|
+
return Err(new CommerceNotFoundError("Entity not found."));
|
|
542
|
+
if (!isCatalogEntityVisible(entity, actor))
|
|
543
|
+
return Err(new CommerceNotFoundError("Entity not found."));
|
|
544
|
+
const attr = await this.repo.findAttributeByLocale(entity.id, locale, ctx);
|
|
545
|
+
if (!attr)
|
|
546
|
+
return Err(new CommerceNotFoundError(`Attributes for locale ${locale} not found.`));
|
|
547
|
+
return Ok(attr);
|
|
548
|
+
}
|
|
549
|
+
catch (error) {
|
|
550
|
+
return Err(toCommerceError(error));
|
|
551
|
+
}
|
|
423
552
|
}
|
|
424
553
|
async createOptionType(input, actor, ctx) {
|
|
425
554
|
assertPermission(actor, "catalog:update");
|
|
@@ -479,7 +608,7 @@ export class EntityService {
|
|
|
479
608
|
return Err(new CommerceValidationError(`Option value "${optVal}" does not exist for option type "${optName}".`));
|
|
480
609
|
optionValueIds.push(ov.id);
|
|
481
610
|
}
|
|
482
|
-
const variant = await this.repo.createVariant({ entityId: input.entityId, status: "active", sortOrder: 0, metadata: {}, ...(input.sku !== undefined ? { sku: input.sku } : {}), ...(input.taxClass !== undefined ? { taxClass: input.taxClass } : {}) }, ctx);
|
|
611
|
+
const variant = await this.repo.createVariant({ entityId: input.entityId, status: "active", sortOrder: 0, metadata: {}, ...(input.sku !== undefined ? { sku: input.sku } : {}), ...(input.barcode !== undefined ? { barcode: input.barcode } : {}), ...(input.taxClass !== undefined ? { taxClass: input.taxClass } : {}) }, ctx);
|
|
483
612
|
await this.repo.createVariantOptionValues(optionValueIds.map((optionValueId) => ({ variantId: variant.id, optionValueId })), ctx);
|
|
484
613
|
return Ok(variant);
|
|
485
614
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { PricingService } from "../pricing/service.js";
|
|
2
|
-
import { brands, categories, entityBrands, entityCategories, optionTypes, optionValues, sellableAttributes, sellableCustomFields, sellableEntities, variantOptionValues, variants } from "./schema.js";
|
|
2
|
+
import { brands, categories, tags, entityTags, entityBrands, entityCategories, optionTypes, optionValues, sellableAttributes, sellableCustomFields, entityFieldDefinitions, sellableEntityRevisions, sellableEntities, variantOptionValues, variants, catalogFieldOwnership } from "./schema.js";
|
|
3
3
|
import { CatalogServiceImpl } from "./service.js";
|
|
4
4
|
type CatalogModuleDeps = {
|
|
5
5
|
pricing: PricingService;
|
|
@@ -8,14 +8,19 @@ export declare const catalogModule: import("../../kernel/module/define.js").AppM
|
|
|
8
8
|
sellableEntities: typeof sellableEntities;
|
|
9
9
|
sellableAttributes: typeof sellableAttributes;
|
|
10
10
|
sellableCustomFields: typeof sellableCustomFields;
|
|
11
|
+
entityFieldDefinitions: typeof entityFieldDefinitions;
|
|
12
|
+
sellableEntityRevisions: typeof sellableEntityRevisions;
|
|
11
13
|
categories: typeof categories;
|
|
12
14
|
entityCategories: typeof entityCategories;
|
|
13
15
|
brands: typeof brands;
|
|
14
16
|
entityBrands: typeof entityBrands;
|
|
17
|
+
tags: typeof tags;
|
|
18
|
+
entityTags: typeof entityTags;
|
|
15
19
|
optionTypes: typeof optionTypes;
|
|
16
20
|
optionValues: typeof optionValues;
|
|
17
21
|
variants: typeof variants;
|
|
18
22
|
variantOptionValues: typeof variantOptionValues;
|
|
23
|
+
catalogFieldOwnership: typeof catalogFieldOwnership;
|
|
19
24
|
}, CatalogServiceImpl, CatalogModuleDeps>;
|
|
20
25
|
export {};
|
|
21
26
|
//# sourceMappingURL=module.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"module.d.ts","sourceRoot":"","sources":["../../../src/modules/catalog/module.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAE5D,OAAO,EACL,MAAM,EACN,UAAU,EACV,YAAY,EACZ,gBAAgB,EAChB,WAAW,EACX,YAAY,EACZ,kBAAkB,EAClB,oBAAoB,EACpB,gBAAgB,EAChB,mBAAmB,EACnB,QAAQ,
|
|
1
|
+
{"version":3,"file":"module.d.ts","sourceRoot":"","sources":["../../../src/modules/catalog/module.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAE5D,OAAO,EACL,MAAM,EACN,UAAU,EACV,IAAI,EACJ,UAAU,EACV,YAAY,EACZ,gBAAgB,EAChB,WAAW,EACX,YAAY,EACZ,kBAAkB,EAClB,oBAAoB,EACpB,sBAAsB,EACtB,uBAAuB,EACvB,gBAAgB,EAChB,mBAAmB,EACnB,QAAQ,EACR,qBAAqB,EACtB,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AAElD,KAAK,iBAAiB,GAAG;IACvB,OAAO,EAAE,cAAc,CAAC;CACzB,CAAC;AAEF,eAAO,MAAM,aAAa;sBAEJ,OAAO,gBAAgB;wBACrB,OAAO,kBAAkB;0BACvB,OAAO,oBAAoB;4BACzB,OAAO,sBAAsB;6BAC5B,OAAO,uBAAuB;gBAC3C,OAAO,UAAU;sBACX,OAAO,gBAAgB;YACjC,OAAO,MAAM;kBACP,OAAO,YAAY;UAC3B,OAAO,IAAI;gBACL,OAAO,UAAU;iBAChB,OAAO,WAAW;kBACjB,OAAO,YAAY;cACvB,OAAO,QAAQ;yBACJ,OAAO,mBAAmB;2BACxB,OAAO,qBAAqB;yCAiCrD,CAAC"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { defineModule } from "../../kernel/module/index.js";
|
|
2
2
|
import { CatalogRepository } from "./repository/index.js";
|
|
3
|
-
import { brands, categories, entityBrands, entityCategories, optionTypes, optionValues, sellableAttributes, sellableCustomFields, sellableEntities, variantOptionValues, variants, } from "./schema.js";
|
|
3
|
+
import { brands, categories, tags, entityTags, entityBrands, entityCategories, optionTypes, optionValues, sellableAttributes, sellableCustomFields, entityFieldDefinitions, sellableEntityRevisions, sellableEntities, variantOptionValues, variants, catalogFieldOwnership, } from "./schema.js";
|
|
4
4
|
import { CatalogServiceImpl } from "./service.js";
|
|
5
5
|
export const catalogModule = defineModule({
|
|
6
6
|
id: "catalog",
|
|
@@ -9,14 +9,19 @@ export const catalogModule = defineModule({
|
|
|
9
9
|
sellableEntities,
|
|
10
10
|
sellableAttributes,
|
|
11
11
|
sellableCustomFields,
|
|
12
|
+
entityFieldDefinitions,
|
|
13
|
+
sellableEntityRevisions,
|
|
12
14
|
categories,
|
|
13
15
|
entityCategories,
|
|
14
16
|
brands,
|
|
15
17
|
entityBrands,
|
|
18
|
+
tags,
|
|
19
|
+
entityTags,
|
|
16
20
|
optionTypes,
|
|
17
21
|
optionValues,
|
|
18
22
|
variants,
|
|
19
23
|
variantOptionValues,
|
|
24
|
+
catalogFieldOwnership,
|
|
20
25
|
}),
|
|
21
26
|
service: (deps) => new CatalogServiceImpl({
|
|
22
27
|
repository: new CatalogRepository(deps.db.db),
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export type FieldPath = string;
|
|
2
|
+
export type FieldOwner = "platform" | "store" | "shared";
|
|
3
|
+
export declare function isValidFieldPath(value: string): value is FieldPath;
|
|
4
|
+
export declare function validateFieldPath(value: string): FieldPath;
|
|
5
|
+
//# sourceMappingURL=ownership.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ownership.d.ts","sourceRoot":"","sources":["../../../src/modules/catalog/ownership.ts"],"names":[],"mappings":"AAEA,MAAM,MAAM,SAAS,GAAG,MAAM,CAAC;AAC/B,MAAM,MAAM,UAAU,GAAG,UAAU,GAAG,OAAO,GAAG,QAAQ,CAAC;AAIzD,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,GAAG,KAAK,IAAI,SAAS,CAElE;AAED,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,CAK1D"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { CommerceValidationError } from "../../kernel/errors.js";
|
|
2
|
+
const fieldPathPattern = /^[A-Za-z0-9_-]+(?:\.[A-Za-z0-9_-]+)*$/;
|
|
3
|
+
export function isValidFieldPath(value) {
|
|
4
|
+
return fieldPathPattern.test(value);
|
|
5
|
+
}
|
|
6
|
+
export function validateFieldPath(value) {
|
|
7
|
+
if (!isValidFieldPath(value)) {
|
|
8
|
+
throw new CommerceValidationError("Field path must contain dot-separated alphanumeric, underscore, or hyphen segments.");
|
|
9
|
+
}
|
|
10
|
+
return value;
|
|
11
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { Actor } from "../../auth/types.js";
|
|
2
|
+
type CatalogVisibility = {
|
|
3
|
+
status: string;
|
|
4
|
+
isVisible: boolean;
|
|
5
|
+
};
|
|
6
|
+
export declare function canReadUnpublishedCatalog(actor: Actor | null): boolean;
|
|
7
|
+
export declare function isCatalogEntityVisible(entity: CatalogVisibility, actor: Actor | null): boolean;
|
|
8
|
+
export {};
|
|
9
|
+
//# sourceMappingURL=read-policy.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"read-policy.d.ts","sourceRoot":"","sources":["../../../src/modules/catalog/read-policy.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,qBAAqB,CAAC;AAEjD,KAAK,iBAAiB,GAAG;IACvB,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,OAAO,CAAC;CACpB,CAAC;AAEF,wBAAgB,yBAAyB,CAAC,KAAK,EAAE,KAAK,GAAG,IAAI,GAAG,OAAO,CAEtE;AAED,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,iBAAiB,EAAE,KAAK,EAAE,KAAK,GAAG,IAAI,GAAG,OAAO,CAG9F"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { hasPermission } from "../../auth/permissions.js";
|
|
2
|
+
export function canReadUnpublishedCatalog(actor) {
|
|
3
|
+
return hasPermission(actor, "catalog:read:unpublished");
|
|
4
|
+
}
|
|
5
|
+
export function isCatalogEntityVisible(entity, actor) {
|
|
6
|
+
return canReadUnpublishedCatalog(actor) ||
|
|
7
|
+
(entity.status === "active" && entity.isVisible);
|
|
8
|
+
}
|