@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 { eq, and, inArray, type SQL } from "drizzle-orm";
1
+ import { eq, and, asc, count, desc, gt, inArray, isNull, lt, max, ne, or, type SQL } from "drizzle-orm";
2
2
  import type { TxContext } from "../../../kernel/database/tx-context.js";
3
3
  import { CommerceNotFoundError } from "../../../kernel/errors.js";
4
4
  import type {
@@ -9,15 +9,21 @@ import {
9
9
  sellableEntities,
10
10
  sellableAttributes,
11
11
  sellableCustomFields,
12
+ entityFieldDefinitions,
12
13
  categories,
13
14
  entityCategories,
14
15
  brands,
15
16
  entityBrands,
17
+ entityTags,
16
18
  optionTypes,
17
19
  optionValues,
18
20
  variants,
19
21
  variantOptionValues,
22
+ sellableEntityRevisions,
23
+ catalogFieldOwnership,
24
+ type SellableEntityRevisionSnapshot,
20
25
  } from "../schema.js";
26
+ import { entityMedia } from "../../media/schema.js";
21
27
 
22
28
  // Infer types from Drizzle schema
23
29
  export type SellableEntity = typeof sellableEntities.$inferSelect;
@@ -27,6 +33,8 @@ export type SellableAttributeInsert = typeof sellableAttributes.$inferInsert;
27
33
  export type SellableCustomField = typeof sellableCustomFields.$inferSelect;
28
34
  export type SellableCustomFieldInsert =
29
35
  typeof sellableCustomFields.$inferInsert;
36
+ export type EntityFieldDefinitionRecord = typeof entityFieldDefinitions.$inferSelect;
37
+ export type EntityFieldDefinitionInsert = typeof entityFieldDefinitions.$inferInsert;
30
38
  export type Category = typeof categories.$inferSelect;
31
39
  export type CategoryInsert = typeof categories.$inferInsert;
32
40
  export type EntityCategory = typeof entityCategories.$inferSelect;
@@ -43,6 +51,14 @@ export type Variant = typeof variants.$inferSelect;
43
51
  export type VariantInsert = typeof variants.$inferInsert;
44
52
  export type VariantOptionValue = typeof variantOptionValues.$inferSelect;
45
53
  export type VariantOptionValueInsert = typeof variantOptionValues.$inferInsert;
54
+ export type SellableEntityRevision = typeof sellableEntityRevisions.$inferSelect;
55
+ export type SellableEntityRevisionInsert = typeof sellableEntityRevisions.$inferInsert;
56
+ export type CatalogFieldOwnership = typeof catalogFieldOwnership.$inferSelect;
57
+ export type CatalogFieldOwnershipInsert = typeof catalogFieldOwnership.$inferInsert;
58
+ export type ProposedCustomField = SellableCustomField & {
59
+ entitySlug: string;
60
+ entityType: string;
61
+ };
46
62
 
47
63
  /**
48
64
  * CatalogRepository provides type-safe database operations for catalog entities.
@@ -108,6 +124,7 @@ export class CatalogRepository {
108
124
  filter?: {
109
125
  type?: string;
110
126
  status?: string;
127
+ isVisible?: boolean;
111
128
  ids?: string[];
112
129
  },
113
130
  ctx?: TxContext,
@@ -123,6 +140,9 @@ export class CatalogRepository {
123
140
  eq(sellableEntities.status, filter.status as SellableEntity["status"]),
124
141
  );
125
142
  }
143
+ if (filter?.isVisible !== undefined) {
144
+ conditions.push(eq(sellableEntities.isVisible, filter.isVisible));
145
+ }
126
146
  if (filter?.ids && filter.ids.length > 0) {
127
147
  conditions.push(inArray(sellableEntities.id, filter.ids));
128
148
  }
@@ -165,6 +185,247 @@ export class CatalogRepository {
165
185
  return result.length > 0;
166
186
  }
167
187
 
188
+ async findFieldOwnership(
189
+ entityId: string,
190
+ ctx?: TxContext,
191
+ storeId?: string,
192
+ ): Promise<CatalogFieldOwnership[]> {
193
+ const db = this.getDb(ctx);
194
+ const conditions = [eq(catalogFieldOwnership.entityId, entityId)];
195
+ if (storeId !== undefined) {
196
+ conditions.push(or(isNull(catalogFieldOwnership.storeId), eq(catalogFieldOwnership.storeId, storeId))!);
197
+ }
198
+ return db
199
+ .select()
200
+ .from(catalogFieldOwnership)
201
+ .where(and(...conditions))
202
+ .orderBy(asc(catalogFieldOwnership.fieldPath), asc(catalogFieldOwnership.updatedAt));
203
+ }
204
+
205
+ async findFieldOwnershipForResolution(
206
+ entityId: string,
207
+ storeId: string,
208
+ variantId: string | null,
209
+ ctx?: TxContext,
210
+ ): Promise<CatalogFieldOwnership[]> {
211
+ const db = this.getDb(ctx);
212
+ return db
213
+ .select()
214
+ .from(catalogFieldOwnership)
215
+ .where(and(
216
+ eq(catalogFieldOwnership.entityId, entityId),
217
+ or(isNull(catalogFieldOwnership.storeId), eq(catalogFieldOwnership.storeId, storeId)),
218
+ variantId == null
219
+ ? isNull(catalogFieldOwnership.variantId)
220
+ : or(isNull(catalogFieldOwnership.variantId), eq(catalogFieldOwnership.variantId, variantId)),
221
+ ));
222
+ }
223
+
224
+ async upsertFieldOwnership(
225
+ data: CatalogFieldOwnershipInsert,
226
+ ctx?: TxContext,
227
+ ): Promise<CatalogFieldOwnership> {
228
+ const db = this.getDb(ctx);
229
+ const rows = await db
230
+ .insert(catalogFieldOwnership)
231
+ .values(data)
232
+ .onConflictDoUpdate({
233
+ target: [
234
+ catalogFieldOwnership.organizationId,
235
+ catalogFieldOwnership.entityId,
236
+ catalogFieldOwnership.variantId,
237
+ catalogFieldOwnership.storeId,
238
+ catalogFieldOwnership.fieldPath,
239
+ ],
240
+ set: {
241
+ owner: data.owner,
242
+ updatedAt: data.updatedAt ?? new Date(),
243
+ },
244
+ })
245
+ .returning();
246
+ return rows[0]!;
247
+ }
248
+
249
+ async seedFieldOwnership(
250
+ data: CatalogFieldOwnershipInsert[],
251
+ ctx?: TxContext,
252
+ ): Promise<void> {
253
+ if (data.length === 0) return;
254
+ const db = this.getDb(ctx);
255
+ await db.insert(catalogFieldOwnership).values(data).onConflictDoNothing();
256
+ }
257
+
258
+ async findRevisionsByEntityId(
259
+ entityId: string,
260
+ ctx?: TxContext,
261
+ ): Promise<SellableEntityRevision[]> {
262
+ const db = this.getDb(ctx);
263
+ return db
264
+ .select()
265
+ .from(sellableEntityRevisions)
266
+ .where(eq(sellableEntityRevisions.entityId, entityId))
267
+ .orderBy(asc(sellableEntityRevisions.revision));
268
+ }
269
+
270
+ async findRevisionMarkers(
271
+ entityId: string,
272
+ since?: Date,
273
+ ctx?: TxContext,
274
+ ): Promise<Array<Pick<SellableEntityRevision, "createdAt" | "reason">>> {
275
+ const db = this.getDb(ctx);
276
+ return db
277
+ .select({
278
+ createdAt: sellableEntityRevisions.createdAt,
279
+ reason: sellableEntityRevisions.reason,
280
+ })
281
+ .from(sellableEntityRevisions)
282
+ .where(and(
283
+ eq(sellableEntityRevisions.entityId, entityId),
284
+ ...(since ? [gt(sellableEntityRevisions.createdAt, since)] : []),
285
+ ))
286
+ .orderBy(asc(sellableEntityRevisions.createdAt));
287
+ }
288
+
289
+ async findLatestRevision(
290
+ entityId: string,
291
+ ctx?: TxContext,
292
+ ): Promise<SellableEntityRevision | undefined> {
293
+ const db = this.getDb(ctx);
294
+ const rows = await db
295
+ .select()
296
+ .from(sellableEntityRevisions)
297
+ .where(eq(sellableEntityRevisions.entityId, entityId))
298
+ .orderBy(desc(sellableEntityRevisions.revision))
299
+ .limit(1);
300
+ return rows[0];
301
+ }
302
+
303
+ async findRevisionById(
304
+ entityId: string,
305
+ id: string,
306
+ ctx?: TxContext,
307
+ ): Promise<SellableEntityRevision | undefined> {
308
+ const db = this.getDb(ctx);
309
+ const rows = await db
310
+ .select()
311
+ .from(sellableEntityRevisions)
312
+ .where(
313
+ and(
314
+ eq(sellableEntityRevisions.entityId, entityId),
315
+ eq(sellableEntityRevisions.id, id),
316
+ ),
317
+ );
318
+ return rows[0];
319
+ }
320
+
321
+ async createRevision(
322
+ data: Omit<SellableEntityRevisionInsert, "id" | "revision" | "pinned">,
323
+ ctx?: TxContext,
324
+ ): Promise<SellableEntityRevision> {
325
+ const db = this.getDb(ctx);
326
+ const rows = await db
327
+ .select({ revision: max(sellableEntityRevisions.revision) })
328
+ .from(sellableEntityRevisions)
329
+ .where(eq(sellableEntityRevisions.entityId, data.entityId));
330
+ const revision = (rows[0]?.revision ?? 0) + 1;
331
+ const inserted = await db
332
+ .insert(sellableEntityRevisions)
333
+ .values({
334
+ ...data,
335
+ revision,
336
+ pinned: revision === 1,
337
+ })
338
+ .returning();
339
+ return inserted[0]!;
340
+ }
341
+
342
+ async updateRevision(
343
+ id: string,
344
+ data: Partial<Pick<SellableEntityRevisionInsert, "createdAt" | "pinned">>,
345
+ ctx?: TxContext,
346
+ ): Promise<SellableEntityRevision | undefined> {
347
+ const db = this.getDb(ctx);
348
+ const rows = await db
349
+ .update(sellableEntityRevisions)
350
+ .set(data)
351
+ .where(eq(sellableEntityRevisions.id, id))
352
+ .returning();
353
+ return rows[0];
354
+ }
355
+
356
+ async deleteRevisionsOlderThan(
357
+ organizationId: string,
358
+ cutoff: Date,
359
+ ctx?: TxContext,
360
+ ): Promise<number> {
361
+ const db = this.getDb(ctx);
362
+ const deleted = await db
363
+ .delete(sellableEntityRevisions)
364
+ .where(
365
+ and(
366
+ eq(sellableEntityRevisions.organizationId, organizationId),
367
+ lt(sellableEntityRevisions.createdAt, cutoff),
368
+ eq(sellableEntityRevisions.pinned, false),
369
+ gt(sellableEntityRevisions.revision, 1),
370
+ ),
371
+ )
372
+ .returning({ id: sellableEntityRevisions.id });
373
+ return deleted.length;
374
+ }
375
+
376
+ async snapshotEntity(
377
+ entityId: string,
378
+ ctx?: TxContext,
379
+ ): Promise<SellableEntityRevisionSnapshot> {
380
+ const entity = await this.findEntityById(entityId, ctx);
381
+ if (!entity) throw new CommerceNotFoundError("Entity not found.");
382
+ const [attributes, customFields, categoriesForEntity, brandsForEntity, media, tagsForEntity] = await Promise.all([
383
+ this.findAttributesByEntityId(entityId, ctx),
384
+ this.findAllCustomFieldsByEntityId(entityId, ctx),
385
+ this.findEntityCategories(entityId, ctx),
386
+ this.findEntityBrands(entityId, ctx),
387
+ this.findEntityMedia(entityId, ctx),
388
+ this.findEntityTags(entityId, ctx),
389
+ ]);
390
+ return {
391
+ entity: entity as unknown as Record<string, unknown>,
392
+ attributes: attributes.sort((a, b) => a.locale.localeCompare(b.locale) || a.id.localeCompare(b.id)) as unknown as Array<Record<string, unknown>>,
393
+ customFields: customFields.sort((a, b) => a.locale.localeCompare(b.locale) || a.fieldName.localeCompare(b.fieldName) || a.id.localeCompare(b.id)) as unknown as Array<Record<string, unknown>>,
394
+ media: media.sort((a, b) => a.mediaAssetId.localeCompare(b.mediaAssetId) || (a.variantId ?? "").localeCompare(b.variantId ?? "")) as unknown as Array<Record<string, unknown>>,
395
+ categories: categoriesForEntity.sort((a, b) => a.categoryId.localeCompare(b.categoryId)) as unknown as Array<Record<string, unknown>>,
396
+ brands: brandsForEntity.sort((a, b) => a.brandId.localeCompare(b.brandId)) as unknown as Array<Record<string, unknown>>,
397
+ tags: tagsForEntity.sort((a, b) => a.tagId.localeCompare(b.tagId)) as unknown as Array<Record<string, unknown>>,
398
+ };
399
+ }
400
+
401
+ async findEntityMedia(
402
+ entityId: string,
403
+ ctx?: TxContext,
404
+ ): Promise<Array<typeof entityMedia.$inferSelect>> {
405
+ const db = this.getDb(ctx);
406
+ return db
407
+ .select()
408
+ .from(entityMedia)
409
+ .where(eq(entityMedia.entityId, entityId));
410
+ }
411
+
412
+ async deleteEntityMediaByEntityId(
413
+ entityId: string,
414
+ ctx?: TxContext,
415
+ ): Promise<void> {
416
+ const db = this.getDb(ctx);
417
+ await db.delete(entityMedia).where(eq(entityMedia.entityId, entityId));
418
+ }
419
+
420
+ async createEntityMedia(
421
+ data: typeof entityMedia.$inferInsert,
422
+ ctx?: TxContext,
423
+ ): Promise<typeof entityMedia.$inferSelect> {
424
+ const db = this.getDb(ctx);
425
+ const rows = await db.insert(entityMedia).values(data).returning();
426
+ return rows[0]!;
427
+ }
428
+
168
429
  // ─────────────────────────────────────────────────────────────────────────────
169
430
  // Sellable Attributes
170
431
  // ─────────────────────────────────────────────────────────────────────────────
@@ -252,6 +513,22 @@ export class CatalogRepository {
252
513
  async findCustomFieldsByEntityId(
253
514
  entityId: string,
254
515
  ctx?: TxContext,
516
+ ): Promise<SellableCustomField[]> {
517
+ const db = this.getDb(ctx);
518
+ return db
519
+ .select()
520
+ .from(sellableCustomFields)
521
+ .where(
522
+ and(
523
+ eq(sellableCustomFields.entityId, entityId),
524
+ eq(sellableCustomFields.status, "approved"),
525
+ ),
526
+ );
527
+ }
528
+
529
+ async findAllCustomFieldsByEntityId(
530
+ entityId: string,
531
+ ctx?: TxContext,
255
532
  ): Promise<SellableCustomField[]> {
256
533
  const db = this.getDb(ctx);
257
534
  return db
@@ -260,6 +537,137 @@ export class CatalogRepository {
260
537
  .where(eq(sellableCustomFields.entityId, entityId));
261
538
  }
262
539
 
540
+ async findProposedCustomField(
541
+ entityId: string,
542
+ fieldName: string,
543
+ locale = "en",
544
+ ctx?: TxContext,
545
+ ): Promise<SellableCustomField | undefined> {
546
+ const db = this.getDb(ctx);
547
+ const rows = await db
548
+ .select()
549
+ .from(sellableCustomFields)
550
+ .where(
551
+ and(
552
+ eq(sellableCustomFields.entityId, entityId),
553
+ eq(sellableCustomFields.fieldName, fieldName),
554
+ eq(sellableCustomFields.locale, locale),
555
+ eq(sellableCustomFields.status, "proposed"),
556
+ ),
557
+ )
558
+ .orderBy(desc(sellableCustomFields.createdAt), desc(sellableCustomFields.id))
559
+ .limit(1);
560
+ return rows[0];
561
+ }
562
+
563
+ async updateProposedCustomField(
564
+ id: string,
565
+ data: Partial<Omit<SellableCustomFieldInsert, "id">>,
566
+ ctx?: TxContext,
567
+ ): Promise<SellableCustomField | undefined> {
568
+ const db = this.getDb(ctx);
569
+ const rows = await db
570
+ .update(sellableCustomFields)
571
+ .set({ ...data, updatedAt: new Date() })
572
+ .where(
573
+ and(
574
+ eq(sellableCustomFields.id, id),
575
+ eq(sellableCustomFields.status, "proposed"),
576
+ ),
577
+ )
578
+ .returning();
579
+ return rows[0];
580
+ }
581
+
582
+ async rejectOtherProposedCustomFields(
583
+ entityId: string,
584
+ fieldName: string,
585
+ locale: string,
586
+ excludeId: string,
587
+ ctx?: TxContext,
588
+ ): Promise<number> {
589
+ const db = this.getDb(ctx);
590
+ const rows = await db
591
+ .update(sellableCustomFields)
592
+ .set({ status: "rejected", updatedAt: new Date() })
593
+ .where(
594
+ and(
595
+ eq(sellableCustomFields.entityId, entityId),
596
+ eq(sellableCustomFields.fieldName, fieldName),
597
+ eq(sellableCustomFields.locale, locale),
598
+ eq(sellableCustomFields.status, "proposed"),
599
+ ne(sellableCustomFields.id, excludeId),
600
+ ),
601
+ )
602
+ .returning({ id: sellableCustomFields.id });
603
+ return rows.length;
604
+ }
605
+
606
+ async listProposedCustomFields(
607
+ organizationId: string,
608
+ pagination: { page: number; limit: number },
609
+ filter?: { entityType?: string },
610
+ ctx?: TxContext,
611
+ ): Promise<{ items: ProposedCustomField[]; total: number }> {
612
+ const db = this.getDb(ctx);
613
+ const page = Math.max(1, pagination.page);
614
+ const limit = Math.max(1, pagination.limit);
615
+ const conditions: SQL[] = [
616
+ eq(sellableCustomFields.status, "proposed"),
617
+ eq(sellableEntities.organizationId, organizationId),
618
+ ];
619
+ if (filter?.entityType !== undefined) {
620
+ conditions.push(eq(sellableEntities.type, filter.entityType));
621
+ }
622
+ const where = and(...conditions);
623
+ const rows = await db
624
+ .select({
625
+ customField: sellableCustomFields,
626
+ entitySlug: sellableEntities.slug,
627
+ entityType: sellableEntities.type,
628
+ })
629
+ .from(sellableCustomFields)
630
+ .innerJoin(sellableEntities, eq(sellableCustomFields.entityId, sellableEntities.id))
631
+ .where(where)
632
+ .orderBy(asc(sellableCustomFields.createdAt), asc(sellableCustomFields.id))
633
+ .limit(limit)
634
+ .offset((page - 1) * limit);
635
+ const totalRows = await db
636
+ .select({ count: count() })
637
+ .from(sellableCustomFields)
638
+ .innerJoin(sellableEntities, eq(sellableCustomFields.entityId, sellableEntities.id))
639
+ .where(where);
640
+ return {
641
+ items: rows.map(({ customField, entitySlug, entityType }) => ({
642
+ ...customField,
643
+ entitySlug,
644
+ entityType,
645
+ })),
646
+ total: totalRows[0]?.count ?? 0,
647
+ };
648
+ }
649
+
650
+ async findCustomFieldByName(
651
+ entityId: string,
652
+ fieldName: string,
653
+ locale = "en",
654
+ ctx?: TxContext,
655
+ ): Promise<SellableCustomField | undefined> {
656
+ const db = this.getDb(ctx);
657
+ const rows = await db
658
+ .select()
659
+ .from(sellableCustomFields)
660
+ .where(
661
+ and(
662
+ eq(sellableCustomFields.entityId, entityId),
663
+ eq(sellableCustomFields.fieldName, fieldName),
664
+ eq(sellableCustomFields.locale, locale),
665
+ eq(sellableCustomFields.status, "approved"),
666
+ ),
667
+ );
668
+ return rows[0];
669
+ }
670
+
263
671
  async createCustomField(
264
672
  data: SellableCustomFieldInsert,
265
673
  ctx?: TxContext,
@@ -269,6 +677,54 @@ export class CatalogRepository {
269
677
  return rows[0]!;
270
678
  }
271
679
 
680
+ async updateCustomField(
681
+ id: string,
682
+ data: Partial<Omit<SellableCustomFieldInsert, "id">>,
683
+ ctx?: TxContext,
684
+ ): Promise<SellableCustomField | undefined> {
685
+ const db = this.getDb(ctx);
686
+ const rows = await db
687
+ .update(sellableCustomFields)
688
+ .set({ ...data, updatedAt: new Date() })
689
+ .where(eq(sellableCustomFields.id, id))
690
+ .returning();
691
+ return rows[0];
692
+ }
693
+
694
+ async upsertCustomField(
695
+ entityId: string,
696
+ fieldName: string,
697
+ locale: string,
698
+ data: Omit<SellableCustomFieldInsert, "entityId" | "fieldName">,
699
+ ctx?: TxContext,
700
+ ): Promise<SellableCustomField> {
701
+ const existing = await this.findCustomFieldByName(entityId, fieldName, locale, ctx);
702
+ if (existing) {
703
+ const updated = await this.updateCustomField(existing.id, data, ctx);
704
+ return updated!;
705
+ }
706
+ return this.createCustomField({ ...data, entityId, fieldName, locale }, ctx);
707
+ }
708
+
709
+ async deleteCustomField(
710
+ entityId: string,
711
+ fieldName: string,
712
+ locale = "en",
713
+ ctx?: TxContext,
714
+ ): Promise<void> {
715
+ const db = this.getDb(ctx);
716
+ await db
717
+ .delete(sellableCustomFields)
718
+ .where(
719
+ and(
720
+ eq(sellableCustomFields.entityId, entityId),
721
+ eq(sellableCustomFields.fieldName, fieldName),
722
+ eq(sellableCustomFields.locale, locale),
723
+ eq(sellableCustomFields.status, "approved"),
724
+ ),
725
+ );
726
+ }
727
+
272
728
  async deleteCustomFieldsByEntityId(
273
729
  entityId: string,
274
730
  ctx?: TxContext,
@@ -279,6 +735,79 @@ export class CatalogRepository {
279
735
  .where(eq(sellableCustomFields.entityId, entityId));
280
736
  }
281
737
 
738
+ async findEntityFieldDefinitions(
739
+ organizationId: string,
740
+ entityType?: string,
741
+ ctx?: TxContext,
742
+ ): Promise<EntityFieldDefinitionRecord[]> {
743
+ const db = this.getDb(ctx);
744
+ const conditions = [eq(entityFieldDefinitions.organizationId, organizationId)];
745
+ if (entityType !== undefined) {
746
+ conditions.push(eq(entityFieldDefinitions.entityType, entityType));
747
+ }
748
+ return db
749
+ .select()
750
+ .from(entityFieldDefinitions)
751
+ .where(and(...conditions))
752
+ .orderBy(asc(entityFieldDefinitions.entityType), asc(entityFieldDefinitions.sortOrder), asc(entityFieldDefinitions.name));
753
+ }
754
+
755
+ async findActiveEntityFieldDefinitions(
756
+ organizationId: string,
757
+ entityType: string,
758
+ ctx?: TxContext,
759
+ ): Promise<EntityFieldDefinitionRecord[]> {
760
+ const db = this.getDb(ctx);
761
+ return db
762
+ .select()
763
+ .from(entityFieldDefinitions)
764
+ .where(
765
+ and(
766
+ eq(entityFieldDefinitions.organizationId, organizationId),
767
+ eq(entityFieldDefinitions.entityType, entityType),
768
+ eq(entityFieldDefinitions.status, "active"),
769
+ ),
770
+ )
771
+ .orderBy(asc(entityFieldDefinitions.sortOrder), asc(entityFieldDefinitions.name));
772
+ }
773
+
774
+ async findEntityFieldDefinitionById(
775
+ organizationId: string,
776
+ id: string,
777
+ ctx?: TxContext,
778
+ ): Promise<EntityFieldDefinitionRecord | undefined> {
779
+ const db = this.getDb(ctx);
780
+ const rows = await db
781
+ .select()
782
+ .from(entityFieldDefinitions)
783
+ .where(and(eq(entityFieldDefinitions.organizationId, organizationId), eq(entityFieldDefinitions.id, id)));
784
+ return rows[0];
785
+ }
786
+
787
+ async createEntityFieldDefinition(
788
+ data: EntityFieldDefinitionInsert,
789
+ ctx?: TxContext,
790
+ ): Promise<EntityFieldDefinitionRecord> {
791
+ const db = this.getDb(ctx);
792
+ const rows = await db.insert(entityFieldDefinitions).values(data).returning();
793
+ return rows[0]!;
794
+ }
795
+
796
+ async updateEntityFieldDefinition(
797
+ organizationId: string,
798
+ id: string,
799
+ data: Partial<Omit<EntityFieldDefinitionInsert, "id" | "organizationId">>,
800
+ ctx?: TxContext,
801
+ ): Promise<EntityFieldDefinitionRecord | undefined> {
802
+ const db = this.getDb(ctx);
803
+ const rows = await db
804
+ .update(entityFieldDefinitions)
805
+ .set({ ...data, updatedAt: new Date() })
806
+ .where(and(eq(entityFieldDefinitions.organizationId, organizationId), eq(entityFieldDefinitions.id, id)))
807
+ .returning();
808
+ return rows[0];
809
+ }
810
+
282
811
  // ─────────────────────────────────────────────────────────────────────────────
283
812
  // Categories
284
813
  // ─────────────────────────────────────────────────────────────────────────────
@@ -286,12 +815,17 @@ export class CatalogRepository {
286
815
  async findCategoryById(
287
816
  id: string,
288
817
  ctx?: TxContext,
818
+ orgId?: string,
289
819
  ): Promise<Category | undefined> {
290
820
  const db = this.getDb(ctx);
291
821
  const rows = await db
292
822
  .select()
293
823
  .from(categories)
294
- .where(eq(categories.id, id));
824
+ .where(
825
+ orgId
826
+ ? and(eq(categories.id, id), eq(categories.organizationId, orgId))
827
+ : eq(categories.id, id),
828
+ );
295
829
  return rows[0];
296
830
  }
297
831
 
@@ -438,9 +972,16 @@ export class CatalogRepository {
438
972
  // Brands
439
973
  // ─────────────────────────────────────────────────────────────────────────────
440
974
 
441
- async findBrandById(id: string, ctx?: TxContext): Promise<Brand | undefined> {
975
+ async findBrandById(id: string, ctx?: TxContext, orgId?: string): Promise<Brand | undefined> {
442
976
  const db = this.getDb(ctx);
443
- const rows = await db.select().from(brands).where(eq(brands.id, id));
977
+ const rows = await db
978
+ .select()
979
+ .from(brands)
980
+ .where(
981
+ orgId
982
+ ? and(eq(brands.id, id), eq(brands.organizationId, orgId))
983
+ : eq(brands.id, id),
984
+ );
444
985
  return rows[0];
445
986
  }
446
987
 
@@ -511,6 +1052,27 @@ export class CatalogRepository {
511
1052
  .where(eq(entityBrands.entityId, entityId));
512
1053
  }
513
1054
 
1055
+ async findEntityTags(
1056
+ entityId: string,
1057
+ ctx?: TxContext,
1058
+ ): Promise<Array<typeof entityTags.$inferSelect>> {
1059
+ const db = this.getDb(ctx);
1060
+ return db
1061
+ .select()
1062
+ .from(entityTags)
1063
+ .where(eq(entityTags.entityId, entityId));
1064
+ }
1065
+
1066
+ async addEntityTag(entityId: string, tagId: string, ctx?: TxContext): Promise<void> {
1067
+ const db = this.getDb(ctx);
1068
+ await db.insert(entityTags).values({ entityId, tagId }).onConflictDoNothing();
1069
+ }
1070
+
1071
+ async deleteEntityTagsByEntityId(entityId: string, ctx?: TxContext): Promise<void> {
1072
+ const db = this.getDb(ctx);
1073
+ await db.delete(entityTags).where(eq(entityTags.entityId, entityId));
1074
+ }
1075
+
514
1076
  async addEntityToBrand(
515
1077
  entityId: string,
516
1078
  brandId: string,
@@ -0,0 +1,35 @@
1
+ import type { SellableEntityRevisionSnapshot } from "./schema.js";
2
+
3
+ export function canonicalSerialize(value: unknown): string {
4
+ if (value instanceof Date) return JSON.stringify(value.toISOString());
5
+ if (Array.isArray(value)) return `[${value.map(canonicalSerialize).join(",")}]`;
6
+ if (value !== null && typeof value === "object") {
7
+ const record = value as Record<string, unknown>;
8
+ return `{${Object.keys(record)
9
+ .sort()
10
+ .map((key) => `${JSON.stringify(key)}:${canonicalSerialize(record[key])}`)
11
+ .join(",")}}`;
12
+ }
13
+ return JSON.stringify(value) ?? "null";
14
+ }
15
+
16
+ // Row bookkeeping timestamps are excluded from comparison only at row roots,
17
+ // never inside nested jsonb where a user key may share the same name.
18
+ function stripRowTimestamps(row: Record<string, unknown>): Record<string, unknown> {
19
+ const { createdAt, updatedAt, ...rest } = row;
20
+ void createdAt;
21
+ void updatedAt;
22
+ return rest;
23
+ }
24
+
25
+ export function comparableSerialize(snapshot: SellableEntityRevisionSnapshot): string {
26
+ return canonicalSerialize({
27
+ entity: stripRowTimestamps(snapshot.entity),
28
+ attributes: snapshot.attributes.map(stripRowTimestamps),
29
+ customFields: snapshot.customFields.map(stripRowTimestamps),
30
+ media: snapshot.media.map(stripRowTimestamps),
31
+ categories: snapshot.categories.map(stripRowTimestamps),
32
+ brands: snapshot.brands.map(stripRowTimestamps),
33
+ tags: (snapshot.tags ?? []).map(stripRowTimestamps),
34
+ });
35
+ }