@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 type { Actor } from "../../auth/types.js";
|
|
4
4
|
import {
|
|
@@ -21,8 +21,12 @@ import type {
|
|
|
21
21
|
import { Err, Ok, type Result } from "../../kernel/result.js";
|
|
22
22
|
import { createLogger } from "../../utils/logger.js";
|
|
23
23
|
import { paginate } from "../../utils/pagination.js";
|
|
24
|
+
import type { JobsAdapter } from "../../kernel/jobs/adapter.js";
|
|
24
25
|
import type { PluginDb } from "../../kernel/database/plugin-types.js";
|
|
25
|
-
import type { TxContext } from "../../kernel/database/tx-context.js";
|
|
26
|
+
import type { CatalogWriteContext, TxContext } from "../../kernel/database/tx-context.js";
|
|
27
|
+
import { isWriteContextTransactional } from "../../kernel/database/tx-context.js";
|
|
28
|
+
import { canReadUnpublishedCatalog, isCatalogEntityVisible } from "./read-policy.js";
|
|
29
|
+
import { isValidFieldPath } from "./ownership.js";
|
|
26
30
|
import type {
|
|
27
31
|
CatalogServiceDeps,
|
|
28
32
|
CatalogEntityHydrated,
|
|
@@ -36,6 +40,7 @@ import type {
|
|
|
36
40
|
CreateOptionTypeInput,
|
|
37
41
|
CreateOptionValueInput,
|
|
38
42
|
CreateVariantInput,
|
|
43
|
+
EntityFieldDefinitionResolver,
|
|
39
44
|
} from "./service.js";
|
|
40
45
|
import type {
|
|
41
46
|
SellableEntity,
|
|
@@ -51,6 +56,26 @@ function hookDatabaseArg(database: CatalogServiceDeps["database"]): { database:
|
|
|
51
56
|
return { database: { db: database.db as PluginDb } };
|
|
52
57
|
}
|
|
53
58
|
|
|
59
|
+
const attributeFields = ["title", "subtitle", "description", "richDescription", "seoTitle", "seoDescription"] as const;
|
|
60
|
+
|
|
61
|
+
function catalogHookContext(
|
|
62
|
+
deps: CatalogServiceDeps,
|
|
63
|
+
actor: Actor | null,
|
|
64
|
+
ctx: CatalogWriteContext | undefined,
|
|
65
|
+
operation: string,
|
|
66
|
+
): HookContext {
|
|
67
|
+
return createHookContext({
|
|
68
|
+
actor,
|
|
69
|
+
tx: isWriteContextTransactional(ctx) ? ctx.tx : null,
|
|
70
|
+
logger: createLogger(`catalog.${operation}`),
|
|
71
|
+
services: deps.services,
|
|
72
|
+
...(deps.services.jobs ? { jobs: deps.services.jobs as JobsAdapter } : {}),
|
|
73
|
+
context: { moduleName: "catalog", ...(ctx?.hookContext ?? {}) },
|
|
74
|
+
...hookDatabaseArg(deps.database),
|
|
75
|
+
commerceConfig: deps.config,
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
54
79
|
type CatalogCreateBeforeHook = BeforeHook<CreateEntityInput>;
|
|
55
80
|
type CatalogCreateAfterHook = AfterHook<SellableEntity>;
|
|
56
81
|
type CatalogUpdateBeforeHook = BeforeHook<UpdateEntityInput>;
|
|
@@ -69,6 +94,7 @@ function getCustomFieldValue(field: SellableCustomField): unknown {
|
|
|
69
94
|
switch (field.fieldType) {
|
|
70
95
|
case "text":
|
|
71
96
|
case "relation":
|
|
97
|
+
case "select":
|
|
72
98
|
return field.textValue;
|
|
73
99
|
case "number":
|
|
74
100
|
return field.numberValue;
|
|
@@ -83,8 +109,14 @@ function getCustomFieldValue(field: SellableCustomField): unknown {
|
|
|
83
109
|
}
|
|
84
110
|
}
|
|
85
111
|
|
|
112
|
+
type CustomFieldData = Omit<SellableCustomFieldInsert, "entityId" | "fieldName" | "locale">;
|
|
113
|
+
type ValidatedCustomField = { fieldName: string; locale: string; data: CustomFieldData | null };
|
|
114
|
+
|
|
86
115
|
export class EntityService {
|
|
87
|
-
constructor(
|
|
116
|
+
constructor(
|
|
117
|
+
private readonly deps: CatalogServiceDeps,
|
|
118
|
+
private readonly resolveEntityFieldDefinitions: EntityFieldDefinitionResolver,
|
|
119
|
+
) {}
|
|
88
120
|
|
|
89
121
|
private get repo() {
|
|
90
122
|
return this.deps.repository;
|
|
@@ -92,7 +124,7 @@ export class EntityService {
|
|
|
92
124
|
|
|
93
125
|
private assertSameOrg(resource: { organizationId?: string | null } | undefined, actor: Actor | null): void {
|
|
94
126
|
if (!resource) return;
|
|
95
|
-
const orgId =
|
|
127
|
+
const orgId = resolveOrgIdForCommerce(actor, this.deps.config);
|
|
96
128
|
if (resource.organizationId && resource.organizationId !== orgId) {
|
|
97
129
|
throw new CommerceNotFoundError("Entity not found.");
|
|
98
130
|
}
|
|
@@ -102,22 +134,38 @@ export class EntityService {
|
|
|
102
134
|
if (entity.sourceStoreId != null) assertPermission(actor, "catalog:sync");
|
|
103
135
|
}
|
|
104
136
|
|
|
105
|
-
private async
|
|
106
|
-
if (!customFields) return Ok(
|
|
137
|
+
private async validateCustomFields(entityType: string, customFields: Record<string, unknown> | undefined, actor: Actor | null, ctx?: TxContext): Promise<Result<ValidatedCustomField[]>> {
|
|
138
|
+
if (!customFields) return Ok([]);
|
|
107
139
|
const entityConfig = this.deps.config.entities?.[entityType];
|
|
108
|
-
|
|
109
|
-
|
|
140
|
+
const definitions = await this.resolveEntityFieldDefinitions(entityType, actor ?? ctx?.actor ?? null, ctx);
|
|
141
|
+
if (!entityConfig && definitions.length === 0) return Ok([]);
|
|
142
|
+
const definitionMap = new Map(definitions.map((f) => [f.name, f]));
|
|
143
|
+
const validated: ValidatedCustomField[] = [];
|
|
110
144
|
for (const [name, value] of Object.entries(customFields)) {
|
|
111
145
|
const def = definitionMap.get(name);
|
|
112
146
|
if (!def) return Err(new CommerceValidationError(`Unknown custom field: ${name}`));
|
|
147
|
+
if (value === null) {
|
|
148
|
+
validated.push({ fieldName: name, locale: "en", data: null });
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
113
151
|
const type = def.type;
|
|
152
|
+
let normalized = value;
|
|
114
153
|
let valid = false;
|
|
115
154
|
switch (type) {
|
|
116
155
|
case "text":
|
|
117
156
|
case "relation":
|
|
118
|
-
case "select":
|
|
119
157
|
valid = typeof value === "string";
|
|
120
158
|
break;
|
|
159
|
+
case "select": {
|
|
160
|
+
if (typeof value !== "string") break;
|
|
161
|
+
const trimmed = value.trim();
|
|
162
|
+
if (def.options && !def.options.includes(trimmed)) {
|
|
163
|
+
return Err(new CommerceValidationError(`Value "${trimmed}" is not permitted for field ${name}. Allowed: ${def.options.join(", ")}`));
|
|
164
|
+
}
|
|
165
|
+
normalized = trimmed;
|
|
166
|
+
valid = true;
|
|
167
|
+
break;
|
|
168
|
+
}
|
|
121
169
|
case "number":
|
|
122
170
|
valid = typeof value === "number";
|
|
123
171
|
break;
|
|
@@ -134,29 +182,50 @@ export class EntityService {
|
|
|
134
182
|
valid = false;
|
|
135
183
|
}
|
|
136
184
|
if (!valid) return Err(new CommerceValidationError(`Custom field ${name} expected type ${type}.`));
|
|
137
|
-
const fieldType =
|
|
138
|
-
const insertData:
|
|
185
|
+
const fieldType = type as SellableCustomField["fieldType"];
|
|
186
|
+
const insertData: CustomFieldData = {
|
|
187
|
+
fieldType,
|
|
188
|
+
source: "merchant",
|
|
189
|
+
status: "approved",
|
|
190
|
+
confidence: null,
|
|
191
|
+
evidence: null,
|
|
192
|
+
approvedAt: null,
|
|
193
|
+
approvedBy: null,
|
|
194
|
+
};
|
|
139
195
|
switch (fieldType) {
|
|
140
196
|
case "text":
|
|
141
197
|
case "relation":
|
|
142
|
-
|
|
198
|
+
case "select":
|
|
199
|
+
insertData.textValue = normalized as string;
|
|
143
200
|
break;
|
|
144
201
|
case "number":
|
|
145
|
-
insertData.numberValue =
|
|
202
|
+
insertData.numberValue = normalized as number;
|
|
146
203
|
break;
|
|
147
204
|
case "boolean":
|
|
148
|
-
insertData.booleanValue =
|
|
205
|
+
insertData.booleanValue = normalized as boolean;
|
|
149
206
|
break;
|
|
150
207
|
case "date":
|
|
151
|
-
insertData.dateValue =
|
|
208
|
+
insertData.dateValue = normalized instanceof Date ? normalized : new Date(normalized as string);
|
|
152
209
|
break;
|
|
153
210
|
case "json":
|
|
154
|
-
insertData.jsonValue =
|
|
211
|
+
insertData.jsonValue = normalized;
|
|
155
212
|
break;
|
|
156
213
|
}
|
|
157
|
-
|
|
214
|
+
validated.push({ fieldName: name, locale: "en", data: insertData });
|
|
215
|
+
}
|
|
216
|
+
return Ok(validated);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
private async writeCustomFields(entityId: string, customFields: ValidatedCustomField[], ctx?: TxContext, upsert = false): Promise<void> {
|
|
220
|
+
for (const customField of customFields) {
|
|
221
|
+
if (customField.data === null) {
|
|
222
|
+
await this.repo.deleteCustomField(entityId, customField.fieldName, customField.locale, ctx);
|
|
223
|
+
} else if (upsert) {
|
|
224
|
+
await this.repo.upsertCustomField(entityId, customField.fieldName, customField.locale, customField.data, ctx);
|
|
225
|
+
} else {
|
|
226
|
+
await this.repo.createCustomField({ ...customField.data, entityId, fieldName: customField.fieldName, locale: customField.locale }, ctx);
|
|
227
|
+
}
|
|
158
228
|
}
|
|
159
|
-
return Ok(undefined);
|
|
160
229
|
}
|
|
161
230
|
|
|
162
231
|
private async hydrateEntity(entity: SellableEntity, options?: GetOptions, ctx?: TxContext): Promise<CatalogEntityHydrated> {
|
|
@@ -185,14 +254,16 @@ export class EntityService {
|
|
|
185
254
|
}
|
|
186
255
|
if (options?.includeCategories) hydrated.categories = await this.repo.findEntityCategories(entity.id, ctx);
|
|
187
256
|
if (options?.includeBrands) hydrated.brands = await this.repo.findEntityBrands(entity.id, ctx);
|
|
257
|
+
if (options?.includeCustomFields) hydrated.customFields = await this.repo.findCustomFieldsByEntityId(entity.id, ctx);
|
|
188
258
|
if (options?.includeMedia) {
|
|
189
259
|
const mediaService = this.deps.services.media as {
|
|
190
260
|
listEntityMedia?: (
|
|
191
261
|
entityId: string,
|
|
192
262
|
opts?: { orgId?: string },
|
|
263
|
+
ctx?: TxContext,
|
|
193
264
|
) => Promise<{ ok: boolean; value?: CatalogEntityHydrated["media"] }>;
|
|
194
265
|
} | undefined;
|
|
195
|
-
const mediaResult = await mediaService?.listEntityMedia?.(entity.id, { orgId: entity.organizationId });
|
|
266
|
+
const mediaResult = await mediaService?.listEntityMedia?.(entity.id, { orgId: entity.organizationId }, ctx);
|
|
196
267
|
hydrated.media = mediaResult?.ok && mediaResult.value ? mediaResult.value : [];
|
|
197
268
|
}
|
|
198
269
|
if (options?.includePricing) {
|
|
@@ -209,39 +280,46 @@ export class EntityService {
|
|
|
209
280
|
|
|
210
281
|
async create(input: CreateEntityInput, actor: Actor | null, ctx?: TxContext): Promise<Result<CatalogEntityHydrated>> { try {
|
|
211
282
|
assertPermission(actor, "catalog:create");
|
|
212
|
-
const orgId =
|
|
283
|
+
const orgId = resolveOrgIdForCommerce(actor, this.deps.config);
|
|
213
284
|
const existingBySlug = await this.repo.findEntityBySlug(orgId, input.slug, ctx);
|
|
214
285
|
if (existingBySlug) return Err(new CommerceConflictError(`Entity with slug ${input.slug} already exists.`));
|
|
215
286
|
const beforeHooks = this.deps.hooks.resolve("catalog.beforeCreate") as CatalogCreateBeforeHook[];
|
|
216
287
|
const afterHooks = this.deps.hooks.resolve("catalog.afterCreate") as CatalogCreateAfterHook[];
|
|
217
|
-
const context: HookContext = createHookContext({ actor, tx: ctx?.tx ?? null, logger: createLogger("catalog.create"), services: this.deps.services, context: { moduleName: "catalog" }, ...hookDatabaseArg(this.deps.database) });
|
|
288
|
+
const context: HookContext = createHookContext({ actor, tx: ctx?.tx ?? null, logger: createLogger("catalog.create"), services: this.deps.services, context: { moduleName: "catalog" }, ...hookDatabaseArg(this.deps.database), commerceConfig: this.deps.config });
|
|
218
289
|
const processedInput = await runBeforeHooks(beforeHooks, input, "create", context);
|
|
219
290
|
if (processedInput.sourceStoreId != null) assertPermission(actor, "catalog:sync");
|
|
220
|
-
const
|
|
291
|
+
const customFieldsResult = await this.validateCustomFields(processedInput.type, processedInput.customFields, actor, ctx);
|
|
292
|
+
if (!customFieldsResult.ok) return customFieldsResult;
|
|
293
|
+
const statuses = ["draft", "active", "archived", "discontinued"] as const;
|
|
294
|
+
const status = statuses.find((value) => value === processedInput.status) ?? "draft";
|
|
295
|
+
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);
|
|
221
296
|
if (processedInput.attributes) {
|
|
222
297
|
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);
|
|
223
298
|
}
|
|
224
|
-
|
|
225
|
-
if (!customFieldsResult.ok) return customFieldsResult;
|
|
299
|
+
await this.writeCustomFields(entity.id, customFieldsResult.value, ctx);
|
|
226
300
|
const hookReport = await runAfterHooks(afterHooks, null, entity, "create", context);
|
|
227
301
|
const hydrated = await this.hydrateEntity(entity, undefined, ctx);
|
|
228
302
|
return Ok(hydrated, hookReport.hasErrors ? { hookErrors: hookReport.errors } : undefined);
|
|
229
303
|
} catch (error) { return Err(toCommerceError(error)); } }
|
|
230
304
|
|
|
231
|
-
async update(id: string, input: UpdateEntityInput, actor: Actor | null, ctx?:
|
|
305
|
+
async update(id: string, input: UpdateEntityInput, actor: Actor | null, ctx?: CatalogWriteContext): Promise<Result<CatalogEntityHydrated>> { try {
|
|
306
|
+
const txCtx = isWriteContextTransactional(ctx) ? ctx : undefined;
|
|
232
307
|
assertPermission(actor, "catalog:update");
|
|
233
|
-
const existing = await this.repo.findEntityById(id,
|
|
308
|
+
const existing = await this.repo.findEntityById(id, txCtx);
|
|
234
309
|
if (!existing) return Err(new CommerceNotFoundError("Entity not found."));
|
|
235
310
|
this.assertSameOrg(existing, actor);
|
|
236
311
|
this.assertEntityWritable(existing, actor);
|
|
237
312
|
const beforeHooks = this.deps.hooks.resolve("catalog.beforeUpdate") as CatalogUpdateBeforeHook[];
|
|
238
313
|
const afterHooks = this.deps.hooks.resolve("catalog.afterUpdate") as CatalogUpdateAfterHook[];
|
|
239
|
-
const context: HookContext =
|
|
314
|
+
const context: HookContext = catalogHookContext(this.deps, actor, ctx, "update");
|
|
240
315
|
const processed = await runBeforeHooks(beforeHooks, input, "update", context);
|
|
241
|
-
const
|
|
316
|
+
const customFieldsResult = await this.validateCustomFields(existing.type, processed.customFields, actor, txCtx);
|
|
317
|
+
if (!customFieldsResult.ok) return customFieldsResult;
|
|
318
|
+
const updated = await this.repo.updateEntity(id, { ...(processed.slug !== undefined ? { slug: processed.slug } : {}), ...(processed.status !== undefined ? { status: processed.status as SellableEntity["status"] } : {}), ...(processed.taxClass !== undefined ? { taxClass: processed.taxClass } : {}), ...(processed.metadata !== undefined ? { metadata: processed.metadata } : {}), ...(processed.isVisible !== undefined ? { isVisible: processed.isVisible } : {}) }, txCtx);
|
|
242
319
|
if (!updated) return Err(new CommerceNotFoundError("Entity not found."));
|
|
320
|
+
await this.writeCustomFields(existing.id, customFieldsResult.value, txCtx, true);
|
|
243
321
|
const hookReport = await runAfterHooks(afterHooks, existing, updated, "update", context);
|
|
244
|
-
const hydrated = await this.hydrateEntity(updated, undefined,
|
|
322
|
+
const hydrated = await this.hydrateEntity(updated, undefined, txCtx);
|
|
245
323
|
return Ok(hydrated, hookReport.hasErrors ? { hookErrors: hookReport.errors } : undefined);
|
|
246
324
|
} catch (error) { return Err(toCommerceError(error)); } }
|
|
247
325
|
|
|
@@ -261,24 +339,27 @@ export class EntityService {
|
|
|
261
339
|
return Ok(undefined);
|
|
262
340
|
} catch (error) { return Err(toCommerceError(error)); } }
|
|
263
341
|
|
|
264
|
-
async getById(id: string, options
|
|
265
|
-
const
|
|
342
|
+
async getById(id: string, options: GetOptions | undefined, actor: Actor | null, ctx?: TxContext): Promise<Result<CatalogEntityHydrated>> {
|
|
343
|
+
const resolvedActor = actor ?? ctx?.actor ?? null;
|
|
344
|
+
const orgId = resolveOrgIdForCommerce(resolvedActor, this.deps.config);
|
|
345
|
+
const context: HookContext = createHookContext({ actor: resolvedActor, tx: ctx?.tx ?? null, logger: createLogger("catalog.read"), services: this.deps.services, context: { moduleName: "catalog" }, ...hookDatabaseArg(this.deps.database), commerceConfig: this.deps.config });
|
|
266
346
|
const globalBeforeHooks = this.deps.hooks.resolve("catalog.beforeRead") as CatalogReadBeforeHook[];
|
|
267
347
|
const globalAfterHooks = this.deps.hooks.resolve("catalog.afterRead") as CatalogReadAfterHook[];
|
|
268
348
|
let processed = await runBeforeHooks(globalBeforeHooks, { id, ...(options !== undefined ? { options } : {}) }, "read", context);
|
|
269
|
-
let entity = await this.repo.findEntityById(processed.id ?? id, ctx);
|
|
349
|
+
let entity = await this.repo.findEntityById(processed.id ?? id, ctx, orgId);
|
|
270
350
|
if (!entity) return Err(new CommerceNotFoundError("Entity not found."));
|
|
351
|
+
if (!isCatalogEntityVisible(entity, resolvedActor)) return Err(new CommerceNotFoundError("Entity not found."));
|
|
271
352
|
const entityBeforeHooks = this.deps.hooks.resolve(`catalog.${entity.type}.beforeRead`) as CatalogReadBeforeHook[];
|
|
272
353
|
if (entityBeforeHooks.length > 0) {
|
|
273
354
|
processed = await runBeforeHooks(entityBeforeHooks, { ...processed, id: entity.id }, "read", context);
|
|
274
355
|
const resolvedId = processed.id ?? entity.id;
|
|
275
356
|
if (resolvedId !== entity.id) {
|
|
276
|
-
const refetched = await this.repo.findEntityById(resolvedId, ctx);
|
|
357
|
+
const refetched = await this.repo.findEntityById(resolvedId, ctx, orgId);
|
|
277
358
|
if (!refetched) return Err(new CommerceNotFoundError("Entity not found."));
|
|
359
|
+
if (!isCatalogEntityVisible(refetched, resolvedActor)) return Err(new CommerceNotFoundError("Entity not found."));
|
|
278
360
|
entity = refetched;
|
|
279
361
|
}
|
|
280
362
|
}
|
|
281
|
-
const resolvedActor = actor ?? ctx?.actor ?? null;
|
|
282
363
|
try {
|
|
283
364
|
this.assertSameOrg(entity, resolvedActor);
|
|
284
365
|
} catch (error) {
|
|
@@ -290,28 +371,48 @@ export class EntityService {
|
|
|
290
371
|
return Ok(result, report.hasErrors ? { hookErrors: report.errors } : undefined);
|
|
291
372
|
}
|
|
292
373
|
|
|
293
|
-
|
|
294
|
-
|
|
374
|
+
/**
|
|
375
|
+
* Reads an entity for a trusted server pipeline after the request boundary
|
|
376
|
+
* has already authorized the caller. Organization scope is explicit; the
|
|
377
|
+
* storefront visibility policy is intentionally not applied, so an internal
|
|
378
|
+
* invariant check never depends on the caller's catalog permissions.
|
|
379
|
+
*/
|
|
380
|
+
async getByIdForInternalUse(
|
|
381
|
+
id: string,
|
|
382
|
+
organizationId: string,
|
|
383
|
+
options?: GetOptions,
|
|
384
|
+
ctx?: TxContext,
|
|
385
|
+
): Promise<Result<CatalogEntityHydrated>> {
|
|
386
|
+
const entity = await this.repo.findEntityById(id, ctx, organizationId);
|
|
387
|
+
if (!entity) return Err(new CommerceNotFoundError("Entity not found."));
|
|
388
|
+
return Ok(await this.hydrateEntity(entity, options, ctx));
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
async getBySlug(slug: string, options: GetOptions | undefined, actor: Actor | null, ctx?: TxContext): Promise<Result<CatalogEntityHydrated>> {
|
|
392
|
+
const context: HookContext = createHookContext({ actor: actor ?? null, tx: ctx?.tx ?? null, logger: createLogger("catalog.read"), services: this.deps.services, context: { moduleName: "catalog" }, ...hookDatabaseArg(this.deps.database), commerceConfig: this.deps.config });
|
|
295
393
|
const globalBeforeHooks = this.deps.hooks.resolve("catalog.beforeRead") as CatalogReadBeforeHook[];
|
|
296
394
|
const globalAfterHooks = this.deps.hooks.resolve("catalog.afterRead") as CatalogReadAfterHook[];
|
|
297
395
|
let processed = await runBeforeHooks(globalBeforeHooks, { slug, ...(options !== undefined ? { options } : {}) }, "read", context);
|
|
298
396
|
let resolvedSlug = processed.slug ?? slug;
|
|
299
|
-
const orgId =
|
|
397
|
+
const orgId = resolveOrgIdForCommerce(actor ?? null, this.deps.config);
|
|
300
398
|
let entity = await this.repo.findEntityBySlug(orgId, resolvedSlug, ctx);
|
|
301
399
|
if (!entity) return Err(new CommerceNotFoundError("Entity not found."));
|
|
400
|
+
if (!isCatalogEntityVisible(entity, actor ?? null)) return Err(new CommerceNotFoundError("Entity not found."));
|
|
302
401
|
const entityBeforeHooks = this.deps.hooks.resolve(`catalog.${entity.type}.beforeRead`) as CatalogReadBeforeHook[];
|
|
303
402
|
if (entityBeforeHooks.length > 0) {
|
|
304
403
|
processed = await runBeforeHooks(entityBeforeHooks, { ...processed, id: entity.id }, "read", context);
|
|
305
404
|
const nextId = processed.id ?? entity.id;
|
|
306
405
|
const nextSlug = processed.slug ?? resolvedSlug;
|
|
307
406
|
if (nextId !== entity.id) {
|
|
308
|
-
const refetched = await this.repo.findEntityById(nextId, ctx);
|
|
407
|
+
const refetched = await this.repo.findEntityById(nextId, ctx, orgId);
|
|
309
408
|
if (!refetched) return Err(new CommerceNotFoundError("Entity not found."));
|
|
409
|
+
if (!isCatalogEntityVisible(refetched, actor ?? null)) return Err(new CommerceNotFoundError("Entity not found."));
|
|
310
410
|
entity = refetched;
|
|
311
411
|
resolvedSlug = nextSlug;
|
|
312
412
|
} else if (nextSlug !== resolvedSlug) {
|
|
313
413
|
const refetched = await this.repo.findEntityBySlug(orgId, nextSlug, ctx);
|
|
314
414
|
if (!refetched) return Err(new CommerceNotFoundError("Entity not found."));
|
|
415
|
+
if (!isCatalogEntityVisible(refetched, actor ?? null)) return Err(new CommerceNotFoundError("Entity not found."));
|
|
315
416
|
entity = refetched;
|
|
316
417
|
}
|
|
317
418
|
}
|
|
@@ -321,9 +422,9 @@ export class EntityService {
|
|
|
321
422
|
return Ok(result, report.hasErrors ? { hookErrors: report.errors } : undefined);
|
|
322
423
|
}
|
|
323
424
|
|
|
324
|
-
async list(params: ListParams, actor
|
|
425
|
+
async list(params: ListParams, actor: Actor | null, ctx?: TxContext): Promise<Result<CatalogListResult>> {
|
|
325
426
|
const resolvedActor = actor ?? ctx?.actor ?? null;
|
|
326
|
-
const context: HookContext = createHookContext({ actor: resolvedActor, tx: ctx?.tx ?? null, logger: createLogger("catalog.list"), services: this.deps.services, context: { moduleName: "catalog" }, ...hookDatabaseArg(this.deps.database) });
|
|
427
|
+
const context: HookContext = createHookContext({ actor: resolvedActor, tx: ctx?.tx ?? null, logger: createLogger("catalog.list"), services: this.deps.services, context: { moduleName: "catalog" }, ...hookDatabaseArg(this.deps.database), commerceConfig: this.deps.config });
|
|
327
428
|
const globalBeforeHooks = this.deps.hooks.resolve("catalog.beforeList") as CatalogListBeforeHook[];
|
|
328
429
|
const globalAfterHooks = this.deps.hooks.resolve("catalog.afterList") as CatalogListAfterHook[];
|
|
329
430
|
let processed = await runBeforeHooks(globalBeforeHooks, params, "list", context);
|
|
@@ -332,20 +433,28 @@ export class EntityService {
|
|
|
332
433
|
const entityBeforeHooks = this.deps.hooks.resolve(`catalog.${initialTypeFilter}.beforeList`) as CatalogListBeforeHook[];
|
|
333
434
|
if (entityBeforeHooks.length > 0) processed = await runBeforeHooks(entityBeforeHooks, processed, "list", context);
|
|
334
435
|
}
|
|
335
|
-
const listOrgId =
|
|
336
|
-
|
|
436
|
+
const listOrgId = resolveOrgIdForCommerce(resolvedActor, this.deps.config);
|
|
437
|
+
const canReadUnpublished = canReadUnpublishedCatalog(resolvedActor);
|
|
438
|
+
let entities = await this.repo.findEntities(listOrgId, {
|
|
439
|
+
...(processed.filter?.type ? { type: processed.filter.type } : {}),
|
|
440
|
+
...(canReadUnpublished
|
|
441
|
+
? (processed.filter?.status ? { status: processed.filter.status } : {})
|
|
442
|
+
: { status: "active", isVisible: true }),
|
|
443
|
+
}, ctx);
|
|
444
|
+
const isUUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
337
445
|
if (processed.filter?.category) {
|
|
338
446
|
const catInput = processed.filter.category;
|
|
339
|
-
const isUUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
340
447
|
let category = await this.repo.findCategoryBySlug(listOrgId, catInput, ctx);
|
|
341
|
-
if (!category && isUUID.test(catInput)) category = await this.repo.findCategoryById(catInput, ctx);
|
|
448
|
+
if (!category && isUUID.test(catInput)) category = await this.repo.findCategoryById(catInput, ctx, listOrgId);
|
|
342
449
|
if (!category) return Err(new CommerceValidationError(`Category not found: "${catInput}".`));
|
|
343
450
|
const entityIds = await this.repo.findEntitiesByCategory(category.id, ctx);
|
|
344
451
|
entities = entities.filter((e) => new Set(entityIds).has(e.id));
|
|
345
452
|
}
|
|
346
453
|
if (processed.filter?.brand) {
|
|
347
454
|
let brand = await this.repo.findBrandBySlug(listOrgId, processed.filter.brand, ctx);
|
|
348
|
-
if (!brand
|
|
455
|
+
if (!brand && isUUID.test(processed.filter.brand)) {
|
|
456
|
+
brand = await this.repo.findBrandById(processed.filter.brand, ctx, listOrgId);
|
|
457
|
+
}
|
|
349
458
|
if (brand) {
|
|
350
459
|
const brandEntityIds: string[] = [];
|
|
351
460
|
for (const entity of entities) {
|
|
@@ -401,19 +510,45 @@ export class EntityService {
|
|
|
401
510
|
archive(id: string, actor: Actor | null, ctx?: TxContext): Promise<Result<CatalogEntityHydrated>> { return this.changeStatus(id, "archived", actor, ctx); }
|
|
402
511
|
discontinue(id: string, actor: Actor | null, ctx?: TxContext): Promise<Result<CatalogEntityHydrated>> { return this.changeStatus(id, "discontinued", actor, ctx); }
|
|
403
512
|
|
|
404
|
-
async setAttributes(entityId: string, locale: string, attrs: SetAttributesInput, actor: Actor | null, ctx?:
|
|
513
|
+
async setAttributes(entityId: string, locale: string, attrs: SetAttributesInput, actor: Actor | null, ctx?: CatalogWriteContext): Promise<Result<void>> {
|
|
405
514
|
try { assertPermission(actor, "catalog:update"); } catch (error) { return Err(toCommerceError(error)); }
|
|
406
|
-
const
|
|
515
|
+
const txCtx = isWriteContextTransactional(ctx) ? ctx : undefined;
|
|
516
|
+
const entity = await this.repo.findEntityById(entityId, txCtx);
|
|
407
517
|
if (!entity) return Err(new CommerceNotFoundError("Entity not found."));
|
|
408
518
|
try { this.assertSameOrg(entity, actor); } catch (error) { return Err(toCommerceError(error)); }
|
|
409
|
-
await this.repo.
|
|
519
|
+
const beforeAttr = await this.repo.findAttributeByLocale(entityId, locale, txCtx);
|
|
520
|
+
const changedFieldPaths: string[] = [];
|
|
521
|
+
for (const field of attributeFields) {
|
|
522
|
+
if (attrs[field] === undefined) continue;
|
|
523
|
+
const before = beforeAttr?.[field] ?? null;
|
|
524
|
+
const after = attrs[field] ?? null;
|
|
525
|
+
if (before !== after) {
|
|
526
|
+
const path = `attributes.${locale}.${field}`;
|
|
527
|
+
if (isValidFieldPath(path)) changedFieldPaths.push(path);
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
await this.repo.upsertAttribute(entityId, locale, { title: attrs.title, subtitle: attrs.subtitle, description: attrs.description, richDescription: attrs.richDescription, seoTitle: attrs.seoTitle, seoDescription: attrs.seoDescription }, txCtx);
|
|
531
|
+
if (changedFieldPaths.length > 0) {
|
|
532
|
+
const afterHooks = this.deps.hooks.resolve("catalog.afterUpdate") as CatalogUpdateAfterHook[];
|
|
533
|
+
const context = catalogHookContext(this.deps, actor, ctx, "update");
|
|
534
|
+
context.context.changedFieldPaths = changedFieldPaths;
|
|
535
|
+
await runAfterHooks(afterHooks, entity, entity, "update", context);
|
|
536
|
+
}
|
|
410
537
|
return Ok(undefined);
|
|
411
538
|
}
|
|
412
539
|
|
|
413
|
-
async getAttributes(entityId: string, locale: string, ctx?: TxContext): Promise<Result<SellableAttribute>> {
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
540
|
+
async getAttributes(entityId: string, locale: string, actor: Actor | null, ctx?: TxContext): Promise<Result<SellableAttribute>> {
|
|
541
|
+
try {
|
|
542
|
+
assertPermission(actor, "catalog:read");
|
|
543
|
+
const entity = await this.repo.findEntityById(entityId, ctx, resolveOrgIdForCommerce(actor, this.deps.config));
|
|
544
|
+
if (!entity) return Err(new CommerceNotFoundError("Entity not found."));
|
|
545
|
+
if (!isCatalogEntityVisible(entity, actor)) return Err(new CommerceNotFoundError("Entity not found."));
|
|
546
|
+
const attr = await this.repo.findAttributeByLocale(entity.id, locale, ctx);
|
|
547
|
+
if (!attr) return Err(new CommerceNotFoundError(`Attributes for locale ${locale} not found.`));
|
|
548
|
+
return Ok(attr);
|
|
549
|
+
} catch (error) {
|
|
550
|
+
return Err(toCommerceError(error));
|
|
551
|
+
}
|
|
417
552
|
}
|
|
418
553
|
|
|
419
554
|
async createOptionType(input: CreateOptionTypeInput, actor: Actor | null, ctx?: TxContext): Promise<Result<OptionType>> {
|
|
@@ -467,7 +602,7 @@ export class EntityService {
|
|
|
467
602
|
if (!ov) return Err(new CommerceValidationError(`Option value "${optVal}" does not exist for option type "${optName}".`));
|
|
468
603
|
optionValueIds.push(ov.id);
|
|
469
604
|
}
|
|
470
|
-
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);
|
|
605
|
+
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);
|
|
471
606
|
await this.repo.createVariantOptionValues(optionValueIds.map((optionValueId) => ({ variantId: variant.id, optionValueId })), ctx);
|
|
472
607
|
return Ok(variant);
|
|
473
608
|
}
|
|
@@ -6,15 +6,20 @@ import { CatalogRepository } from "./repository/index.js";
|
|
|
6
6
|
import {
|
|
7
7
|
brands,
|
|
8
8
|
categories,
|
|
9
|
+
tags,
|
|
10
|
+
entityTags,
|
|
9
11
|
entityBrands,
|
|
10
12
|
entityCategories,
|
|
11
13
|
optionTypes,
|
|
12
14
|
optionValues,
|
|
13
15
|
sellableAttributes,
|
|
14
16
|
sellableCustomFields,
|
|
17
|
+
entityFieldDefinitions,
|
|
18
|
+
sellableEntityRevisions,
|
|
15
19
|
sellableEntities,
|
|
16
20
|
variantOptionValues,
|
|
17
21
|
variants,
|
|
22
|
+
catalogFieldOwnership,
|
|
18
23
|
} from "./schema.js";
|
|
19
24
|
import { CatalogServiceImpl } from "./service.js";
|
|
20
25
|
|
|
@@ -27,14 +32,19 @@ export const catalogModule = defineModule<
|
|
|
27
32
|
sellableEntities: typeof sellableEntities;
|
|
28
33
|
sellableAttributes: typeof sellableAttributes;
|
|
29
34
|
sellableCustomFields: typeof sellableCustomFields;
|
|
35
|
+
entityFieldDefinitions: typeof entityFieldDefinitions;
|
|
36
|
+
sellableEntityRevisions: typeof sellableEntityRevisions;
|
|
30
37
|
categories: typeof categories;
|
|
31
38
|
entityCategories: typeof entityCategories;
|
|
32
39
|
brands: typeof brands;
|
|
33
40
|
entityBrands: typeof entityBrands;
|
|
41
|
+
tags: typeof tags;
|
|
42
|
+
entityTags: typeof entityTags;
|
|
34
43
|
optionTypes: typeof optionTypes;
|
|
35
44
|
optionValues: typeof optionValues;
|
|
36
45
|
variants: typeof variants;
|
|
37
46
|
variantOptionValues: typeof variantOptionValues;
|
|
47
|
+
catalogFieldOwnership: typeof catalogFieldOwnership;
|
|
38
48
|
},
|
|
39
49
|
CatalogServiceImpl,
|
|
40
50
|
CatalogModuleDeps
|
|
@@ -45,14 +55,19 @@ export const catalogModule = defineModule<
|
|
|
45
55
|
sellableEntities,
|
|
46
56
|
sellableAttributes,
|
|
47
57
|
sellableCustomFields,
|
|
58
|
+
entityFieldDefinitions,
|
|
59
|
+
sellableEntityRevisions,
|
|
48
60
|
categories,
|
|
49
61
|
entityCategories,
|
|
50
62
|
brands,
|
|
51
63
|
entityBrands,
|
|
64
|
+
tags,
|
|
65
|
+
entityTags,
|
|
52
66
|
optionTypes,
|
|
53
67
|
optionValues,
|
|
54
68
|
variants,
|
|
55
69
|
variantOptionValues,
|
|
70
|
+
catalogFieldOwnership,
|
|
56
71
|
}),
|
|
57
72
|
service: (deps) =>
|
|
58
73
|
new CatalogServiceImpl({
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { CommerceValidationError } from "../../kernel/errors.js";
|
|
2
|
+
|
|
3
|
+
export type FieldPath = string;
|
|
4
|
+
export type FieldOwner = "platform" | "store" | "shared";
|
|
5
|
+
|
|
6
|
+
const fieldPathPattern = /^[A-Za-z0-9_-]+(?:\.[A-Za-z0-9_-]+)*$/;
|
|
7
|
+
|
|
8
|
+
export function isValidFieldPath(value: string): value is FieldPath {
|
|
9
|
+
return fieldPathPattern.test(value);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function validateFieldPath(value: string): FieldPath {
|
|
13
|
+
if (!isValidFieldPath(value)) {
|
|
14
|
+
throw new CommerceValidationError("Field path must contain dot-separated alphanumeric, underscore, or hyphen segments.");
|
|
15
|
+
}
|
|
16
|
+
return value;
|
|
17
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { hasPermission } from "../../auth/permissions.js";
|
|
2
|
+
import type { Actor } from "../../auth/types.js";
|
|
3
|
+
|
|
4
|
+
type CatalogVisibility = {
|
|
5
|
+
status: string;
|
|
6
|
+
isVisible: boolean;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export function canReadUnpublishedCatalog(actor: Actor | null): boolean {
|
|
10
|
+
return hasPermission(actor, "catalog:read:unpublished");
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function isCatalogEntityVisible(entity: CatalogVisibility, actor: Actor | null): boolean {
|
|
14
|
+
return canReadUnpublishedCatalog(actor) ||
|
|
15
|
+
(entity.status === "active" && entity.isVisible);
|
|
16
|
+
}
|