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