@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.
Files changed (360) hide show
  1. package/dist/auth/auth-schema-guard.d.ts +2444 -0
  2. package/dist/auth/auth-schema-guard.d.ts.map +1 -0
  3. package/dist/auth/auth-schema-guard.js +113 -0
  4. package/dist/auth/auth-schema.d.ts +228 -0
  5. package/dist/auth/auth-schema.d.ts.map +1 -1
  6. package/dist/auth/auth-schema.js +24 -2
  7. package/dist/auth/middleware.d.ts +1 -0
  8. package/dist/auth/middleware.d.ts.map +1 -1
  9. package/dist/auth/middleware.js +13 -5
  10. package/dist/auth/org.d.ts +8 -2
  11. package/dist/auth/org.d.ts.map +1 -1
  12. package/dist/auth/org.js +12 -4
  13. package/dist/auth/organization-guard.d.ts +5 -0
  14. package/dist/auth/organization-guard.d.ts.map +1 -0
  15. package/dist/auth/organization-guard.js +124 -0
  16. package/dist/auth/permissions.d.ts +10 -0
  17. package/dist/auth/permissions.d.ts.map +1 -1
  18. package/dist/auth/permissions.js +31 -7
  19. package/dist/auth/role-authority.d.ts +44 -0
  20. package/dist/auth/role-authority.d.ts.map +1 -0
  21. package/dist/auth/role-authority.js +85 -0
  22. package/dist/auth/setup.d.ts.map +1 -1
  23. package/dist/auth/setup.js +17 -0
  24. package/dist/auth/strict-org-resolution.d.ts.map +1 -1
  25. package/dist/auth/strict-org-resolution.js +9 -1
  26. package/dist/auth/types.d.ts +1 -1
  27. package/dist/auth/types.d.ts.map +1 -1
  28. package/dist/config/defaults.d.ts.map +1 -1
  29. package/dist/config/defaults.js +2 -0
  30. package/dist/config/types.d.ts +33 -4
  31. package/dist/config/types.d.ts.map +1 -1
  32. package/dist/hooks/checkout.d.ts.map +1 -1
  33. package/dist/hooks/checkout.js +10 -9
  34. package/dist/index.d.ts +9 -6
  35. package/dist/index.d.ts.map +1 -1
  36. package/dist/index.js +5 -3
  37. package/dist/interfaces/rest/audit-middleware.d.ts.map +1 -1
  38. package/dist/interfaces/rest/audit-middleware.js +2 -0
  39. package/dist/interfaces/rest/customer-portal.d.ts.map +1 -1
  40. package/dist/interfaces/rest/customer-portal.js +9 -8
  41. package/dist/interfaces/rest/index.d.ts.map +1 -1
  42. package/dist/interfaces/rest/index.js +4 -0
  43. package/dist/interfaces/rest/route-coverage.d.ts +14 -0
  44. package/dist/interfaces/rest/route-coverage.d.ts.map +1 -0
  45. package/dist/interfaces/rest/route-coverage.js +81 -0
  46. package/dist/interfaces/rest/router.d.ts +7 -2
  47. package/dist/interfaces/rest/router.d.ts.map +1 -1
  48. package/dist/interfaces/rest/router.js +8 -4
  49. package/dist/interfaces/rest/routes/admin/compensation-failures.d.ts.map +1 -1
  50. package/dist/interfaces/rest/routes/admin/compensation-failures.js +7 -5
  51. package/dist/interfaces/rest/routes/admin/custom-field-proposals.d.ts +5 -0
  52. package/dist/interfaces/rest/routes/admin/custom-field-proposals.d.ts.map +1 -0
  53. package/dist/interfaces/rest/routes/admin/custom-field-proposals.js +22 -0
  54. package/dist/interfaces/rest/routes/admin/entity-field-definitions.d.ts +5 -0
  55. package/dist/interfaces/rest/routes/admin/entity-field-definitions.d.ts.map +1 -0
  56. package/dist/interfaces/rest/routes/admin/entity-field-definitions.js +39 -0
  57. package/dist/interfaces/rest/routes/admin/permissions.d.ts +1 -1
  58. package/dist/interfaces/rest/routes/admin/permissions.d.ts.map +1 -1
  59. package/dist/interfaces/rest/routes/admin/permissions.js +2 -0
  60. package/dist/interfaces/rest/routes/admin/staff.d.ts.map +1 -1
  61. package/dist/interfaces/rest/routes/admin/staff.js +126 -66
  62. package/dist/interfaces/rest/routes/admin-jobs.js +3 -3
  63. package/dist/interfaces/rest/routes/audit.js +3 -3
  64. package/dist/interfaces/rest/routes/carts.d.ts.map +1 -1
  65. package/dist/interfaces/rest/routes/carts.js +13 -10
  66. package/dist/interfaces/rest/routes/catalog.d.ts.map +1 -1
  67. package/dist/interfaces/rest/routes/catalog.js +47 -4
  68. package/dist/interfaces/rest/routes/checkout.d.ts.map +1 -1
  69. package/dist/interfaces/rest/routes/checkout.js +30 -35
  70. package/dist/interfaces/rest/routes/documents.d.ts.map +1 -1
  71. package/dist/interfaces/rest/routes/documents.js +10 -5
  72. package/dist/interfaces/rest/routes/inventory.d.ts.map +1 -1
  73. package/dist/interfaces/rest/routes/inventory.js +2 -0
  74. package/dist/interfaces/rest/routes/media.d.ts.map +1 -1
  75. package/dist/interfaces/rest/routes/media.js +7 -3
  76. package/dist/interfaces/rest/routes/orders.d.ts.map +1 -1
  77. package/dist/interfaces/rest/routes/orders.js +26 -8
  78. package/dist/interfaces/rest/routes/pricing.js +2 -2
  79. package/dist/interfaces/rest/routes/promotions.d.ts.map +1 -1
  80. package/dist/interfaces/rest/routes/promotions.js +13 -10
  81. package/dist/interfaces/rest/routes/search.d.ts.map +1 -1
  82. package/dist/interfaces/rest/routes/search.js +14 -1
  83. package/dist/interfaces/rest/schemas/admin-custom-field-proposals.d.ts +134 -0
  84. package/dist/interfaces/rest/schemas/admin-custom-field-proposals.d.ts.map +1 -0
  85. package/dist/interfaces/rest/schemas/admin-custom-field-proposals.js +26 -0
  86. package/dist/interfaces/rest/schemas/admin-entity-field-definitions.d.ts +592 -0
  87. package/dist/interfaces/rest/schemas/admin-entity-field-definitions.d.ts.map +1 -0
  88. package/dist/interfaces/rest/schemas/admin-entity-field-definitions.js +109 -0
  89. package/dist/interfaces/rest/schemas/carts.d.ts +7 -0
  90. package/dist/interfaces/rest/schemas/carts.d.ts.map +1 -1
  91. package/dist/interfaces/rest/schemas/catalog.d.ts +1364 -401
  92. package/dist/interfaces/rest/schemas/catalog.d.ts.map +1 -1
  93. package/dist/interfaces/rest/schemas/catalog.js +86 -0
  94. package/dist/interfaces/rest/schemas/customer-portal.d.ts +35 -0
  95. package/dist/interfaces/rest/schemas/customer-portal.d.ts.map +1 -1
  96. package/dist/interfaces/rest/schemas/orders.d.ts +204 -0
  97. package/dist/interfaces/rest/schemas/orders.d.ts.map +1 -1
  98. package/dist/interfaces/rest/schemas/pricing.d.ts +1 -0
  99. package/dist/interfaces/rest/schemas/pricing.d.ts.map +1 -1
  100. package/dist/interfaces/rest/schemas/responses.d.ts +71 -0
  101. package/dist/interfaces/rest/schemas/responses.d.ts.map +1 -1
  102. package/dist/interfaces/rest/schemas/search.d.ts +1 -0
  103. package/dist/interfaces/rest/schemas/search.d.ts.map +1 -1
  104. package/dist/interfaces/rest/schemas/search.js +1 -0
  105. package/dist/interfaces/rest/utils.d.ts +15 -0
  106. package/dist/interfaces/rest/utils.d.ts.map +1 -1
  107. package/dist/interfaces/rest/utils.js +45 -2
  108. package/dist/kernel/compensation/executor.js +2 -2
  109. package/dist/kernel/database/tx-context.d.ts +8 -0
  110. package/dist/kernel/database/tx-context.d.ts.map +1 -1
  111. package/dist/kernel/database/tx-context.js +13 -0
  112. package/dist/kernel/hooks/create-context.d.ts +2 -0
  113. package/dist/kernel/hooks/create-context.d.ts.map +1 -1
  114. package/dist/kernel/hooks/create-context.js +1 -0
  115. package/dist/kernel/hooks/types.d.ts +2 -0
  116. package/dist/kernel/hooks/types.d.ts.map +1 -1
  117. package/dist/kernel/plugin/manifest.d.ts.map +1 -1
  118. package/dist/kernel/plugin/manifest.js +9 -5
  119. package/dist/modules/analytics/service.js +2 -2
  120. package/dist/modules/audit/service.js +3 -3
  121. package/dist/modules/cart/repository/index.d.ts +13 -7
  122. package/dist/modules/cart/repository/index.d.ts.map +1 -1
  123. package/dist/modules/cart/repository/index.js +16 -10
  124. package/dist/modules/cart/schema.d.ts +17 -0
  125. package/dist/modules/cart/schema.d.ts.map +1 -1
  126. package/dist/modules/cart/schema.js +4 -0
  127. package/dist/modules/cart/service.d.ts +22 -9
  128. package/dist/modules/cart/service.d.ts.map +1 -1
  129. package/dist/modules/cart/service.js +57 -33
  130. package/dist/modules/catalog/brand-service.js +7 -7
  131. package/dist/modules/catalog/category-service.js +7 -7
  132. package/dist/modules/catalog/entity-service.d.ts +19 -10
  133. package/dist/modules/catalog/entity-service.d.ts.map +1 -1
  134. package/dist/modules/catalog/entity-service.js +180 -51
  135. package/dist/modules/catalog/module.d.ts +6 -1
  136. package/dist/modules/catalog/module.d.ts.map +1 -1
  137. package/dist/modules/catalog/module.js +6 -1
  138. package/dist/modules/catalog/ownership.d.ts +5 -0
  139. package/dist/modules/catalog/ownership.d.ts.map +1 -0
  140. package/dist/modules/catalog/ownership.js +11 -0
  141. package/dist/modules/catalog/read-policy.d.ts +9 -0
  142. package/dist/modules/catalog/read-policy.d.ts.map +1 -0
  143. package/dist/modules/catalog/read-policy.js +8 -0
  144. package/dist/modules/catalog/repository/index.d.ts +55 -3
  145. package/dist/modules/catalog/repository/index.d.ts.map +1 -1
  146. package/dist/modules/catalog/repository/index.js +335 -6
  147. package/dist/modules/catalog/revision.d.ts +4 -0
  148. package/dist/modules/catalog/revision.d.ts.map +1 -0
  149. package/dist/modules/catalog/revision.js +33 -0
  150. package/dist/modules/catalog/schema.d.ts +979 -110
  151. package/dist/modules/catalog/schema.d.ts.map +1 -1
  152. package/dist/modules/catalog/schema.js +112 -2
  153. package/dist/modules/catalog/schemas.d.ts +2 -0
  154. package/dist/modules/catalog/schemas.d.ts.map +1 -1
  155. package/dist/modules/catalog/schemas.js +2 -0
  156. package/dist/modules/catalog/service.d.ts +80 -15
  157. package/dist/modules/catalog/service.d.ts.map +1 -1
  158. package/dist/modules/catalog/service.js +522 -14
  159. package/dist/modules/channels/adapter.d.ts +85 -0
  160. package/dist/modules/channels/adapter.d.ts.map +1 -1
  161. package/dist/modules/channels/adapter.js +9 -1
  162. package/dist/modules/customers/module.d.ts.map +1 -1
  163. package/dist/modules/customers/module.js +1 -0
  164. package/dist/modules/customers/service.d.ts +2 -0
  165. package/dist/modules/customers/service.d.ts.map +1 -1
  166. package/dist/modules/customers/service.js +21 -20
  167. package/dist/modules/documents/module.d.ts.map +1 -1
  168. package/dist/modules/documents/module.js +1 -0
  169. package/dist/modules/documents/service.d.ts +18 -4
  170. package/dist/modules/documents/service.d.ts.map +1 -1
  171. package/dist/modules/documents/service.js +54 -16
  172. package/dist/modules/fulfillment/module.d.ts.map +1 -1
  173. package/dist/modules/fulfillment/module.js +1 -0
  174. package/dist/modules/fulfillment/service.d.ts +2 -0
  175. package/dist/modules/fulfillment/service.d.ts.map +1 -1
  176. package/dist/modules/fulfillment/service.js +15 -6
  177. package/dist/modules/inventory/service.d.ts.map +1 -1
  178. package/dist/modules/inventory/service.js +15 -14
  179. package/dist/modules/media/module.d.ts +68 -0
  180. package/dist/modules/media/module.d.ts.map +1 -1
  181. package/dist/modules/media/module.js +2 -0
  182. package/dist/modules/media/schema.d.ts +70 -0
  183. package/dist/modules/media/schema.d.ts.map +1 -1
  184. package/dist/modules/media/schema.js +20 -2
  185. package/dist/modules/media/service.d.ts +8 -1
  186. package/dist/modules/media/service.d.ts.map +1 -1
  187. package/dist/modules/media/service.js +71 -23
  188. package/dist/modules/orders/guest-access.d.ts +27 -0
  189. package/dist/modules/orders/guest-access.d.ts.map +1 -0
  190. package/dist/modules/orders/guest-access.js +46 -0
  191. package/dist/modules/orders/module.d.ts.map +1 -1
  192. package/dist/modules/orders/module.js +1 -0
  193. package/dist/modules/orders/quote.d.ts.map +1 -1
  194. package/dist/modules/orders/quote.js +1 -0
  195. package/dist/modules/orders/repository/index.d.ts +1 -1
  196. package/dist/modules/orders/repository/index.d.ts.map +1 -1
  197. package/dist/modules/orders/repository/index.js +2 -2
  198. package/dist/modules/orders/schema.d.ts +17 -0
  199. package/dist/modules/orders/schema.d.ts.map +1 -1
  200. package/dist/modules/orders/schema.js +5 -3
  201. package/dist/modules/orders/service.d.ts +16 -7
  202. package/dist/modules/orders/service.d.ts.map +1 -1
  203. package/dist/modules/orders/service.js +184 -55
  204. package/dist/modules/pricing/module.d.ts.map +1 -1
  205. package/dist/modules/pricing/module.js +1 -0
  206. package/dist/modules/pricing/schema.d.ts +17 -0
  207. package/dist/modules/pricing/schema.d.ts.map +1 -1
  208. package/dist/modules/pricing/schema.js +1 -0
  209. package/dist/modules/pricing/schemas.d.ts +1 -0
  210. package/dist/modules/pricing/schemas.d.ts.map +1 -1
  211. package/dist/modules/pricing/schemas.js +1 -0
  212. package/dist/modules/pricing/service.d.ts +2 -0
  213. package/dist/modules/pricing/service.d.ts.map +1 -1
  214. package/dist/modules/pricing/service.js +18 -12
  215. package/dist/modules/promotions/module.d.ts.map +1 -1
  216. package/dist/modules/promotions/module.js +1 -0
  217. package/dist/modules/promotions/service.d.ts +3 -0
  218. package/dist/modules/promotions/service.d.ts.map +1 -1
  219. package/dist/modules/promotions/service.js +19 -11
  220. package/dist/modules/search/adapter.d.ts +8 -0
  221. package/dist/modules/search/adapter.d.ts.map +1 -1
  222. package/dist/modules/search/module.d.ts.map +1 -1
  223. package/dist/modules/search/module.js +3 -0
  224. package/dist/modules/search/service.d.ts +5 -0
  225. package/dist/modules/search/service.d.ts.map +1 -1
  226. package/dist/modules/search/service.js +131 -11
  227. package/dist/modules/settings/module.d.ts.map +1 -1
  228. package/dist/modules/settings/module.js +1 -0
  229. package/dist/modules/settings/service.d.ts +3 -0
  230. package/dist/modules/settings/service.d.ts.map +1 -1
  231. package/dist/modules/settings/service.js +6 -4
  232. package/dist/modules/shipping/service.js +9 -9
  233. package/dist/modules/tax/module.d.ts.map +1 -1
  234. package/dist/modules/tax/module.js +1 -0
  235. package/dist/modules/tax/service.d.ts +3 -0
  236. package/dist/modules/tax/service.d.ts.map +1 -1
  237. package/dist/modules/tax/service.js +11 -9
  238. package/dist/modules/webhooks/hook.js +2 -2
  239. package/dist/modules/webhooks/module.d.ts.map +1 -1
  240. package/dist/modules/webhooks/module.js +1 -0
  241. package/dist/modules/webhooks/service.d.ts +2 -0
  242. package/dist/modules/webhooks/service.d.ts.map +1 -1
  243. package/dist/modules/webhooks/service.js +4 -4
  244. package/dist/runtime/kernel-modules.d.ts +73 -0
  245. package/dist/runtime/kernel-modules.d.ts.map +1 -1
  246. package/dist/runtime/server.d.ts.map +1 -1
  247. package/dist/runtime/server.js +10 -2
  248. package/dist/test-utils/create-pglite-adapter.d.ts.map +1 -1
  249. package/dist/test-utils/create-pglite-adapter.js +35 -13
  250. package/dist/test-utils/create-test-config.d.ts.map +1 -1
  251. package/dist/test-utils/create-test-config.js +4 -0
  252. package/dist/test-utils/rest-api-test-utils.d.ts.map +1 -1
  253. package/dist/test-utils/rest-api-test-utils.js +2 -0
  254. package/dist/test-utils/test-actors.js +2 -2
  255. package/dist/utils/id.d.ts +2 -0
  256. package/dist/utils/id.d.ts.map +1 -1
  257. package/dist/utils/id.js +11 -0
  258. package/package.json +6 -5
  259. package/src/auth/auth-schema-guard.ts +143 -0
  260. package/src/auth/auth-schema.ts +50 -19
  261. package/src/auth/middleware.ts +17 -5
  262. package/src/auth/org.ts +15 -3
  263. package/src/auth/organization-guard.ts +158 -0
  264. package/src/auth/permissions.ts +31 -8
  265. package/src/auth/role-authority.ts +107 -0
  266. package/src/auth/setup.ts +17 -0
  267. package/src/auth/strict-org-resolution.ts +7 -1
  268. package/src/auth/types.ts +1 -1
  269. package/src/config/defaults.ts +2 -0
  270. package/src/config/types.ts +32 -4
  271. package/src/hooks/checkout.ts +19 -12
  272. package/src/index.ts +30 -6
  273. package/src/interfaces/rest/audit-middleware.ts +2 -0
  274. package/src/interfaces/rest/customer-portal.ts +31 -24
  275. package/src/interfaces/rest/index.ts +4 -0
  276. package/src/interfaces/rest/route-coverage.ts +103 -0
  277. package/src/interfaces/rest/router.ts +16 -10
  278. package/src/interfaces/rest/routes/admin/compensation-failures.ts +8 -5
  279. package/src/interfaces/rest/routes/admin/custom-field-proposals.ts +37 -0
  280. package/src/interfaces/rest/routes/admin/entity-field-definitions.ts +89 -0
  281. package/src/interfaces/rest/routes/admin/permissions.ts +3 -1
  282. package/src/interfaces/rest/routes/admin/staff.ts +161 -72
  283. package/src/interfaces/rest/routes/admin-jobs.ts +3 -3
  284. package/src/interfaces/rest/routes/audit.ts +3 -3
  285. package/src/interfaces/rest/routes/carts.ts +17 -7
  286. package/src/interfaces/rest/routes/catalog.ts +96 -0
  287. package/src/interfaces/rest/routes/checkout.ts +41 -38
  288. package/src/interfaces/rest/routes/documents.ts +15 -1
  289. package/src/interfaces/rest/routes/inventory.ts +2 -0
  290. package/src/interfaces/rest/routes/media.ts +8 -3
  291. package/src/interfaces/rest/routes/orders.ts +27 -8
  292. package/src/interfaces/rest/routes/pricing.ts +2 -2
  293. package/src/interfaces/rest/routes/promotions.ts +12 -9
  294. package/src/interfaces/rest/routes/search.ts +19 -1
  295. package/src/interfaces/rest/schemas/admin-custom-field-proposals.ts +29 -0
  296. package/src/interfaces/rest/schemas/admin-entity-field-definitions.ts +120 -0
  297. package/src/interfaces/rest/schemas/catalog.ts +92 -0
  298. package/src/interfaces/rest/schemas/search.ts +2 -0
  299. package/src/interfaces/rest/utils.ts +61 -2
  300. package/src/kernel/compensation/executor.ts +2 -2
  301. package/src/kernel/database/tx-context.ts +25 -0
  302. package/src/kernel/hooks/create-context.ts +3 -0
  303. package/src/kernel/hooks/types.ts +2 -0
  304. package/src/kernel/plugin/manifest.ts +9 -5
  305. package/src/modules/analytics/service.ts +2 -2
  306. package/src/modules/audit/service.ts +3 -3
  307. package/src/modules/cart/repository/index.ts +22 -8
  308. package/src/modules/cart/schema.ts +4 -0
  309. package/src/modules/cart/service.ts +62 -27
  310. package/src/modules/catalog/brand-service.ts +7 -7
  311. package/src/modules/catalog/category-service.ts +7 -7
  312. package/src/modules/catalog/entity-service.ts +188 -53
  313. package/src/modules/catalog/module.ts +15 -0
  314. package/src/modules/catalog/ownership.ts +17 -0
  315. package/src/modules/catalog/read-policy.ts +16 -0
  316. package/src/modules/catalog/repository/index.ts +566 -4
  317. package/src/modules/catalog/revision.ts +35 -0
  318. package/src/modules/catalog/schema.ts +169 -1
  319. package/src/modules/catalog/schemas.ts +2 -0
  320. package/src/modules/catalog/service.ts +766 -26
  321. package/src/modules/channels/adapter.ts +107 -1
  322. package/src/modules/customers/module.ts +2 -0
  323. package/src/modules/customers/service.ts +23 -22
  324. package/src/modules/documents/module.ts +2 -0
  325. package/src/modules/documents/service.ts +74 -11
  326. package/src/modules/fulfillment/module.ts +2 -0
  327. package/src/modules/fulfillment/service.ts +17 -6
  328. package/src/modules/inventory/service.ts +15 -24
  329. package/src/modules/media/module.ts +2 -0
  330. package/src/modules/media/schema.ts +23 -2
  331. package/src/modules/media/service.ts +89 -28
  332. package/src/modules/orders/guest-access.ts +55 -0
  333. package/src/modules/orders/module.ts +1 -0
  334. package/src/modules/orders/quote.ts +1 -0
  335. package/src/modules/orders/repository/index.ts +2 -0
  336. package/src/modules/orders/schema.ts +5 -3
  337. package/src/modules/orders/service.ts +302 -52
  338. package/src/modules/pricing/module.ts +2 -0
  339. package/src/modules/pricing/schema.ts +1 -0
  340. package/src/modules/pricing/schemas.ts +1 -0
  341. package/src/modules/pricing/service.ts +21 -18
  342. package/src/modules/promotions/module.ts +2 -0
  343. package/src/modules/promotions/service.ts +25 -16
  344. package/src/modules/search/adapter.ts +8 -0
  345. package/src/modules/search/module.ts +3 -0
  346. package/src/modules/search/service.ts +142 -12
  347. package/src/modules/settings/module.ts +2 -0
  348. package/src/modules/settings/service.ts +8 -4
  349. package/src/modules/shipping/service.ts +9 -9
  350. package/src/modules/tax/module.ts +1 -0
  351. package/src/modules/tax/service.ts +13 -9
  352. package/src/modules/webhooks/hook.ts +2 -2
  353. package/src/modules/webhooks/module.ts +2 -0
  354. package/src/modules/webhooks/service.ts +6 -4
  355. package/src/runtime/server.ts +16 -2
  356. package/src/test-utils/create-pglite-adapter.ts +34 -12
  357. package/src/test-utils/create-test-config.ts +10 -6
  358. package/src/test-utils/rest-api-test-utils.ts +2 -0
  359. package/src/test-utils/test-actors.ts +2 -2
  360. package/src/utils/id.ts +20 -0
@@ -1,4 +1,4 @@
1
- import { resolveOrgId } from "../../auth/org.js";
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(private readonly deps: CatalogServiceDeps) {}
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 = resolveOrgId(actor);
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 validateAndCreateCustomFields(entityId: string, entityType: string, customFields: Record<string, unknown> | undefined, ctx?: TxContext): Promise<Result<void>> {
106
- if (!customFields) return Ok(undefined);
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
- if (!entityConfig) return Ok(undefined);
109
- const definitionMap = new Map(entityConfig.fields.map((f) => [f.name, f]));
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 = (type === "select" ? "text" : type) as SellableCustomField["fieldType"];
138
- const insertData: SellableCustomFieldInsert = { entityId, fieldName: name, fieldType };
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
- insertData.textValue = value as string;
198
+ case "select":
199
+ insertData.textValue = normalized as string;
143
200
  break;
144
201
  case "number":
145
- insertData.numberValue = value as number;
202
+ insertData.numberValue = normalized as number;
146
203
  break;
147
204
  case "boolean":
148
- insertData.booleanValue = value as boolean;
205
+ insertData.booleanValue = normalized as boolean;
149
206
  break;
150
207
  case "date":
151
- insertData.dateValue = value instanceof Date ? value : new Date(value as string);
208
+ insertData.dateValue = normalized instanceof Date ? normalized : new Date(normalized as string);
152
209
  break;
153
210
  case "json":
154
- insertData.jsonValue = value;
211
+ insertData.jsonValue = normalized;
155
212
  break;
156
213
  }
157
- await this.repo.createCustomField(insertData, ctx);
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 = resolveOrgId(actor);
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 entity = await this.repo.createEntity({ organizationId: orgId, type: processedInput.type, slug: processedInput.slug, status: "draft", isVisible: false, ...(processedInput.sourceStoreId !== undefined ? { sourceStoreId: processedInput.sourceStoreId } : {}), ...(processedInput.taxClass !== undefined ? { taxClass: processedInput.taxClass } : {}), metadata: processedInput.metadata ?? {} }, ctx);
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
- const customFieldsResult = await this.validateAndCreateCustomFields(entity.id, entity.type, processedInput.customFields, ctx);
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?: TxContext): Promise<Result<CatalogEntityHydrated>> { try {
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, ctx);
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 = createHookContext({ actor, tx: ctx?.tx ?? null, logger: createLogger("catalog.update"), services: this.deps.services, context: { moduleName: "catalog" }, ...hookDatabaseArg(this.deps.database) });
314
+ const context: HookContext = catalogHookContext(this.deps, actor, ctx, "update");
240
315
  const processed = await runBeforeHooks(beforeHooks, input, "update", context);
241
- 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 } : {}) }, ctx);
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, ctx);
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?: GetOptions, actor?: Actor | null, ctx?: TxContext): Promise<Result<CatalogEntityHydrated>> {
265
- 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) });
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
- async getBySlug(slug: string, options?: GetOptions, actor?: Actor | null, ctx?: TxContext): Promise<Result<CatalogEntityHydrated>> {
294
- 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) });
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 = resolveOrgId(actor ?? null);
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?: Actor | null, ctx?: TxContext): Promise<Result<CatalogListResult>> {
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 = resolveOrgId(resolvedActor);
336
- let entities = await this.repo.findEntities(listOrgId, { ...(processed.filter?.type ? { type: processed.filter.type } : {}), ...(processed.filter?.status ? { status: processed.filter.status } : {}) }, ctx);
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) brand = await this.repo.findBrandById(processed.filter.brand, ctx);
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?: TxContext): Promise<Result<void>> {
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 entity = await this.repo.findEntityById(entityId, ctx);
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.upsertAttribute(entityId, locale, { title: attrs.title, subtitle: attrs.subtitle, description: attrs.description, richDescription: attrs.richDescription, seoTitle: attrs.seoTitle, seoDescription: attrs.seoDescription }, ctx);
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
- const attr = await this.repo.findAttributeByLocale(entityId, locale, ctx);
415
- if (!attr) return Err(new CommerceNotFoundError(`Attributes for locale ${locale} not found.`));
416
- return Ok(attr);
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
+ }