@trokky/trokky 2.0.3

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 (639) hide show
  1. package/dist/adapters/filesystem/filesystem-adapter.d.ts +80 -0
  2. package/dist/adapters/filesystem/filesystem-adapter.d.ts.map +1 -0
  3. package/dist/adapters/filesystem/filesystem-adapter.js +1370 -0
  4. package/dist/adapters/filesystem/filesystem-adapter.js.map +1 -0
  5. package/dist/adapters/filesystem/index.d.ts +29 -0
  6. package/dist/adapters/filesystem/index.d.ts.map +1 -0
  7. package/dist/adapters/filesystem/index.js +31 -0
  8. package/dist/adapters/filesystem/index.js.map +1 -0
  9. package/dist/adapters/filesystem/types.d.ts +111 -0
  10. package/dist/adapters/filesystem/types.d.ts.map +1 -0
  11. package/dist/adapters/filesystem/types.js +2 -0
  12. package/dist/adapters/filesystem/types.js.map +1 -0
  13. package/dist/adapters/filesystem-data/filesystem-data-adapter.d.ts +111 -0
  14. package/dist/adapters/filesystem-data/filesystem-data-adapter.d.ts.map +1 -0
  15. package/dist/adapters/filesystem-data/filesystem-data-adapter.js +1363 -0
  16. package/dist/adapters/filesystem-data/filesystem-data-adapter.js.map +1 -0
  17. package/dist/adapters/filesystem-data/index.d.ts +3 -0
  18. package/dist/adapters/filesystem-data/index.d.ts.map +1 -0
  19. package/dist/adapters/filesystem-data/index.js +13 -0
  20. package/dist/adapters/filesystem-data/index.js.map +1 -0
  21. package/dist/adapters/filesystem-data/types.d.ts +147 -0
  22. package/dist/adapters/filesystem-data/types.d.ts.map +1 -0
  23. package/dist/adapters/filesystem-data/types.js +2 -0
  24. package/dist/adapters/filesystem-data/types.js.map +1 -0
  25. package/dist/adapters/filesystem-media/filesystem-media-adapter.d.ts +32 -0
  26. package/dist/adapters/filesystem-media/filesystem-media-adapter.d.ts.map +1 -0
  27. package/dist/adapters/filesystem-media/filesystem-media-adapter.js +639 -0
  28. package/dist/adapters/filesystem-media/filesystem-media-adapter.js.map +1 -0
  29. package/dist/adapters/filesystem-media/index.d.ts +3 -0
  30. package/dist/adapters/filesystem-media/index.d.ts.map +1 -0
  31. package/dist/adapters/filesystem-media/index.js +13 -0
  32. package/dist/adapters/filesystem-media/index.js.map +1 -0
  33. package/dist/adapters/filesystem-media/types.d.ts +76 -0
  34. package/dist/adapters/filesystem-media/types.d.ts.map +1 -0
  35. package/dist/adapters/filesystem-media/types.js +2 -0
  36. package/dist/adapters/filesystem-media/types.js.map +1 -0
  37. package/dist/adapters/postgres-data/index.d.ts +3 -0
  38. package/dist/adapters/postgres-data/index.d.ts.map +1 -0
  39. package/dist/adapters/postgres-data/index.js +13 -0
  40. package/dist/adapters/postgres-data/index.js.map +1 -0
  41. package/dist/adapters/postgres-data/postgres-data-adapter.d.ts +154 -0
  42. package/dist/adapters/postgres-data/postgres-data-adapter.d.ts.map +1 -0
  43. package/dist/adapters/postgres-data/postgres-data-adapter.js +1432 -0
  44. package/dist/adapters/postgres-data/postgres-data-adapter.js.map +1 -0
  45. package/dist/adapters/postgres-data/types.d.ts +139 -0
  46. package/dist/adapters/postgres-data/types.d.ts.map +1 -0
  47. package/dist/adapters/postgres-data/types.js +2 -0
  48. package/dist/adapters/postgres-data/types.js.map +1 -0
  49. package/dist/core/adapters/registry.d.ts +79 -0
  50. package/dist/core/adapters/registry.d.ts.map +1 -0
  51. package/dist/core/adapters/registry.js +149 -0
  52. package/dist/core/adapters/registry.js.map +1 -0
  53. package/dist/core/core/engine.d.ts +551 -0
  54. package/dist/core/core/engine.d.ts.map +1 -0
  55. package/dist/core/core/engine.js +1074 -0
  56. package/dist/core/core/engine.js.map +1 -0
  57. package/dist/core/crypto/adapter.d.ts +63 -0
  58. package/dist/core/crypto/adapter.d.ts.map +1 -0
  59. package/dist/core/crypto/adapter.js +63 -0
  60. package/dist/core/crypto/adapter.js.map +1 -0
  61. package/dist/core/crypto/fallback-adapter.d.ts +23 -0
  62. package/dist/core/crypto/fallback-adapter.d.ts.map +1 -0
  63. package/dist/core/crypto/fallback-adapter.js +195 -0
  64. package/dist/core/crypto/fallback-adapter.js.map +1 -0
  65. package/dist/core/crypto/node-adapter.d.ts +20 -0
  66. package/dist/core/crypto/node-adapter.d.ts.map +1 -0
  67. package/dist/core/crypto/node-adapter.js +85 -0
  68. package/dist/core/crypto/node-adapter.js.map +1 -0
  69. package/dist/core/crypto/password-hash.d.ts +73 -0
  70. package/dist/core/crypto/password-hash.d.ts.map +1 -0
  71. package/dist/core/crypto/password-hash.js +244 -0
  72. package/dist/core/crypto/password-hash.js.map +1 -0
  73. package/dist/core/crypto/webcrypto-adapter.d.ts +24 -0
  74. package/dist/core/crypto/webcrypto-adapter.d.ts.map +1 -0
  75. package/dist/core/crypto/webcrypto-adapter.js +158 -0
  76. package/dist/core/crypto/webcrypto-adapter.js.map +1 -0
  77. package/dist/core/errors/index.d.ts +30 -0
  78. package/dist/core/errors/index.d.ts.map +1 -0
  79. package/dist/core/errors/index.js +61 -0
  80. package/dist/core/errors/index.js.map +1 -0
  81. package/dist/core/events/event-bus.d.ts +121 -0
  82. package/dist/core/events/event-bus.d.ts.map +1 -0
  83. package/dist/core/events/event-bus.js +631 -0
  84. package/dist/core/events/event-bus.js.map +1 -0
  85. package/dist/core/events/index.d.ts +17 -0
  86. package/dist/core/events/index.d.ts.map +1 -0
  87. package/dist/core/events/index.js +22 -0
  88. package/dist/core/events/index.js.map +1 -0
  89. package/dist/core/events/memory-storage.d.ts +57 -0
  90. package/dist/core/events/memory-storage.d.ts.map +1 -0
  91. package/dist/core/events/memory-storage.js +175 -0
  92. package/dist/core/events/memory-storage.js.map +1 -0
  93. package/dist/core/events/types.d.ts +434 -0
  94. package/dist/core/events/types.d.ts.map +1 -0
  95. package/dist/core/events/types.js +9 -0
  96. package/dist/core/events/types.js.map +1 -0
  97. package/dist/core/events/utils.d.ts +134 -0
  98. package/dist/core/events/utils.d.ts.map +1 -0
  99. package/dist/core/events/utils.js +333 -0
  100. package/dist/core/events/utils.js.map +1 -0
  101. package/dist/core/fields/conditional.d.ts +115 -0
  102. package/dist/core/fields/conditional.d.ts.map +1 -0
  103. package/dist/core/fields/conditional.js +296 -0
  104. package/dist/core/fields/conditional.js.map +1 -0
  105. package/dist/core/fields/field-type.d.ts +218 -0
  106. package/dist/core/fields/field-type.d.ts.map +1 -0
  107. package/dist/core/fields/field-type.js +17 -0
  108. package/dist/core/fields/field-type.js.map +1 -0
  109. package/dist/core/fields/helpers.d.ts +86 -0
  110. package/dist/core/fields/helpers.d.ts.map +1 -0
  111. package/dist/core/fields/helpers.js +260 -0
  112. package/dist/core/fields/helpers.js.map +1 -0
  113. package/dist/core/fields/index.d.ts +5 -0
  114. package/dist/core/fields/index.d.ts.map +1 -0
  115. package/dist/core/fields/index.js +6 -0
  116. package/dist/core/fields/index.js.map +1 -0
  117. package/dist/core/fields/registry.d.ts +78 -0
  118. package/dist/core/fields/registry.d.ts.map +1 -0
  119. package/dist/core/fields/registry.js +185 -0
  120. package/dist/core/fields/registry.js.map +1 -0
  121. package/dist/core/index.d.ts +39 -0
  122. package/dist/core/index.d.ts.map +1 -0
  123. package/dist/core/index.js +57 -0
  124. package/dist/core/index.js.map +1 -0
  125. package/dist/core/media/image-processor.d.ts +24 -0
  126. package/dist/core/media/image-processor.d.ts.map +1 -0
  127. package/dist/core/media/image-processor.js +69 -0
  128. package/dist/core/media/image-processor.js.map +1 -0
  129. package/dist/core/media/processors/cloudflare-images.d.ts +83 -0
  130. package/dist/core/media/processors/cloudflare-images.d.ts.map +1 -0
  131. package/dist/core/media/processors/cloudflare-images.js +241 -0
  132. package/dist/core/media/processors/cloudflare-images.js.map +1 -0
  133. package/dist/core/media/processors/cloudflare-transform-store.d.ts +127 -0
  134. package/dist/core/media/processors/cloudflare-transform-store.d.ts.map +1 -0
  135. package/dist/core/media/processors/cloudflare-transform-store.js +237 -0
  136. package/dist/core/media/processors/cloudflare-transform-store.js.map +1 -0
  137. package/dist/core/media/processors/cloudflare-transformations.d.ts +81 -0
  138. package/dist/core/media/processors/cloudflare-transformations.d.ts.map +1 -0
  139. package/dist/core/media/processors/cloudflare-transformations.js +144 -0
  140. package/dist/core/media/processors/cloudflare-transformations.js.map +1 -0
  141. package/dist/core/media/processors/index.d.ts +9 -0
  142. package/dist/core/media/processors/index.d.ts.map +1 -0
  143. package/dist/core/media/processors/index.js +13 -0
  144. package/dist/core/media/processors/index.js.map +1 -0
  145. package/dist/core/media/processors/noop.d.ts +22 -0
  146. package/dist/core/media/processors/noop.d.ts.map +1 -0
  147. package/dist/core/media/processors/noop.js +38 -0
  148. package/dist/core/media/processors/noop.js.map +1 -0
  149. package/dist/core/media/processors/sharp.d.ts +30 -0
  150. package/dist/core/media/processors/sharp.d.ts.map +1 -0
  151. package/dist/core/media/processors/sharp.js +126 -0
  152. package/dist/core/media/processors/sharp.js.map +1 -0
  153. package/dist/core/media/processors/types.d.ts +67 -0
  154. package/dist/core/media/processors/types.d.ts.map +1 -0
  155. package/dist/core/media/processors/types.js +42 -0
  156. package/dist/core/media/processors/types.js.map +1 -0
  157. package/dist/core/media/processors/workers-images.d.ts +112 -0
  158. package/dist/core/media/processors/workers-images.d.ts.map +1 -0
  159. package/dist/core/media/processors/workers-images.js +257 -0
  160. package/dist/core/media/processors/workers-images.js.map +1 -0
  161. package/dist/core/schema/registry.d.ts +41 -0
  162. package/dist/core/schema/registry.d.ts.map +1 -0
  163. package/dist/core/schema/registry.js +248 -0
  164. package/dist/core/schema/registry.js.map +1 -0
  165. package/dist/core/schema/singleton.d.ts +107 -0
  166. package/dist/core/schema/singleton.d.ts.map +1 -0
  167. package/dist/core/schema/singleton.js +180 -0
  168. package/dist/core/schema/singleton.js.map +1 -0
  169. package/dist/core/security/auth.d.ts +120 -0
  170. package/dist/core/security/auth.d.ts.map +1 -0
  171. package/dist/core/security/auth.js +274 -0
  172. package/dist/core/security/auth.js.map +1 -0
  173. package/dist/core/security/captcha/index.d.ts +21 -0
  174. package/dist/core/security/captcha/index.d.ts.map +1 -0
  175. package/dist/core/security/captcha/index.js +34 -0
  176. package/dist/core/security/captcha/index.js.map +1 -0
  177. package/dist/core/security/captcha/recaptcha.d.ts +41 -0
  178. package/dist/core/security/captcha/recaptcha.d.ts.map +1 -0
  179. package/dist/core/security/captcha/recaptcha.js +89 -0
  180. package/dist/core/security/captcha/recaptcha.js.map +1 -0
  181. package/dist/core/security/captcha/turnstile.d.ts +41 -0
  182. package/dist/core/security/captcha/turnstile.d.ts.map +1 -0
  183. package/dist/core/security/captcha/turnstile.js +90 -0
  184. package/dist/core/security/captcha/turnstile.js.map +1 -0
  185. package/dist/core/security/captcha/types.d.ts +66 -0
  186. package/dist/core/security/captcha/types.d.ts.map +1 -0
  187. package/dist/core/security/captcha/types.js +7 -0
  188. package/dist/core/security/captcha/types.js.map +1 -0
  189. package/dist/core/security/mfa/email-otp.d.ts +91 -0
  190. package/dist/core/security/mfa/email-otp.d.ts.map +1 -0
  191. package/dist/core/security/mfa/email-otp.js +264 -0
  192. package/dist/core/security/mfa/email-otp.js.map +1 -0
  193. package/dist/core/security/mfa/index.d.ts +8 -0
  194. package/dist/core/security/mfa/index.d.ts.map +1 -0
  195. package/dist/core/security/mfa/index.js +8 -0
  196. package/dist/core/security/mfa/index.js.map +1 -0
  197. package/dist/core/security/mfa/totp.d.ts +87 -0
  198. package/dist/core/security/mfa/totp.d.ts.map +1 -0
  199. package/dist/core/security/mfa/totp.js +293 -0
  200. package/dist/core/security/mfa/totp.js.map +1 -0
  201. package/dist/core/security/middleware.d.ts +102 -0
  202. package/dist/core/security/middleware.d.ts.map +1 -0
  203. package/dist/core/security/middleware.js +261 -0
  204. package/dist/core/security/middleware.js.map +1 -0
  205. package/dist/core/security/oauth/google.d.ts +76 -0
  206. package/dist/core/security/oauth/google.d.ts.map +1 -0
  207. package/dist/core/security/oauth/google.js +154 -0
  208. package/dist/core/security/oauth/google.js.map +1 -0
  209. package/dist/core/security/oauth/index.d.ts +8 -0
  210. package/dist/core/security/oauth/index.d.ts.map +1 -0
  211. package/dist/core/security/oauth/index.js +7 -0
  212. package/dist/core/security/oauth/index.js.map +1 -0
  213. package/dist/core/security/oauth2/authorization-server.d.ts +195 -0
  214. package/dist/core/security/oauth2/authorization-server.d.ts.map +1 -0
  215. package/dist/core/security/oauth2/authorization-server.js +569 -0
  216. package/dist/core/security/oauth2/authorization-server.js.map +1 -0
  217. package/dist/core/security/oauth2/index.d.ts +9 -0
  218. package/dist/core/security/oauth2/index.d.ts.map +1 -0
  219. package/dist/core/security/oauth2/index.js +9 -0
  220. package/dist/core/security/oauth2/index.js.map +1 -0
  221. package/dist/core/security/rate-limiter.d.ts +15 -0
  222. package/dist/core/security/rate-limiter.d.ts.map +1 -0
  223. package/dist/core/security/rate-limiter.js +48 -0
  224. package/dist/core/security/rate-limiter.js.map +1 -0
  225. package/dist/core/security/validation.d.ts +20 -0
  226. package/dist/core/security/validation.d.ts.map +1 -0
  227. package/dist/core/security/validation.js +273 -0
  228. package/dist/core/security/validation.js.map +1 -0
  229. package/dist/core/services/auth-service.d.ts +128 -0
  230. package/dist/core/services/auth-service.d.ts.map +1 -0
  231. package/dist/core/services/auth-service.js +530 -0
  232. package/dist/core/services/auth-service.js.map +1 -0
  233. package/dist/core/services/captcha-service.d.ts +44 -0
  234. package/dist/core/services/captcha-service.d.ts.map +1 -0
  235. package/dist/core/services/captcha-service.js +82 -0
  236. package/dist/core/services/captcha-service.js.map +1 -0
  237. package/dist/core/services/document-service.d.ts +59 -0
  238. package/dist/core/services/document-service.d.ts.map +1 -0
  239. package/dist/core/services/document-service.js +269 -0
  240. package/dist/core/services/document-service.js.map +1 -0
  241. package/dist/core/services/media-service.d.ts +40 -0
  242. package/dist/core/services/media-service.d.ts.map +1 -0
  243. package/dist/core/services/media-service.js +396 -0
  244. package/dist/core/services/media-service.js.map +1 -0
  245. package/dist/core/services/mfa-service.d.ts +123 -0
  246. package/dist/core/services/mfa-service.d.ts.map +1 -0
  247. package/dist/core/services/mfa-service.js +606 -0
  248. package/dist/core/services/mfa-service.js.map +1 -0
  249. package/dist/core/services/oauth-service.d.ts +58 -0
  250. package/dist/core/services/oauth-service.d.ts.map +1 -0
  251. package/dist/core/services/oauth-service.js +253 -0
  252. package/dist/core/services/oauth-service.js.map +1 -0
  253. package/dist/core/services/oauth2-server-service.d.ts +61 -0
  254. package/dist/core/services/oauth2-server-service.d.ts.map +1 -0
  255. package/dist/core/services/oauth2-server-service.js +259 -0
  256. package/dist/core/services/oauth2-server-service.js.map +1 -0
  257. package/dist/core/services/passkey-service.d.ts +59 -0
  258. package/dist/core/services/passkey-service.d.ts.map +1 -0
  259. package/dist/core/services/passkey-service.js +238 -0
  260. package/dist/core/services/passkey-service.js.map +1 -0
  261. package/dist/core/services/token-service.d.ts +31 -0
  262. package/dist/core/services/token-service.d.ts.map +1 -0
  263. package/dist/core/services/token-service.js +132 -0
  264. package/dist/core/services/token-service.js.map +1 -0
  265. package/dist/core/services/trusted-device-service.d.ts +39 -0
  266. package/dist/core/services/trusted-device-service.d.ts.map +1 -0
  267. package/dist/core/services/trusted-device-service.js +123 -0
  268. package/dist/core/services/trusted-device-service.js.map +1 -0
  269. package/dist/core/services/user-service.d.ts +38 -0
  270. package/dist/core/services/user-service.d.ts.map +1 -0
  271. package/dist/core/services/user-service.js +265 -0
  272. package/dist/core/services/user-service.js.map +1 -0
  273. package/dist/core/studio/integration.d.ts +54 -0
  274. package/dist/core/studio/integration.d.ts.map +1 -0
  275. package/dist/core/studio/integration.js +326 -0
  276. package/dist/core/studio/integration.js.map +1 -0
  277. package/dist/core/types/index.d.ts +282 -0
  278. package/dist/core/types/index.d.ts.map +1 -0
  279. package/dist/core/types/index.js +83 -0
  280. package/dist/core/types/index.js.map +1 -0
  281. package/dist/core/types/mail.d.ts +90 -0
  282. package/dist/core/types/mail.d.ts.map +1 -0
  283. package/dist/core/types/mail.js +7 -0
  284. package/dist/core/types/mail.js.map +1 -0
  285. package/dist/core/types/oauth2.d.ts +9 -0
  286. package/dist/core/types/oauth2.d.ts.map +1 -0
  287. package/dist/core/types/oauth2.js +7 -0
  288. package/dist/core/types/oauth2.js.map +1 -0
  289. package/dist/core/types/storage-adapters.d.ts +611 -0
  290. package/dist/core/types/storage-adapters.d.ts.map +1 -0
  291. package/dist/core/types/storage-adapters.js +14 -0
  292. package/dist/core/types/storage-adapters.js.map +1 -0
  293. package/dist/core/types/user.d.ts +10 -0
  294. package/dist/core/types/user.d.ts.map +1 -0
  295. package/dist/core/types/user.js +45 -0
  296. package/dist/core/types/user.js.map +1 -0
  297. package/dist/core/utils/id-generator.d.ts +16 -0
  298. package/dist/core/utils/id-generator.d.ts.map +1 -0
  299. package/dist/core/utils/id-generator.js +61 -0
  300. package/dist/core/utils/id-generator.js.map +1 -0
  301. package/dist/core/utils/logger.d.ts +100 -0
  302. package/dist/core/utils/logger.d.ts.map +1 -0
  303. package/dist/core/utils/logger.js +212 -0
  304. package/dist/core/utils/logger.js.map +1 -0
  305. package/dist/core/utils/reference-expander.d.ts +57 -0
  306. package/dist/core/utils/reference-expander.d.ts.map +1 -0
  307. package/dist/core/utils/reference-expander.js +192 -0
  308. package/dist/core/utils/reference-expander.js.map +1 -0
  309. package/dist/core/utils/universal-crypto.d.ts +46 -0
  310. package/dist/core/utils/universal-crypto.d.ts.map +1 -0
  311. package/dist/core/utils/universal-crypto.js +171 -0
  312. package/dist/core/utils/universal-crypto.js.map +1 -0
  313. package/dist/core/validation/validator.d.ts +11 -0
  314. package/dist/core/validation/validator.d.ts.map +1 -0
  315. package/dist/core/validation/validator.js +164 -0
  316. package/dist/core/validation/validator.js.map +1 -0
  317. package/dist/i18n/config.d.ts +4077 -0
  318. package/dist/i18n/config.d.ts.map +1 -0
  319. package/dist/i18n/config.js +116 -0
  320. package/dist/i18n/config.js.map +1 -0
  321. package/dist/i18n/hooks.d.ts +86 -0
  322. package/dist/i18n/hooks.d.ts.map +1 -0
  323. package/dist/i18n/hooks.js +122 -0
  324. package/dist/i18n/hooks.js.map +1 -0
  325. package/dist/i18n/index.d.ts +39 -0
  326. package/dist/i18n/index.d.ts.map +1 -0
  327. package/dist/i18n/index.js +40 -0
  328. package/dist/i18n/index.js.map +1 -0
  329. package/dist/i18n/locales/en/auth.json +108 -0
  330. package/dist/i18n/locales/en/common.json +100 -0
  331. package/dist/i18n/locales/en/errors.json +55 -0
  332. package/dist/i18n/locales/en/fields.json +456 -0
  333. package/dist/i18n/locales/en/studio.json +1329 -0
  334. package/dist/i18n/locales/fr/auth.json +108 -0
  335. package/dist/i18n/locales/fr/common.json +100 -0
  336. package/dist/i18n/locales/fr/errors.json +55 -0
  337. package/dist/i18n/locales/fr/fields.json +456 -0
  338. package/dist/i18n/locales/fr/studio.json +1274 -0
  339. package/dist/i18n/provider.d.ts +45 -0
  340. package/dist/i18n/provider.d.ts.map +1 -0
  341. package/dist/i18n/provider.js +49 -0
  342. package/dist/i18n/provider.js.map +1 -0
  343. package/dist/i18n/types.d.ts +8 -0
  344. package/dist/i18n/types.d.ts.map +1 -0
  345. package/dist/i18n/types.js +7 -0
  346. package/dist/i18n/types.js.map +1 -0
  347. package/dist/index.d.ts +15 -0
  348. package/dist/index.d.ts.map +1 -0
  349. package/dist/index.js +16 -0
  350. package/dist/index.js.map +1 -0
  351. package/dist/integrations/express/adapter.d.ts +33 -0
  352. package/dist/integrations/express/adapter.d.ts.map +1 -0
  353. package/dist/integrations/express/adapter.js +203 -0
  354. package/dist/integrations/express/adapter.js.map +1 -0
  355. package/dist/integrations/express/config.d.ts +599 -0
  356. package/dist/integrations/express/config.d.ts.map +1 -0
  357. package/dist/integrations/express/config.js +166 -0
  358. package/dist/integrations/express/config.js.map +1 -0
  359. package/dist/integrations/express/index.d.ts +10 -0
  360. package/dist/integrations/express/index.d.ts.map +1 -0
  361. package/dist/integrations/express/index.js +9 -0
  362. package/dist/integrations/express/index.js.map +1 -0
  363. package/dist/integrations/express/integration.d.ts +111 -0
  364. package/dist/integrations/express/integration.d.ts.map +1 -0
  365. package/dist/integrations/express/integration.js +730 -0
  366. package/dist/integrations/express/integration.js.map +1 -0
  367. package/dist/integrations/express/media-rewrite-middleware.d.ts +78 -0
  368. package/dist/integrations/express/media-rewrite-middleware.d.ts.map +1 -0
  369. package/dist/integrations/express/media-rewrite-middleware.js +211 -0
  370. package/dist/integrations/express/media-rewrite-middleware.js.map +1 -0
  371. package/dist/integrations/express/media-url-generator.d.ts +52 -0
  372. package/dist/integrations/express/media-url-generator.d.ts.map +1 -0
  373. package/dist/integrations/express/media-url-generator.js +107 -0
  374. package/dist/integrations/express/media-url-generator.js.map +1 -0
  375. package/dist/integrations/express/middleware.d.ts +25 -0
  376. package/dist/integrations/express/middleware.d.ts.map +1 -0
  377. package/dist/integrations/express/middleware.js +130 -0
  378. package/dist/integrations/express/middleware.js.map +1 -0
  379. package/dist/integrations/express/server.d.ts +99 -0
  380. package/dist/integrations/express/server.d.ts.map +1 -0
  381. package/dist/integrations/express/server.js +597 -0
  382. package/dist/integrations/express/server.js.map +1 -0
  383. package/dist/integrations/express/types.d.ts +106 -0
  384. package/dist/integrations/express/types.d.ts.map +1 -0
  385. package/dist/integrations/express/types.js +2 -0
  386. package/dist/integrations/express/types.js.map +1 -0
  387. package/dist/mail/adapters/console.d.ts +30 -0
  388. package/dist/mail/adapters/console.d.ts.map +1 -0
  389. package/dist/mail/adapters/console.js +169 -0
  390. package/dist/mail/adapters/console.js.map +1 -0
  391. package/dist/mail/adapters/resend.d.ts +18 -0
  392. package/dist/mail/adapters/resend.d.ts.map +1 -0
  393. package/dist/mail/adapters/resend.js +148 -0
  394. package/dist/mail/adapters/resend.js.map +1 -0
  395. package/dist/mail/adapters/smtp.d.ts +22 -0
  396. package/dist/mail/adapters/smtp.d.ts.map +1 -0
  397. package/dist/mail/adapters/smtp.js +180 -0
  398. package/dist/mail/adapters/smtp.js.map +1 -0
  399. package/dist/mail/index.d.ts +15 -0
  400. package/dist/mail/index.d.ts.map +1 -0
  401. package/dist/mail/index.js +13 -0
  402. package/dist/mail/index.js.map +1 -0
  403. package/dist/mail/mail-service.d.ts +180 -0
  404. package/dist/mail/mail-service.d.ts.map +1 -0
  405. package/dist/mail/mail-service.js +370 -0
  406. package/dist/mail/mail-service.js.map +1 -0
  407. package/dist/mail/notification-service.d.ts +82 -0
  408. package/dist/mail/notification-service.d.ts.map +1 -0
  409. package/dist/mail/notification-service.js +262 -0
  410. package/dist/mail/notification-service.js.map +1 -0
  411. package/dist/mail/templates/built-in-templates.d.ts +47 -0
  412. package/dist/mail/templates/built-in-templates.d.ts.map +1 -0
  413. package/dist/mail/templates/built-in-templates.js +416 -0
  414. package/dist/mail/templates/built-in-templates.js.map +1 -0
  415. package/dist/mail/templates/custom-template-renderer.d.ts +115 -0
  416. package/dist/mail/templates/custom-template-renderer.d.ts.map +1 -0
  417. package/dist/mail/templates/custom-template-renderer.js +225 -0
  418. package/dist/mail/templates/custom-template-renderer.js.map +1 -0
  419. package/dist/mail/types.d.ts +7 -0
  420. package/dist/mail/types.d.ts.map +1 -0
  421. package/dist/mail/types.js +7 -0
  422. package/dist/mail/types.js.map +1 -0
  423. package/dist/routes/auth/captcha.d.ts +33 -0
  424. package/dist/routes/auth/captcha.d.ts.map +1 -0
  425. package/dist/routes/auth/captcha.js +102 -0
  426. package/dist/routes/auth/captcha.js.map +1 -0
  427. package/dist/routes/auth/change-password.d.ts +17 -0
  428. package/dist/routes/auth/change-password.d.ts.map +1 -0
  429. package/dist/routes/auth/change-password.js +121 -0
  430. package/dist/routes/auth/change-password.js.map +1 -0
  431. package/dist/routes/auth/index.d.ts +13 -0
  432. package/dist/routes/auth/index.d.ts.map +1 -0
  433. package/dist/routes/auth/index.js +35 -0
  434. package/dist/routes/auth/index.js.map +1 -0
  435. package/dist/routes/auth/mfa.d.ts +121 -0
  436. package/dist/routes/auth/mfa.d.ts.map +1 -0
  437. package/dist/routes/auth/mfa.js +1143 -0
  438. package/dist/routes/auth/mfa.js.map +1 -0
  439. package/dist/routes/auth/oauth.d.ts +37 -0
  440. package/dist/routes/auth/oauth.d.ts.map +1 -0
  441. package/dist/routes/auth/oauth.js +420 -0
  442. package/dist/routes/auth/oauth.js.map +1 -0
  443. package/dist/routes/auth/oauth2-server.d.ts +70 -0
  444. package/dist/routes/auth/oauth2-server.d.ts.map +1 -0
  445. package/dist/routes/auth/oauth2-server.js +775 -0
  446. package/dist/routes/auth/oauth2-server.js.map +1 -0
  447. package/dist/routes/auth/passkey.d.ts +65 -0
  448. package/dist/routes/auth/passkey.d.ts.map +1 -0
  449. package/dist/routes/auth/passkey.js +910 -0
  450. package/dist/routes/auth/passkey.js.map +1 -0
  451. package/dist/routes/auth/password-reset.d.ts +23 -0
  452. package/dist/routes/auth/password-reset.d.ts.map +1 -0
  453. package/dist/routes/auth/password-reset.js +336 -0
  454. package/dist/routes/auth/password-reset.js.map +1 -0
  455. package/dist/routes/handlers/audit.d.ts +21 -0
  456. package/dist/routes/handlers/audit.d.ts.map +1 -0
  457. package/dist/routes/handlers/audit.js +124 -0
  458. package/dist/routes/handlers/audit.js.map +1 -0
  459. package/dist/routes/handlers/auth.d.ts +150 -0
  460. package/dist/routes/handlers/auth.d.ts.map +1 -0
  461. package/dist/routes/handlers/auth.js +512 -0
  462. package/dist/routes/handlers/auth.js.map +1 -0
  463. package/dist/routes/handlers/base.d.ts +34 -0
  464. package/dist/routes/handlers/base.d.ts.map +1 -0
  465. package/dist/routes/handlers/base.js +366 -0
  466. package/dist/routes/handlers/base.js.map +1 -0
  467. package/dist/routes/handlers/config.d.ts +31 -0
  468. package/dist/routes/handlers/config.d.ts.map +1 -0
  469. package/dist/routes/handlers/config.js +483 -0
  470. package/dist/routes/handlers/config.js.map +1 -0
  471. package/dist/routes/handlers/documents.d.ts +50 -0
  472. package/dist/routes/handlers/documents.d.ts.map +1 -0
  473. package/dist/routes/handlers/documents.js +708 -0
  474. package/dist/routes/handlers/documents.js.map +1 -0
  475. package/dist/routes/handlers/index.d.ts +12 -0
  476. package/dist/routes/handlers/index.d.ts.map +1 -0
  477. package/dist/routes/handlers/index.js +11 -0
  478. package/dist/routes/handlers/index.js.map +1 -0
  479. package/dist/routes/handlers/media.d.ts +24 -0
  480. package/dist/routes/handlers/media.d.ts.map +1 -0
  481. package/dist/routes/handlers/media.js +432 -0
  482. package/dist/routes/handlers/media.js.map +1 -0
  483. package/dist/routes/handlers/search.d.ts +10 -0
  484. package/dist/routes/handlers/search.d.ts.map +1 -0
  485. package/dist/routes/handlers/search.js +153 -0
  486. package/dist/routes/handlers/search.js.map +1 -0
  487. package/dist/routes/handlers/tokens.d.ts +14 -0
  488. package/dist/routes/handlers/tokens.d.ts.map +1 -0
  489. package/dist/routes/handlers/tokens.js +109 -0
  490. package/dist/routes/handlers/tokens.js.map +1 -0
  491. package/dist/routes/handlers/types.d.ts +13 -0
  492. package/dist/routes/handlers/types.d.ts.map +1 -0
  493. package/dist/routes/handlers/types.js +5 -0
  494. package/dist/routes/handlers/types.js.map +1 -0
  495. package/dist/routes/handlers/users.d.ts +16 -0
  496. package/dist/routes/handlers/users.d.ts.map +1 -0
  497. package/dist/routes/handlers/users.js +198 -0
  498. package/dist/routes/handlers/users.js.map +1 -0
  499. package/dist/routes/handlers/webhooks.d.ts +37 -0
  500. package/dist/routes/handlers/webhooks.d.ts.map +1 -0
  501. package/dist/routes/handlers/webhooks.js +343 -0
  502. package/dist/routes/handlers/webhooks.js.map +1 -0
  503. package/dist/routes/index.d.ts +5 -0
  504. package/dist/routes/index.d.ts.map +1 -0
  505. package/dist/routes/index.js +5 -0
  506. package/dist/routes/index.js.map +1 -0
  507. package/dist/routes/routes.d.ts +29 -0
  508. package/dist/routes/routes.d.ts.map +1 -0
  509. package/dist/routes/routes.js +531 -0
  510. package/dist/routes/routes.js.map +1 -0
  511. package/dist/routes/slug-processor.d.ts +32 -0
  512. package/dist/routes/slug-processor.d.ts.map +1 -0
  513. package/dist/routes/slug-processor.js +328 -0
  514. package/dist/routes/slug-processor.js.map +1 -0
  515. package/dist/routes/types.d.ts +31 -0
  516. package/dist/routes/types.d.ts.map +1 -0
  517. package/dist/routes/types.js +7 -0
  518. package/dist/routes/types.js.map +1 -0
  519. package/dist/routes/users/index.d.ts +7 -0
  520. package/dist/routes/users/index.d.ts.map +1 -0
  521. package/dist/routes/users/index.js +7 -0
  522. package/dist/routes/users/index.js.map +1 -0
  523. package/dist/structure/builder/StructureBuilder.d.ts +209 -0
  524. package/dist/structure/builder/StructureBuilder.d.ts.map +1 -0
  525. package/dist/structure/builder/StructureBuilder.js +425 -0
  526. package/dist/structure/builder/StructureBuilder.js.map +1 -0
  527. package/dist/structure/builder/index.d.ts +8 -0
  528. package/dist/structure/builder/index.d.ts.map +1 -0
  529. package/dist/structure/builder/index.js +6 -0
  530. package/dist/structure/builder/index.js.map +1 -0
  531. package/dist/structure/builder/navigation-tree-builder.d.ts +33 -0
  532. package/dist/structure/builder/navigation-tree-builder.d.ts.map +1 -0
  533. package/dist/structure/builder/navigation-tree-builder.js +284 -0
  534. package/dist/structure/builder/navigation-tree-builder.js.map +1 -0
  535. package/dist/structure/errors/index.d.ts +72 -0
  536. package/dist/structure/errors/index.d.ts.map +1 -0
  537. package/dist/structure/errors/index.js +136 -0
  538. package/dist/structure/errors/index.js.map +1 -0
  539. package/dist/structure/index.d.ts +14 -0
  540. package/dist/structure/index.d.ts.map +1 -0
  541. package/dist/structure/index.js +16 -0
  542. package/dist/structure/index.js.map +1 -0
  543. package/dist/structure/types/filters.d.ts +167 -0
  544. package/dist/structure/types/filters.d.ts.map +1 -0
  545. package/dist/structure/types/filters.js +38 -0
  546. package/dist/structure/types/filters.js.map +1 -0
  547. package/dist/structure/types/index.d.ts +11 -0
  548. package/dist/structure/types/index.d.ts.map +1 -0
  549. package/dist/structure/types/index.js +7 -0
  550. package/dist/structure/types/index.js.map +1 -0
  551. package/dist/structure/types/permissions.d.ts +159 -0
  552. package/dist/structure/types/permissions.d.ts.map +1 -0
  553. package/dist/structure/types/permissions.js +49 -0
  554. package/dist/structure/types/permissions.js.map +1 -0
  555. package/dist/structure/types/structure.d.ts +342 -0
  556. package/dist/structure/types/structure.d.ts.map +1 -0
  557. package/dist/structure/types/structure.js +6 -0
  558. package/dist/structure/types/structure.js.map +1 -0
  559. package/dist/structure/types/views.d.ts +278 -0
  560. package/dist/structure/types/views.d.ts.map +1 -0
  561. package/dist/structure/types/views.js +6 -0
  562. package/dist/structure/types/views.js.map +1 -0
  563. package/dist/structure/utils/hash.d.ts +61 -0
  564. package/dist/structure/utils/hash.d.ts.map +1 -0
  565. package/dist/structure/utils/hash.js +118 -0
  566. package/dist/structure/utils/hash.js.map +1 -0
  567. package/dist/structure/utils/index.d.ts +10 -0
  568. package/dist/structure/utils/index.d.ts.map +1 -0
  569. package/dist/structure/utils/index.js +7 -0
  570. package/dist/structure/utils/index.js.map +1 -0
  571. package/dist/structure/utils/permission-checker.d.ts +57 -0
  572. package/dist/structure/utils/permission-checker.d.ts.map +1 -0
  573. package/dist/structure/utils/permission-checker.js +286 -0
  574. package/dist/structure/utils/permission-checker.js.map +1 -0
  575. package/dist/structure/utils/query-builder.d.ts +82 -0
  576. package/dist/structure/utils/query-builder.d.ts.map +1 -0
  577. package/dist/structure/utils/query-builder.js +489 -0
  578. package/dist/structure/utils/query-builder.js.map +1 -0
  579. package/dist/structure/utils/structure-merger.d.ts +91 -0
  580. package/dist/structure/utils/structure-merger.d.ts.map +1 -0
  581. package/dist/structure/utils/structure-merger.js +445 -0
  582. package/dist/structure/utils/structure-merger.js.map +1 -0
  583. package/dist/structure/validation/schemas.d.ts +3107 -0
  584. package/dist/structure/validation/schemas.d.ts.map +1 -0
  585. package/dist/structure/validation/schemas.js +436 -0
  586. package/dist/structure/validation/schemas.js.map +1 -0
  587. package/dist/types/api-requests.d.ts +334 -0
  588. package/dist/types/api-requests.d.ts.map +1 -0
  589. package/dist/types/api-requests.js +8 -0
  590. package/dist/types/api-requests.js.map +1 -0
  591. package/dist/types/auth.d.ts +394 -0
  592. package/dist/types/auth.d.ts.map +1 -0
  593. package/dist/types/auth.js +8 -0
  594. package/dist/types/auth.js.map +1 -0
  595. package/dist/types/document.d.ts +49 -0
  596. package/dist/types/document.d.ts.map +1 -0
  597. package/dist/types/document.js +6 -0
  598. package/dist/types/document.js.map +1 -0
  599. package/dist/types/field-system.d.ts +197 -0
  600. package/dist/types/field-system.d.ts.map +1 -0
  601. package/dist/types/field-system.js +8 -0
  602. package/dist/types/field-system.js.map +1 -0
  603. package/dist/types/fields.d.ts +17 -0
  604. package/dist/types/fields.d.ts.map +1 -0
  605. package/dist/types/fields.js +21 -0
  606. package/dist/types/fields.js.map +1 -0
  607. package/dist/types/http.d.ts +162 -0
  608. package/dist/types/http.d.ts.map +1 -0
  609. package/dist/types/http.js +8 -0
  610. package/dist/types/http.js.map +1 -0
  611. package/dist/types/i18n.d.ts +56 -0
  612. package/dist/types/i18n.d.ts.map +1 -0
  613. package/dist/types/i18n.js +25 -0
  614. package/dist/types/i18n.js.map +1 -0
  615. package/dist/types/index.d.ts +22 -0
  616. package/dist/types/index.d.ts.map +1 -0
  617. package/dist/types/index.js +10 -0
  618. package/dist/types/index.js.map +1 -0
  619. package/dist/types/mail.d.ts +224 -0
  620. package/dist/types/mail.d.ts.map +1 -0
  621. package/dist/types/mail.js +8 -0
  622. package/dist/types/mail.js.map +1 -0
  623. package/dist/types/media.d.ts +56 -0
  624. package/dist/types/media.d.ts.map +1 -0
  625. package/dist/types/media.js +6 -0
  626. package/dist/types/media.js.map +1 -0
  627. package/dist/types/oauth2.d.ts +287 -0
  628. package/dist/types/oauth2.d.ts.map +1 -0
  629. package/dist/types/oauth2.js +43 -0
  630. package/dist/types/oauth2.js.map +1 -0
  631. package/dist/types/passkey.d.ts +217 -0
  632. package/dist/types/passkey.d.ts.map +1 -0
  633. package/dist/types/passkey.js +8 -0
  634. package/dist/types/passkey.js.map +1 -0
  635. package/dist/types/slug.d.ts +99 -0
  636. package/dist/types/slug.d.ts.map +1 -0
  637. package/dist/types/slug.js +8 -0
  638. package/dist/types/slug.js.map +1 -0
  639. package/package.json +183 -0
@@ -0,0 +1,1432 @@
1
+ import { Pool } from 'pg';
2
+ import { SecurityValidator, InvalidInputError, createLogger, AUDIT_OPERATIONS } from '../../core/index.js';
3
+ /**
4
+ * Serialise a value for a JSONB column. `undefined` and `null` both become SQL NULL, which
5
+ * reads back as `undefined` — an unset field is never stored as `'{}'` or `'[]'`.
6
+ */
7
+ function jsonColumnValue(value) {
8
+ return value === undefined || value === null ? null : JSON.stringify(value);
9
+ }
10
+ export class PostgresDataAdapter {
11
+ config;
12
+ pool;
13
+ logger = createLogger('adapter', 'PostgresDataAdapter');
14
+ initialized = false;
15
+ initializationPromise = null;
16
+ closePromise = null;
17
+ // Security limits
18
+ MAX_DOCUMENT_SIZE = 10 * 1024 * 1024; // 10MB
19
+ MAX_LIST_LIMIT = 1000;
20
+ constructor(config = {}) {
21
+ // Security: Validate schema and table prefix to prevent SQL injection
22
+ const schemaPattern = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
23
+ const prefixPattern = /^[a-zA-Z0-9_]*$/;
24
+ const schema = config.schema || 'public';
25
+ const tablePrefix = config.tablePrefix || 'trokky_';
26
+ if (!schemaPattern.test(schema)) {
27
+ throw new Error(`Invalid PostgreSQL schema name: "${schema}". Schema names must start with a letter or underscore and contain only alphanumeric characters and underscores.`);
28
+ }
29
+ if (!prefixPattern.test(tablePrefix)) {
30
+ throw new Error(`Invalid table prefix: "${tablePrefix}". Table prefixes must contain only alphanumeric characters and underscores.`);
31
+ }
32
+ this.config = {
33
+ connection: config.connection || process.env.DATABASE_URL || 'postgresql://localhost:5432/trokky',
34
+ pool: {
35
+ max: 20,
36
+ idleTimeoutMillis: 30000,
37
+ connectionTimeoutMillis: 2000,
38
+ ...config.pool
39
+ },
40
+ schema,
41
+ tablePrefix,
42
+ autoMigrate: config.autoMigrate ?? true,
43
+ enableQueryLogging: config.enableQueryLogging ?? false,
44
+ ssl: config.ssl ?? false,
45
+ connectionTimeout: config.connectionTimeout || 5000
46
+ };
47
+ // Initialize connection pool
48
+ const poolConfig = typeof this.config.connection === 'string'
49
+ ? {
50
+ connectionString: this.config.connection,
51
+ ...this.config.pool,
52
+ ...(this.config.ssl !== undefined && { ssl: this.config.ssl })
53
+ }
54
+ : {
55
+ ...this.config.connection,
56
+ ...this.config.pool,
57
+ ...(this.config.ssl !== undefined && { ssl: this.config.ssl })
58
+ };
59
+ this.pool = new Pool(poolConfig);
60
+ // Setup error handling
61
+ this.pool.on('error', (err) => {
62
+ this.logger.error('PostgreSQL pool error', err);
63
+ });
64
+ this.pool.on('connect', (client) => {
65
+ this.logger.debug('New PostgreSQL client connected');
66
+ });
67
+ this.logger.info('PostgresDataAdapter constructed', {
68
+ schema: this.config.schema,
69
+ tablePrefix: this.config.tablePrefix,
70
+ autoMigrate: this.config.autoMigrate,
71
+ enableQueryLogging: this.config.enableQueryLogging
72
+ });
73
+ // Initialize immediately like filesystem adapter - but async
74
+ // Store initialization promise for later awaiting
75
+ if (this.config.autoMigrate) {
76
+ this.logger.info('Starting PostgreSQL auto-migration during construction');
77
+ this.initializationPromise = this.initialize();
78
+ // Start initialization but don't block construction
79
+ this.initializationPromise
80
+ .then(() => {
81
+ this.logger.info('PostgreSQL adapter initialization completed successfully during construction');
82
+ })
83
+ .catch(error => {
84
+ this.logger.error('Failed to initialize PostgreSQL adapter during construction', error);
85
+ });
86
+ }
87
+ else {
88
+ this.logger.info('PostgreSQL auto-migration disabled in config');
89
+ }
90
+ }
91
+ async initialize() {
92
+ if (this.initialized) {
93
+ this.logger.debug('PostgreSQL adapter already initialized, skipping');
94
+ return;
95
+ }
96
+ this.logger.info('Starting PostgreSQL adapter initialization');
97
+ try {
98
+ // Test connection
99
+ this.logger.info('Testing PostgreSQL connection...');
100
+ const isHealthy = await this.healthCheck();
101
+ this.logger.info('PostgreSQL connection test result', { healthy: isHealthy });
102
+ // Run migrations if enabled
103
+ if (this.config.autoMigrate) {
104
+ this.logger.info('Running PostgreSQL migrations...');
105
+ await this.runMigrations();
106
+ this.logger.info('PostgreSQL migrations completed');
107
+ }
108
+ else {
109
+ this.logger.info('PostgreSQL auto-migration disabled, skipping migrations');
110
+ }
111
+ this.initialized = true;
112
+ this.logger.info('✅ PostgreSQL adapter initialized successfully', {
113
+ schema: this.config.schema,
114
+ tablePrefix: this.config.tablePrefix,
115
+ autoMigrate: this.config.autoMigrate
116
+ });
117
+ }
118
+ catch (error) {
119
+ this.logger.error('❌ Failed to initialize PostgreSQL adapter', error);
120
+ throw error;
121
+ }
122
+ }
123
+ /**
124
+ * Close the connection pool. Until this runs the pool's idle clients keep the event loop
125
+ * alive, so a process that stops its HTTP server never exits and a test file leaks its
126
+ * sockets into the next one.
127
+ *
128
+ * `pool.end()` throws if it is called a second time, and shutdown paths do get entered
129
+ * twice (a SIGTERM arriving while a manual stop is already in flight), so the first call's
130
+ * promise is remembered and replayed rather than re-ending the pool.
131
+ */
132
+ async close() {
133
+ if (!this.closePromise) {
134
+ this.closePromise = this.pool.end();
135
+ this.closePromise.then(() => this.logger.info('PostgreSQL connection pool closed'), error => this.logger.error('Error closing PostgreSQL connection pool', error));
136
+ }
137
+ return this.closePromise;
138
+ }
139
+ async ensureInitialized() {
140
+ if (!this.initialized) {
141
+ // If we have an initialization promise from constructor, await it
142
+ if (this.initializationPromise) {
143
+ await this.initializationPromise;
144
+ }
145
+ else {
146
+ // Fallback: initialize now if not done during construction
147
+ await this.initialize();
148
+ }
149
+ }
150
+ }
151
+ tableName(entity) {
152
+ return `${this.config.schema}.${this.config.tablePrefix}${entity}`;
153
+ }
154
+ async query(text, params) {
155
+ await this.ensureInitialized();
156
+ if (this.config.enableQueryLogging) {
157
+ this.logger.debug('Executing query', { text, params });
158
+ }
159
+ try {
160
+ const result = await this.pool.query(text, params);
161
+ return result;
162
+ }
163
+ catch (error) {
164
+ this.logger.error('Query failed', { text, params, error });
165
+ throw error;
166
+ }
167
+ }
168
+ async directQuery(text, params) {
169
+ // Direct query without ensureInitialized() - used during initialization
170
+ if (this.config.enableQueryLogging) {
171
+ this.logger.debug('Executing direct query', { text, params });
172
+ }
173
+ try {
174
+ const result = await this.pool.query(text, params);
175
+ return result;
176
+ }
177
+ catch (error) {
178
+ this.logger.error('Direct query failed', { text, params, error });
179
+ throw error;
180
+ }
181
+ }
182
+ // ==========================================================================
183
+ // DOCUMENT OPERATIONS
184
+ // ==========================================================================
185
+ /**
186
+ * Shared shape for every document read path, so get/save/list cannot drift apart.
187
+ *
188
+ * `_status` lives inside the JSONB payload, so it is pulled out of the data and re-emitted as
189
+ * a metadata field with the same `'draft'` fallback the filesystem adapter applies; the rest of
190
+ * the user data is spread last, exactly as the filesystem adapter does. SQL NULLs become
191
+ * `undefined` — the contract never returns `null` for an unknown actor.
192
+ */
193
+ mapRowToDocument(row) {
194
+ const { _status, ...data } = (row.data ?? {});
195
+ return {
196
+ id: row.id,
197
+ _id: row.id,
198
+ _collection: row.collection,
199
+ _createdAt: row.created_at,
200
+ _updatedAt: row.updated_at,
201
+ _revision: row.revision ?? 1,
202
+ _status: _status ?? 'draft',
203
+ _createdBy: row.created_by ?? undefined,
204
+ _updatedBy: row.updated_by ?? undefined,
205
+ _createdByType: row.created_by_type ?? undefined,
206
+ _updatedByType: row.updated_by_type ?? undefined,
207
+ ...data
208
+ };
209
+ }
210
+ async getDocument(collection, id) {
211
+ SecurityValidator.validateCollectionName(collection);
212
+ SecurityValidator.validateDocumentId(id);
213
+ const result = await this.query(`SELECT * FROM ${this.tableName('documents')} WHERE collection = $1 AND id = $2`, [collection, id]);
214
+ if (result.rows.length === 0) {
215
+ return null;
216
+ }
217
+ return this.mapRowToDocument(result.rows[0]);
218
+ }
219
+ async saveDocument(collection, id, data, auditContext) {
220
+ SecurityValidator.validateCollectionName(collection);
221
+ SecurityValidator.validateDocumentId(id);
222
+ SecurityValidator.validateDocumentData(data);
223
+ const dataJson = JSON.stringify(data);
224
+ if (dataJson.length > this.MAX_DOCUMENT_SIZE) {
225
+ throw new InvalidInputError(`Document exceeds maximum size of ${this.MAX_DOCUMENT_SIZE} bytes`);
226
+ }
227
+ const now = new Date();
228
+ const userId = auditContext?.userId;
229
+ const userType = auditContext?.userType;
230
+ try {
231
+ // UPSERT (INSERT ... ON CONFLICT ... DO UPDATE). `_status` is defaulted in SQL so the
232
+ // create/update distinction is decided by the same statement that performs the write:
233
+ // an explicit incoming status wins, otherwise the stored one is preserved on update and
234
+ // 'draft' is used on create. `created_by`/`created_by_type` are left out of the SET list,
235
+ // so the original creator survives an update (filesystem behaviour).
236
+ const result = await this.query(`
237
+ INSERT INTO ${this.tableName('documents')}
238
+ (collection, id, data, created_at, updated_at, created_by, updated_by, created_by_type, updated_by_type, revision)
239
+ VALUES (
240
+ $1, $2,
241
+ $3::jsonb || jsonb_build_object('_status', COALESCE($3::jsonb->>'_status', 'draft')),
242
+ $4, $4, $5, $5, $6, $6, 1
243
+ )
244
+ ON CONFLICT (collection, id)
245
+ DO UPDATE SET
246
+ data = $3::jsonb || jsonb_build_object('_status', COALESCE($3::jsonb->>'_status', ${this.config.tablePrefix}documents.data->>'_status', 'draft')),
247
+ updated_at = $4,
248
+ updated_by = $5,
249
+ updated_by_type = $6,
250
+ revision = COALESCE(${this.config.tablePrefix}documents.revision, 1) + 1
251
+ RETURNING *
252
+ `, [collection, id, JSON.stringify(data), now, userId, userType]);
253
+ const row = result.rows[0];
254
+ // Create audit log if context provided
255
+ if (auditContext) {
256
+ await this.createAuditLog({
257
+ documentId: id,
258
+ collection,
259
+ operation: AUDIT_OPERATIONS.UPDATE,
260
+ actorType: auditContext.userType,
261
+ actorId: auditContext.userId,
262
+ actorUsername: auditContext.username,
263
+ changes: { after: data },
264
+ timestamp: now,
265
+ revision: row.revision ?? 1,
266
+ ipAddress: auditContext.ipAddress,
267
+ userAgent: auditContext.userAgent
268
+ });
269
+ }
270
+ return this.mapRowToDocument(row);
271
+ }
272
+ catch (error) {
273
+ this.logger.error('Failed to save document', { collection, id, error });
274
+ throw error;
275
+ }
276
+ }
277
+ /**
278
+ * Document fields that live in their own column rather than inside the JSONB payload.
279
+ * Anything not listed here — including `_status` and any other leading-underscore field the
280
+ * schema happens to define — is addressed through `data`.
281
+ */
282
+ static DOCUMENT_SYSTEM_COLUMNS = {
283
+ id: 'id',
284
+ _id: 'id',
285
+ _revision: 'revision',
286
+ _createdAt: 'created_at',
287
+ _updatedAt: 'updated_at',
288
+ _createdBy: 'created_by',
289
+ _updatedBy: 'updated_by',
290
+ _createdByType: 'created_by_type',
291
+ _updatedByType: 'updated_by_type'
292
+ };
293
+ /** SECURITY: field names reach the SQL text directly, so they are whitelisted first. */
294
+ static DOCUMENT_FIELD_PATTERN = /^[a-zA-Z0-9_.\-]+$/;
295
+ /** Updatable user columns, in the order they are written. `json` columns are JSONB. */
296
+ static USER_UPDATE_COLUMNS = [
297
+ { key: 'username', column: 'username' },
298
+ { key: 'email', column: 'email' },
299
+ { key: 'passwordHash', column: 'password_hash' },
300
+ { key: 'firstName', column: 'first_name' },
301
+ { key: 'lastName', column: 'last_name' },
302
+ { key: 'role', column: 'role' },
303
+ { key: 'permissions', column: 'permissions', json: true },
304
+ { key: 'isActive', column: 'is_active' },
305
+ { key: 'profileImage', column: 'profile_image' },
306
+ { key: 'preferences', column: 'preferences', json: true },
307
+ { key: 'oauthProviders', column: 'oauth_providers', json: true },
308
+ { key: 'mfa', column: 'mfa', json: true },
309
+ { key: 'passkeys', column: 'passkeys', json: true },
310
+ { key: 'lastLoginAt', column: 'last_login_at' }
311
+ ];
312
+ /**
313
+ * Filter clauses shared by listDocuments and countDocuments, so the two cannot disagree
314
+ * about what a filter means. Data fields are compared as `jsonb`, not as text: `String(value)`
315
+ * turned `false`, `0` and `null` into strings that never matched anything.
316
+ */
317
+ buildDocumentFilterClauses(filter, params) {
318
+ return Object.entries(filter).map(([key, value]) => {
319
+ // SECURITY: Validate field name to prevent SQL injection
320
+ // Allow only alphanumeric, underscore, dash, and dot (for nested fields)
321
+ if (!PostgresDataAdapter.DOCUMENT_FIELD_PATTERN.test(key)) {
322
+ throw new Error(`Invalid filter field name: ${key}`);
323
+ }
324
+ const column = PostgresDataAdapter.DOCUMENT_SYSTEM_COLUMNS[key];
325
+ if (column) {
326
+ if (value === null || value === undefined) {
327
+ return `${column} IS NULL`;
328
+ }
329
+ params.push(value);
330
+ return `${column} = $${params.length}`;
331
+ }
332
+ // Data field (including _status, which is stored inside the payload)
333
+ params.push(key);
334
+ const keyParam = `$${params.length}::text`;
335
+ params.push(JSON.stringify(value ?? null));
336
+ return `data->${keyParam} = $${params.length}::jsonb`;
337
+ });
338
+ }
339
+ /**
340
+ * Accepts both sort grammars the API surfaces: `-field` (leading minus = descending) and
341
+ * `field.asc` / `field.desc`. The direction is stripped before the field name is validated,
342
+ * otherwise `-title` reads as a field literally named `-title`.
343
+ */
344
+ buildDocumentSortClause(rawSortField) {
345
+ let field = rawSortField.trim();
346
+ let direction = 'ASC';
347
+ if (field.startsWith('-')) {
348
+ direction = 'DESC';
349
+ field = field.slice(1);
350
+ }
351
+ else {
352
+ const dotted = /^(.+)\.(asc|desc)$/i.exec(field);
353
+ if (dotted) {
354
+ field = dotted[1];
355
+ direction = dotted[2].toLowerCase() === 'desc' ? 'DESC' : 'ASC';
356
+ }
357
+ }
358
+ // SECURITY: Validate field name to prevent SQL injection
359
+ if (!PostgresDataAdapter.DOCUMENT_FIELD_PATTERN.test(field)) {
360
+ throw new Error(`Invalid sort field name: ${field}`);
361
+ }
362
+ // Data fields are ordered by the stored jsonb value (`->`), not by its text rendering
363
+ // (`->>`): as text, 13 sorts before 2. jsonb ordering compares numbers numerically and
364
+ // strings by collation, which is what the filesystem adapter's `<` / `>` on the parsed
365
+ // JS values does for a homogeneously-typed field.
366
+ const column = PostgresDataAdapter.DOCUMENT_SYSTEM_COLUMNS[field] ?? `data->'${field}'`;
367
+ return `${column} ${direction}`;
368
+ }
369
+ async listDocuments(collection, options = {}) {
370
+ SecurityValidator.validateCollectionName(collection);
371
+ // No implicit default: an unbounded list returns everything up to the hard cap
372
+ const limit = Math.min(options.limit ?? this.MAX_LIST_LIMIT, this.MAX_LIST_LIMIT);
373
+ const offset = options.offset || 0;
374
+ let query = `SELECT * FROM ${this.tableName('documents')} WHERE collection = $1`;
375
+ const params = [collection];
376
+ if (options.filter) {
377
+ const clauses = this.buildDocumentFilterClauses(options.filter, params);
378
+ if (clauses.length > 0) {
379
+ query += ` AND ${clauses.join(' AND ')}`;
380
+ }
381
+ }
382
+ if (options.sort) {
383
+ const sortFields = Array.isArray(options.sort) ? options.sort : [options.sort];
384
+ query += ` ORDER BY ${sortFields.map(field => this.buildDocumentSortClause(field)).join(', ')}`;
385
+ }
386
+ else {
387
+ query += ` ORDER BY created_at DESC`;
388
+ }
389
+ query += ` LIMIT $${params.length + 1} OFFSET $${params.length + 2}`;
390
+ params.push(limit, offset);
391
+ this.logger.debug('Listing documents', { collection, query, params });
392
+ const result = await this.query(query, params);
393
+ return result.rows.map((row) => this.mapRowToDocument(row));
394
+ }
395
+ async deleteDocument(collection, id) {
396
+ SecurityValidator.validateCollectionName(collection);
397
+ SecurityValidator.validateDocumentId(id);
398
+ const result = await this.query(`DELETE FROM ${this.tableName('documents')} WHERE collection = $1 AND id = $2`, [collection, id]);
399
+ if (result.rowCount === 0) {
400
+ throw new InvalidInputError(`Document ${collection}:${id} not found`);
401
+ }
402
+ }
403
+ async documentExists(collection, id) {
404
+ SecurityValidator.validateCollectionName(collection);
405
+ SecurityValidator.validateDocumentId(id);
406
+ const result = await this.query(`SELECT 1 FROM ${this.tableName('documents')} WHERE collection = $1 AND id = $2 LIMIT 1`, [collection, id]);
407
+ return result.rows.length > 0;
408
+ }
409
+ async countDocuments(collection, filter) {
410
+ SecurityValidator.validateCollectionName(collection);
411
+ let query = `SELECT COUNT(*) as count FROM ${this.tableName('documents')} WHERE collection = $1`;
412
+ const params = [collection];
413
+ if (filter) {
414
+ // Same clause builder as listDocuments: same field validation, same jsonb equality,
415
+ // same system-field column mapping
416
+ const clauses = this.buildDocumentFilterClauses(filter, params);
417
+ if (clauses.length > 0) {
418
+ query += ` AND ${clauses.join(' AND ')}`;
419
+ }
420
+ }
421
+ const result = await this.query(query, params);
422
+ return parseInt(result.rows[0].count, 10);
423
+ }
424
+ // ==========================================================================
425
+ // AUDIT LOG OPERATIONS
426
+ // ==========================================================================
427
+ async createAuditLog(auditLog) {
428
+ const id = `audit_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
429
+ const result = await this.query(`
430
+ INSERT INTO ${this.tableName('audit_logs')}
431
+ (id, operation, resource_type, resource_id, actor_type, actor_id, changes, metadata, timestamp)
432
+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
433
+ RETURNING *
434
+ `, [
435
+ id,
436
+ auditLog.operation,
437
+ auditLog.collection,
438
+ auditLog.documentId,
439
+ auditLog.actorType,
440
+ auditLog.actorId,
441
+ auditLog.changes,
442
+ auditLog.metadata || {},
443
+ auditLog.timestamp
444
+ ]);
445
+ const row = result.rows[0];
446
+ return {
447
+ id: row.id,
448
+ documentId: row.resource_id,
449
+ collection: row.resource_type,
450
+ operation: row.operation,
451
+ actorType: row.actor_type,
452
+ actorId: row.actor_id,
453
+ actorUsername: auditLog.actorUsername,
454
+ changes: row.changes,
455
+ timestamp: row.timestamp,
456
+ revision: auditLog.revision,
457
+ ipAddress: auditLog.ipAddress,
458
+ userAgent: auditLog.userAgent,
459
+ sessionId: auditLog.sessionId,
460
+ metadata: row.metadata
461
+ };
462
+ }
463
+ /**
464
+ * Get audit logs for a specific document
465
+ */
466
+ async getDocumentAuditLogs(documentId, options = {}) {
467
+ const limit = options.limit || 100;
468
+ const offset = options.offset || 0;
469
+ const result = await this.query(`
470
+ SELECT * FROM ${this.tableName('audit_logs')}
471
+ WHERE resource_id = $1
472
+ ORDER BY timestamp DESC
473
+ LIMIT $2 OFFSET $3
474
+ `, [documentId, limit, offset]);
475
+ return result.rows.map((row) => ({
476
+ id: row.id,
477
+ documentId: row.resource_id,
478
+ collection: row.resource_type,
479
+ operation: row.operation,
480
+ actorType: row.actor_type,
481
+ actorId: row.actor_id,
482
+ changes: row.changes,
483
+ timestamp: row.timestamp,
484
+ metadata: row.metadata
485
+ }));
486
+ }
487
+ /**
488
+ * Get audit logs for a collection
489
+ */
490
+ async getCollectionAuditLogs(collection, options = {}) {
491
+ const limit = options.limit || 100;
492
+ const offset = options.offset || 0;
493
+ const result = await this.query(`
494
+ SELECT * FROM ${this.tableName('audit_logs')}
495
+ WHERE resource_type = $1
496
+ ORDER BY timestamp DESC
497
+ LIMIT $2 OFFSET $3
498
+ `, [collection, limit, offset]);
499
+ return result.rows.map((row) => ({
500
+ id: row.id,
501
+ documentId: row.resource_id,
502
+ collection: row.resource_type,
503
+ operation: row.operation,
504
+ actorType: row.actor_type,
505
+ actorId: row.actor_id,
506
+ changes: row.changes,
507
+ timestamp: row.timestamp,
508
+ metadata: row.metadata
509
+ }));
510
+ }
511
+ /**
512
+ * Get audit logs by actor
513
+ */
514
+ async getActorAuditLogs(actorId, options = {}) {
515
+ const limit = options.limit || 100;
516
+ const offset = options.offset || 0;
517
+ const result = await this.query(`
518
+ SELECT * FROM ${this.tableName('audit_logs')}
519
+ WHERE actor_id = $1
520
+ ORDER BY timestamp DESC
521
+ LIMIT $2 OFFSET $3
522
+ `, [actorId, limit, offset]);
523
+ return result.rows.map((row) => ({
524
+ id: row.id,
525
+ documentId: row.resource_id,
526
+ collection: row.resource_type,
527
+ operation: row.operation,
528
+ actorType: row.actor_type,
529
+ actorId: row.actor_id,
530
+ changes: row.changes,
531
+ timestamp: row.timestamp,
532
+ metadata: row.metadata
533
+ }));
534
+ }
535
+ // ==========================================================================
536
+ // UTILITY OPERATIONS
537
+ // ==========================================================================
538
+ async healthCheck() {
539
+ try {
540
+ const result = await this.pool.query('SELECT 1 as health');
541
+ return result.rows[0].health === 1;
542
+ }
543
+ catch (error) {
544
+ this.logger.error('Health check failed', error);
545
+ return false;
546
+ }
547
+ }
548
+ async getStorageInfo() {
549
+ const [collectionsResult, docsResult, usersResult, tokensResult] = await Promise.all([
550
+ this.query(`SELECT DISTINCT collection FROM ${this.tableName('documents')}`),
551
+ this.query(`SELECT COUNT(*) as count FROM ${this.tableName('documents')}`),
552
+ this.query(`SELECT COUNT(*) as count FROM ${this.tableName('users')}`),
553
+ this.query(`SELECT COUNT(*) as count FROM ${this.tableName('app_tokens')}`)
554
+ ]);
555
+ return {
556
+ collections: collectionsResult.rows.map((row) => row.collection),
557
+ documentCount: parseInt(docsResult.rows[0].count, 10),
558
+ userCount: parseInt(usersResult.rows[0].count, 10),
559
+ tokenCount: parseInt(tokensResult.rows[0].count, 10),
560
+ database: 'postgresql'
561
+ };
562
+ }
563
+ async beginTransaction() {
564
+ const client = await this.pool.connect();
565
+ await client.query('BEGIN');
566
+ return new PostgresTransaction(client);
567
+ }
568
+ // ==========================================================================
569
+ // USER OPERATIONS
570
+ // ==========================================================================
571
+ async getUser(id) {
572
+ SecurityValidator.validateDocumentId(id);
573
+ const result = await this.query(`SELECT * FROM ${this.tableName('users')} WHERE id = $1`, [id]);
574
+ if (result.rows.length === 0) {
575
+ return null;
576
+ }
577
+ const row = result.rows[0];
578
+ return this.mapRowToUser(row);
579
+ }
580
+ async saveUser(id, userData) {
581
+ SecurityValidator.validateDocumentId(id);
582
+ const now = new Date().toISOString();
583
+ try {
584
+ // Check if this is an update (user exists) or create (new user)
585
+ const existingUser = await this.getUser(id);
586
+ if (existingUser) {
587
+ // Update existing user
588
+ const result = await this.updateUserRow(id, userData, now);
589
+ const row = result.rows[0];
590
+ return this.mapRowToUser(row);
591
+ }
592
+ else {
593
+ // Create new user
594
+ const createData = userData;
595
+ // Use passwordHash from caller (like filesystem adapter)
596
+ const passwordHash = createData.passwordHash || '';
597
+ // Every supplied field is persisted: a restore writes a user in one create, and
598
+ // hardcoding oauth_providers/mfa/passkeys or dropping lastLoginAt silently lost them.
599
+ // Fields that were not supplied are stored as SQL NULL (not '{}' / '[]') so they read
600
+ // back as undefined rather than as an empty value the caller never asked for.
601
+ const result = await this.query(`
602
+ INSERT INTO ${this.tableName('users')}
603
+ (id, username, email, password_hash, first_name, last_name, role, permissions, is_active, profile_image, preferences, oauth_providers, mfa, passkeys, last_login_at, created_at, updated_at)
604
+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $16)
605
+ RETURNING *
606
+ `, [
607
+ id,
608
+ createData.username,
609
+ createData.email,
610
+ passwordHash,
611
+ createData.firstName,
612
+ createData.lastName,
613
+ createData.role,
614
+ JSON.stringify(createData.permissions ?? []),
615
+ createData.isActive ?? true,
616
+ createData.profileImage ?? null,
617
+ jsonColumnValue(createData.preferences),
618
+ jsonColumnValue(createData.oauthProviders),
619
+ jsonColumnValue(createData.mfa),
620
+ jsonColumnValue(createData.passkeys),
621
+ createData.lastLoginAt ?? null,
622
+ now
623
+ ]);
624
+ const row = result.rows[0];
625
+ return this.mapRowToUser(row);
626
+ }
627
+ }
628
+ catch (error) {
629
+ this.logger.error('Failed to save user', { id, userData, error });
630
+ throw error;
631
+ }
632
+ }
633
+ async saveUserIf(id, userData, condition) {
634
+ SecurityValidator.validateDocumentId(id);
635
+ try {
636
+ const result = await this.updateUserRow(id, userData, new Date().toISOString(), condition.passwordHash);
637
+ if (!result.rowCount) {
638
+ return null;
639
+ }
640
+ const row = result.rows[0];
641
+ return this.mapRowToUser(row);
642
+ }
643
+ catch (error) {
644
+ this.logger.error('Failed to conditionally save user', { id, error });
645
+ throw error;
646
+ }
647
+ }
648
+ /**
649
+ * Shared UPDATE for user rows. When expectedPasswordHash is provided the
650
+ * write only applies while the stored hash still matches, in one statement.
651
+ *
652
+ * The SET list is built from the keys actually present in `updateData`. The previous
653
+ * `COALESCE($n, col)` form could not tell "omitted" from "explicitly null", so clearing a
654
+ * field was impossible: an explicit `null` silently kept the old value.
655
+ */
656
+ async updateUserRow(id, updateData, now, expectedPasswordHash) {
657
+ const params = [id];
658
+ const assignments = [];
659
+ for (const { key, column, json } of PostgresDataAdapter.USER_UPDATE_COLUMNS) {
660
+ // An absent key and an explicit `undefined` both mean "unchanged"; an explicit `null`
661
+ // writes NULL and clears the field.
662
+ if (!Object.prototype.hasOwnProperty.call(updateData, key)) {
663
+ continue;
664
+ }
665
+ const value = updateData[key];
666
+ if (value === undefined) {
667
+ continue;
668
+ }
669
+ params.push(json ? jsonColumnValue(value) : value);
670
+ assignments.push(`${column} = $${params.length}`);
671
+ }
672
+ params.push(now);
673
+ assignments.push(`updated_at = $${params.length}`);
674
+ let condition = '';
675
+ if (expectedPasswordHash !== undefined) {
676
+ params.push(expectedPasswordHash);
677
+ condition = ` AND password_hash = $${params.length}`;
678
+ }
679
+ return await this.query(`
680
+ UPDATE ${this.tableName('users')}
681
+ SET
682
+ ${assignments.join(',\n ')}
683
+ WHERE id = $1${condition}
684
+ RETURNING *
685
+ `, params);
686
+ }
687
+ async listUsers(options = {}) {
688
+ // No implicit default: an unbounded list returns everything up to the hard cap
689
+ const limit = Math.min(options.limit ?? this.MAX_LIST_LIMIT, this.MAX_LIST_LIMIT);
690
+ const offset = options.offset || 0;
691
+ let query = `SELECT * FROM ${this.tableName('users')}`;
692
+ const params = [];
693
+ const conditions = [];
694
+ // Add filtering
695
+ if (options.role) {
696
+ conditions.push(`role = $${params.length + 1}`);
697
+ params.push(options.role);
698
+ }
699
+ if (options.isActive !== undefined) {
700
+ conditions.push(`is_active = $${params.length + 1}`);
701
+ params.push(options.isActive);
702
+ }
703
+ if (conditions.length > 0) {
704
+ query += ` WHERE ${conditions.join(' AND ')}`;
705
+ }
706
+ query += ` ORDER BY created_at DESC LIMIT $${params.length + 1} OFFSET $${params.length + 2}`;
707
+ params.push(limit, offset);
708
+ const result = await this.query(query, params);
709
+ return result.rows.map((row) => this.mapRowToUser(row));
710
+ }
711
+ async deleteUser(id) {
712
+ SecurityValidator.validateDocumentId(id);
713
+ const result = await this.query(`DELETE FROM ${this.tableName('users')} WHERE id = $1`, [id]);
714
+ if (result.rowCount === 0) {
715
+ throw new InvalidInputError(`User ${id} not found`);
716
+ }
717
+ }
718
+ async getUserByUsername(username) {
719
+ if (!username || typeof username !== 'string') {
720
+ throw new InvalidInputError('Username is required');
721
+ }
722
+ const result = await this.query(`SELECT * FROM ${this.tableName('users')} WHERE username = $1`, [username]);
723
+ if (result.rows.length === 0) {
724
+ return null;
725
+ }
726
+ const row = result.rows[0];
727
+ return this.mapRowToUser(row);
728
+ }
729
+ async getUserByEmail(email) {
730
+ if (!email || typeof email !== 'string') {
731
+ throw new InvalidInputError('Email is required');
732
+ }
733
+ const result = await this.query(`SELECT * FROM ${this.tableName('users')} WHERE email = $1`, [email]);
734
+ if (result.rows.length === 0) {
735
+ return null;
736
+ }
737
+ const row = result.rows[0];
738
+ return this.mapRowToUser(row);
739
+ }
740
+ async isUsernameAvailable(username) {
741
+ const user = await this.getUserByUsername(username);
742
+ return user === null;
743
+ }
744
+ async isEmailAvailable(email) {
745
+ const user = await this.getUserByEmail(email);
746
+ return user === null;
747
+ }
748
+ async getUserByOAuthProvider(provider, providerId) {
749
+ if (!provider || !providerId) {
750
+ return null;
751
+ }
752
+ // Query using JSONB containment operator to find user with matching OAuth provider
753
+ const result = await this.query(`SELECT * FROM ${this.tableName('users')}
754
+ WHERE oauth_providers @> $1::jsonb`, [JSON.stringify([{ provider, providerId }])]);
755
+ if (result.rows.length === 0) {
756
+ return null;
757
+ }
758
+ const row = result.rows[0];
759
+ return this.mapRowToUser(row);
760
+ }
761
+ async getUserByPasskeyCredentialId(credentialId) {
762
+ if (!credentialId || typeof credentialId !== 'string') {
763
+ return null;
764
+ }
765
+ // Security: Validate credential ID format (base64url)
766
+ // Base64url uses A-Z, a-z, 0-9, -, and _ characters
767
+ if (!/^[A-Za-z0-9_-]+$/.test(credentialId)) {
768
+ this.logger.warn('Invalid passkey credential ID format', {
769
+ credentialIdPrefix: credentialId.substring(0, 8)
770
+ });
771
+ return null;
772
+ }
773
+ // Query using JSONB containment operator to find user with matching passkey credential ID
774
+ const result = await this.query(`SELECT * FROM ${this.tableName('users')}
775
+ WHERE passkeys @> $1::jsonb`, [JSON.stringify([{ id: credentialId }])]);
776
+ if (result.rows.length === 0) {
777
+ return null;
778
+ }
779
+ const row = result.rows[0];
780
+ return this.mapRowToUser(row);
781
+ }
782
+ /** Every nullable column is mapped to `undefined`: the contract never surfaces `null`. */
783
+ mapRowToUser(row) {
784
+ return {
785
+ id: row.id,
786
+ username: row.username,
787
+ email: row.email,
788
+ passwordHash: row.password_hash,
789
+ firstName: row.first_name,
790
+ lastName: row.last_name,
791
+ role: row.role,
792
+ // Created users always get `[]`; only an update that explicitly clears permissions
793
+ // leaves NULL behind, and a cleared field reads back as undefined
794
+ permissions: (row.permissions ?? undefined),
795
+ isActive: row.is_active,
796
+ profileImage: row.profile_image ?? undefined,
797
+ preferences: row.preferences ?? undefined,
798
+ oauthProviders: row.oauth_providers ?? undefined,
799
+ mfa: row.mfa ?? undefined,
800
+ passkeys: row.passkeys ?? undefined,
801
+ lastLoginAt: row.last_login_at ?? undefined,
802
+ createdAt: row.created_at,
803
+ updatedAt: row.updated_at
804
+ };
805
+ }
806
+ // ==========================================================================
807
+ // APP TOKEN OPERATIONS
808
+ // ==========================================================================
809
+ async getAppToken(id) {
810
+ SecurityValidator.validateDocumentId(id);
811
+ const result = await this.query(`SELECT * FROM ${this.tableName('app_tokens')} WHERE id = $1`, [id]);
812
+ if (result.rows.length === 0) {
813
+ return null;
814
+ }
815
+ const row = result.rows[0];
816
+ return this.mapRowToAppToken(row);
817
+ }
818
+ async saveAppToken(id, tokenData) {
819
+ SecurityValidator.validateDocumentId(id);
820
+ const now = new Date();
821
+ try {
822
+ // Check if this is an update (token exists) or create (new token)
823
+ const existingToken = await this.getAppToken(id);
824
+ if (existingToken) {
825
+ // Update existing token
826
+ const updateData = tokenData;
827
+ const result = await this.query(`
828
+ UPDATE ${this.tableName('app_tokens')}
829
+ SET
830
+ name = COALESCE($2, name),
831
+ permissions = COALESCE($3, permissions),
832
+ is_active = COALESCE($4, is_active),
833
+ expires_at = COALESCE($5, expires_at),
834
+ updated_at = $6
835
+ WHERE id = $1
836
+ RETURNING *
837
+ `, [
838
+ id,
839
+ updateData.name,
840
+ JSON.stringify(updateData.permissions || []),
841
+ updateData.isActive,
842
+ updateData.expiresAt ? new Date(updateData.expiresAt) : null,
843
+ now
844
+ ]);
845
+ const row = result.rows[0];
846
+ return this.mapRowToAppToken(row);
847
+ }
848
+ else {
849
+ // Create new token
850
+ const createData = tokenData;
851
+ // Use tokenHash from caller (this should be set by the calling code)
852
+ const tokenHash = createData.tokenHash || '';
853
+ const createdBy = createData.createdBy || 'system';
854
+ const result = await this.query(`
855
+ INSERT INTO ${this.tableName('app_tokens')}
856
+ (id, name, hash, permissions, created_by, is_active, expires_at, created_at, updated_at)
857
+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $8)
858
+ RETURNING *
859
+ `, [
860
+ id,
861
+ createData.name,
862
+ tokenHash,
863
+ JSON.stringify(createData.permissions || []),
864
+ createdBy,
865
+ true,
866
+ createData.expiresAt ? new Date(createData.expiresAt) : null,
867
+ now
868
+ ]);
869
+ const row = result.rows[0];
870
+ return this.mapRowToAppToken(row);
871
+ }
872
+ }
873
+ catch (error) {
874
+ this.logger.error('Failed to save app token', { id, tokenData, error });
875
+ throw error;
876
+ }
877
+ }
878
+ async listAppTokens(options = {}) {
879
+ const limit = Math.min(options.limit || 50, this.MAX_LIST_LIMIT);
880
+ const offset = options.offset || 0;
881
+ let query = `SELECT * FROM ${this.tableName('app_tokens')}`;
882
+ const params = [];
883
+ const conditions = [];
884
+ // Add filtering
885
+ if (options.createdBy) {
886
+ conditions.push(`created_by = $${params.length + 1}`);
887
+ params.push(options.createdBy);
888
+ }
889
+ if (options.isActive !== undefined) {
890
+ conditions.push(`is_active = $${params.length + 1}`);
891
+ params.push(options.isActive);
892
+ }
893
+ if (conditions.length > 0) {
894
+ query += ` WHERE ${conditions.join(' AND ')}`;
895
+ }
896
+ query += ` ORDER BY created_at DESC LIMIT $${params.length + 1} OFFSET $${params.length + 2}`;
897
+ params.push(limit, offset);
898
+ const result = await this.query(query, params);
899
+ return result.rows.map((row) => this.mapRowToAppToken(row));
900
+ }
901
+ async deleteAppToken(id) {
902
+ SecurityValidator.validateDocumentId(id);
903
+ const result = await this.query(`DELETE FROM ${this.tableName('app_tokens')} WHERE id = $1`, [id]);
904
+ if (result.rowCount === 0) {
905
+ throw new InvalidInputError(`App token ${id} not found`);
906
+ }
907
+ }
908
+ async getAppTokenByHash(hash) {
909
+ if (!hash || typeof hash !== 'string') {
910
+ throw new InvalidInputError('Token hash is required');
911
+ }
912
+ const result = await this.query(`SELECT * FROM ${this.tableName('app_tokens')} WHERE hash = $1 AND is_active = true`, [hash]);
913
+ if (result.rows.length === 0) {
914
+ return null;
915
+ }
916
+ const row = result.rows[0];
917
+ // Update last_used_at
918
+ await this.query(`UPDATE ${this.tableName('app_tokens')} SET last_used_at = $1 WHERE id = $2`, [new Date(), row.id]);
919
+ return this.mapRowToAppToken(row);
920
+ }
921
+ mapRowToAppToken(row) {
922
+ return {
923
+ id: row.id,
924
+ name: row.name,
925
+ description: '', // Not stored in current schema, could be added
926
+ tokenHash: row.hash,
927
+ permissions: row.permissions,
928
+ createdBy: row.created_by,
929
+ isActive: row.is_active,
930
+ lastUsedAt: row.last_used_at ? row.last_used_at.toISOString() : undefined,
931
+ usageCount: 0, // Not tracked in current schema, could be added
932
+ expiresAt: row.expires_at ? row.expires_at.toISOString() : undefined,
933
+ createdAt: row.created_at.toISOString(),
934
+ updatedAt: row.updated_at.toISOString()
935
+ };
936
+ }
937
+ // ==========================================================================
938
+ // MIGRATION SUPPORT
939
+ // ==========================================================================
940
+ async runMigrations() {
941
+ this.logger.info('Creating migrations tracking table...');
942
+ // Create migrations table if it doesn't exist
943
+ await this.directQuery(`
944
+ CREATE TABLE IF NOT EXISTS ${this.tableName('migrations')} (
945
+ id VARCHAR(255) PRIMARY KEY,
946
+ name VARCHAR(255) NOT NULL,
947
+ applied_at TIMESTAMP DEFAULT NOW(),
948
+ checksum VARCHAR(255) NOT NULL
949
+ )
950
+ `);
951
+ this.logger.info('Running initial schema migrations...');
952
+ // Run initial schema migrations
953
+ await this.createTables();
954
+ this.logger.info('Schema migrations completed');
955
+ }
956
+ async createTables() {
957
+ this.logger.info('Creating documents table...');
958
+ // Documents table
959
+ await this.directQuery(`
960
+ CREATE TABLE IF NOT EXISTS ${this.tableName('documents')} (
961
+ collection VARCHAR(255) NOT NULL,
962
+ id VARCHAR(255) NOT NULL,
963
+ data JSONB NOT NULL,
964
+ created_at TIMESTAMP DEFAULT NOW(),
965
+ updated_at TIMESTAMP DEFAULT NOW(),
966
+ created_by VARCHAR(255),
967
+ updated_by VARCHAR(255),
968
+ created_by_type VARCHAR(50),
969
+ updated_by_type VARCHAR(50),
970
+ revision INTEGER NOT NULL DEFAULT 1,
971
+ PRIMARY KEY (collection, id)
972
+ )
973
+ `);
974
+ // Migrations for databases created before revision / actor-type tracking existed.
975
+ // ADD COLUMN IF NOT EXISTS keeps them safe to re-run on every boot.
976
+ for (const [column, definition] of [
977
+ ['revision', 'INTEGER NOT NULL DEFAULT 1'],
978
+ ['created_by_type', 'VARCHAR(50)'],
979
+ ['updated_by_type', 'VARCHAR(50)']
980
+ ]) {
981
+ await this.directQuery(`
982
+ DO $$
983
+ BEGIN
984
+ IF NOT EXISTS (
985
+ SELECT 1 FROM information_schema.columns
986
+ WHERE table_schema = '${this.config.schema}'
987
+ AND table_name = '${this.config.tablePrefix}documents'
988
+ AND column_name = '${column}'
989
+ ) THEN
990
+ ALTER TABLE ${this.tableName('documents')}
991
+ ADD COLUMN IF NOT EXISTS ${column} ${definition};
992
+ END IF;
993
+ END $$
994
+ `);
995
+ }
996
+ this.logger.info('Creating performance indexes for documents...');
997
+ // Create indexes for better performance
998
+ await this.directQuery(`
999
+ CREATE INDEX IF NOT EXISTS ${this.config.tablePrefix}documents_collection_idx
1000
+ ON ${this.tableName('documents')} (collection)
1001
+ `);
1002
+ await this.directQuery(`
1003
+ CREATE INDEX IF NOT EXISTS ${this.config.tablePrefix}documents_updated_at_idx
1004
+ ON ${this.tableName('documents')} (updated_at DESC)
1005
+ `);
1006
+ // The default list order is created_at DESC
1007
+ await this.directQuery(`
1008
+ CREATE INDEX IF NOT EXISTS ${this.config.tablePrefix}documents_created_at_idx
1009
+ ON ${this.tableName('documents')} (created_at DESC)
1010
+ `);
1011
+ await this.directQuery(`
1012
+ CREATE INDEX IF NOT EXISTS ${this.config.tablePrefix}documents_data_gin_idx
1013
+ ON ${this.tableName('documents')} USING GIN (data)
1014
+ `);
1015
+ this.logger.info('Creating audit logs table...');
1016
+ // Audit logs table
1017
+ await this.directQuery(`
1018
+ CREATE TABLE IF NOT EXISTS ${this.tableName('audit_logs')} (
1019
+ id VARCHAR(255) PRIMARY KEY,
1020
+ operation VARCHAR(100) NOT NULL,
1021
+ resource_type VARCHAR(100) NOT NULL,
1022
+ resource_id VARCHAR(255) NOT NULL,
1023
+ actor_type VARCHAR(50) NOT NULL,
1024
+ actor_id VARCHAR(255) NOT NULL,
1025
+ changes JSONB,
1026
+ metadata JSONB,
1027
+ timestamp TIMESTAMP DEFAULT NOW()
1028
+ )
1029
+ `);
1030
+ this.logger.info('Creating performance indexes for audit logs...');
1031
+ await this.directQuery(`
1032
+ CREATE INDEX IF NOT EXISTS ${this.config.tablePrefix}audit_logs_resource_idx
1033
+ ON ${this.tableName('audit_logs')} (resource_type, resource_id)
1034
+ `);
1035
+ await this.directQuery(`
1036
+ CREATE INDEX IF NOT EXISTS ${this.config.tablePrefix}audit_logs_timestamp_idx
1037
+ ON ${this.tableName('audit_logs')} (timestamp DESC)
1038
+ `);
1039
+ this.logger.info('Creating users table...');
1040
+ // Users table
1041
+ await this.directQuery(`
1042
+ CREATE TABLE IF NOT EXISTS ${this.tableName('users')} (
1043
+ id VARCHAR(255) PRIMARY KEY,
1044
+ username VARCHAR(255) UNIQUE NOT NULL,
1045
+ email VARCHAR(255) UNIQUE NOT NULL,
1046
+ password_hash VARCHAR(255) NOT NULL,
1047
+ first_name VARCHAR(255) NOT NULL,
1048
+ last_name VARCHAR(255) NOT NULL,
1049
+ role VARCHAR(50) NOT NULL DEFAULT 'viewer',
1050
+ permissions JSONB DEFAULT '[]',
1051
+ is_active BOOLEAN NOT NULL DEFAULT true,
1052
+ profile_image VARCHAR(500),
1053
+ last_login_at VARCHAR(50),
1054
+ preferences JSONB DEFAULT '{}',
1055
+ oauth_providers JSONB DEFAULT '[]',
1056
+ mfa JSONB DEFAULT '{}',
1057
+ passkeys JSONB DEFAULT '[]',
1058
+ created_at VARCHAR(50) NOT NULL,
1059
+ updated_at VARCHAR(50) NOT NULL
1060
+ )
1061
+ `);
1062
+ // Migration: Add oauth_providers column if it doesn't exist (for existing databases)
1063
+ await this.directQuery(`
1064
+ DO $$
1065
+ BEGIN
1066
+ IF NOT EXISTS (
1067
+ SELECT 1 FROM information_schema.columns
1068
+ WHERE table_schema = '${this.config.schema}'
1069
+ AND table_name = '${this.config.tablePrefix}users'
1070
+ AND column_name = 'oauth_providers'
1071
+ ) THEN
1072
+ ALTER TABLE ${this.tableName('users')}
1073
+ ADD COLUMN oauth_providers JSONB DEFAULT '[]';
1074
+ END IF;
1075
+ END $$
1076
+ `);
1077
+ // Migration: Add mfa column if it doesn't exist (for existing databases)
1078
+ await this.directQuery(`
1079
+ DO $$
1080
+ BEGIN
1081
+ IF NOT EXISTS (
1082
+ SELECT 1 FROM information_schema.columns
1083
+ WHERE table_schema = '${this.config.schema}'
1084
+ AND table_name = '${this.config.tablePrefix}users'
1085
+ AND column_name = 'mfa'
1086
+ ) THEN
1087
+ ALTER TABLE ${this.tableName('users')}
1088
+ ADD COLUMN mfa JSONB DEFAULT '{}';
1089
+ END IF;
1090
+ END $$
1091
+ `);
1092
+ // Migration: Add passkeys column if it doesn't exist (for existing databases)
1093
+ await this.directQuery(`
1094
+ DO $$
1095
+ BEGIN
1096
+ IF NOT EXISTS (
1097
+ SELECT 1 FROM information_schema.columns
1098
+ WHERE table_schema = '${this.config.schema}'
1099
+ AND table_name = '${this.config.tablePrefix}users'
1100
+ AND column_name = 'passkeys'
1101
+ ) THEN
1102
+ ALTER TABLE ${this.tableName('users')}
1103
+ ADD COLUMN passkeys JSONB DEFAULT '[]';
1104
+ END IF;
1105
+ END $$
1106
+ `);
1107
+ await this.directQuery(`
1108
+ CREATE INDEX IF NOT EXISTS ${this.config.tablePrefix}users_username_idx
1109
+ ON ${this.tableName('users')} (username)
1110
+ `);
1111
+ await this.directQuery(`
1112
+ CREATE INDEX IF NOT EXISTS ${this.config.tablePrefix}users_email_idx
1113
+ ON ${this.tableName('users')} (email)
1114
+ `);
1115
+ // GIN index for efficient OAuth provider lookups
1116
+ await this.directQuery(`
1117
+ CREATE INDEX IF NOT EXISTS ${this.config.tablePrefix}users_oauth_providers_idx
1118
+ ON ${this.tableName('users')} USING GIN (oauth_providers)
1119
+ `);
1120
+ // GIN index for efficient passkey credential lookups
1121
+ await this.directQuery(`
1122
+ CREATE INDEX IF NOT EXISTS ${this.config.tablePrefix}users_passkeys_idx
1123
+ ON ${this.tableName('users')} USING GIN (passkeys)
1124
+ `);
1125
+ this.logger.info('Creating app tokens table...');
1126
+ // App tokens table
1127
+ await this.directQuery(`
1128
+ CREATE TABLE IF NOT EXISTS ${this.tableName('app_tokens')} (
1129
+ id VARCHAR(255) PRIMARY KEY,
1130
+ name VARCHAR(255) NOT NULL,
1131
+ hash VARCHAR(255) UNIQUE NOT NULL,
1132
+ permissions JSONB DEFAULT '[]',
1133
+ created_by VARCHAR(255) NOT NULL,
1134
+ is_active BOOLEAN NOT NULL DEFAULT true,
1135
+ last_used_at TIMESTAMP,
1136
+ expires_at TIMESTAMP,
1137
+ created_at TIMESTAMP DEFAULT NOW(),
1138
+ updated_at TIMESTAMP DEFAULT NOW()
1139
+ )
1140
+ `);
1141
+ await this.directQuery(`
1142
+ CREATE INDEX IF NOT EXISTS ${this.config.tablePrefix}app_tokens_hash_idx
1143
+ ON ${this.tableName('app_tokens')} (hash)
1144
+ `);
1145
+ this.logger.info('Creating webhooks table...');
1146
+ // Webhooks table
1147
+ await this.directQuery(`
1148
+ CREATE TABLE IF NOT EXISTS ${this.tableName('webhooks')} (
1149
+ id VARCHAR(255) PRIMARY KEY,
1150
+ name VARCHAR(255) NOT NULL,
1151
+ url VARCHAR(500) NOT NULL,
1152
+ events JSONB DEFAULT '[]',
1153
+ headers JSONB DEFAULT '{}',
1154
+ is_active BOOLEAN NOT NULL DEFAULT true,
1155
+ secret VARCHAR(255),
1156
+ timeout_ms INTEGER DEFAULT 5000,
1157
+ retry_attempts INTEGER DEFAULT 3,
1158
+ created_by VARCHAR(255) NOT NULL,
1159
+ created_at TIMESTAMP DEFAULT NOW(),
1160
+ updated_at TIMESTAMP DEFAULT NOW()
1161
+ )
1162
+ `);
1163
+ this.logger.info('Creating settings table...');
1164
+ // Settings table
1165
+ await this.directQuery(`
1166
+ CREATE TABLE IF NOT EXISTS ${this.tableName('settings')} (
1167
+ id VARCHAR(255) PRIMARY KEY,
1168
+ public_url VARCHAR(500),
1169
+ studio_title VARCHAR(255) DEFAULT 'Trokky Studio',
1170
+ default_theme VARCHAR(50) DEFAULT 'system',
1171
+ config JSONB DEFAULT '{}',
1172
+ created_at TIMESTAMP DEFAULT NOW(),
1173
+ updated_at TIMESTAMP DEFAULT NOW(),
1174
+ updated_by VARCHAR(255)
1175
+ )
1176
+ `);
1177
+ this.logger.info('✅ All database tables and indexes created successfully');
1178
+ }
1179
+ /**
1180
+ * Get settings from database
1181
+ */
1182
+ async getSettings() {
1183
+ try {
1184
+ const result = await this.pool.query(`SELECT * FROM ${this.config.tablePrefix}settings WHERE id = $1`, ['studio-settings']);
1185
+ if (result.rows.length === 0) {
1186
+ return null;
1187
+ }
1188
+ const row = result.rows[0];
1189
+ return {
1190
+ id: row.id,
1191
+ publicUrl: row.public_url,
1192
+ studioTitle: row.studio_title,
1193
+ organizationName: row.config?.organizationName,
1194
+ primaryColor: row.config?.primaryColor,
1195
+ secondaryColor: row.config?.secondaryColor,
1196
+ logo: row.config?.logo,
1197
+ defaultTheme: row.default_theme,
1198
+ // MFA settings
1199
+ mfaRequired: row.config?.mfaRequired,
1200
+ mfaEnforcedRoles: row.config?.mfaEnforcedRoles,
1201
+ mfaAllowedMethods: row.config?.mfaAllowedMethods,
1202
+ mfaTrustDeviceDays: row.config?.mfaTrustDeviceDays,
1203
+ mfaGracePeriodDays: row.config?.mfaGracePeriodDays,
1204
+ _createdAt: row.created_at.toISOString(),
1205
+ _updatedAt: row.updated_at.toISOString(),
1206
+ _updatedBy: row.updated_by
1207
+ };
1208
+ }
1209
+ catch (error) {
1210
+ this.logger.error('Failed to get settings', { error });
1211
+ throw error;
1212
+ }
1213
+ }
1214
+ /**
1215
+ * Save settings to database
1216
+ */
1217
+ async saveSettings(settings) {
1218
+ try {
1219
+ // Extract branding and MFA fields for config JSONB
1220
+ const config = {
1221
+ organizationName: settings.organizationName,
1222
+ primaryColor: settings.primaryColor,
1223
+ secondaryColor: settings.secondaryColor,
1224
+ logo: settings.logo,
1225
+ // MFA settings
1226
+ mfaRequired: settings.mfaRequired,
1227
+ mfaEnforcedRoles: settings.mfaEnforcedRoles,
1228
+ mfaAllowedMethods: settings.mfaAllowedMethods,
1229
+ mfaTrustDeviceDays: settings.mfaTrustDeviceDays,
1230
+ mfaGracePeriodDays: settings.mfaGracePeriodDays
1231
+ };
1232
+ await this.query(`INSERT INTO ${this.tableName('settings')}
1233
+ (id, public_url, studio_title, default_theme, config, created_at, updated_at, updated_by)
1234
+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
1235
+ ON CONFLICT (id)
1236
+ DO UPDATE SET
1237
+ public_url = EXCLUDED.public_url,
1238
+ studio_title = EXCLUDED.studio_title,
1239
+ default_theme = EXCLUDED.default_theme,
1240
+ config = EXCLUDED.config,
1241
+ updated_at = EXCLUDED.updated_at,
1242
+ updated_by = EXCLUDED.updated_by`, [
1243
+ settings.id || 'studio-settings',
1244
+ settings.publicUrl,
1245
+ settings.studioTitle,
1246
+ settings.defaultTheme,
1247
+ JSON.stringify(config),
1248
+ settings._createdAt || new Date().toISOString(),
1249
+ settings._updatedAt || new Date().toISOString(),
1250
+ settings._updatedBy || 'system'
1251
+ ]);
1252
+ this.logger.info('Settings saved successfully', { id: settings.id, config });
1253
+ }
1254
+ catch (error) {
1255
+ this.logger.error('Failed to save settings', { error });
1256
+ throw error;
1257
+ }
1258
+ }
1259
+ // ==========================================================================
1260
+ // WEBHOOK OPERATIONS
1261
+ // ==========================================================================
1262
+ async getWebhook(id) {
1263
+ SecurityValidator.validateDocumentId(id);
1264
+ const result = await this.query(`SELECT * FROM ${this.tableName('webhooks')} WHERE id = $1`, [id]);
1265
+ if (result.rows.length === 0) {
1266
+ return null;
1267
+ }
1268
+ const row = result.rows[0];
1269
+ return this.mapRowToWebhook(row);
1270
+ }
1271
+ async saveWebhook(id, webhookData) {
1272
+ SecurityValidator.validateDocumentId(id);
1273
+ const now = new Date();
1274
+ try {
1275
+ // Check if this is an update (webhook exists) or create (new webhook)
1276
+ const existingWebhook = await this.getWebhook(id);
1277
+ if (existingWebhook) {
1278
+ // Update existing webhook
1279
+ const result = await this.query(`
1280
+ UPDATE ${this.tableName('webhooks')}
1281
+ SET
1282
+ name = COALESCE($2, name),
1283
+ url = COALESCE($3, url),
1284
+ events = COALESCE($4, events),
1285
+ headers = COALESCE($5, headers),
1286
+ is_active = COALESCE($6, is_active),
1287
+ secret = COALESCE($7, secret),
1288
+ timeout_ms = COALESCE($8, timeout_ms),
1289
+ retry_attempts = COALESCE($9, retry_attempts),
1290
+ updated_at = $10
1291
+ WHERE id = $1
1292
+ RETURNING *
1293
+ `, [
1294
+ id,
1295
+ webhookData.name,
1296
+ webhookData.url,
1297
+ webhookData.events ? JSON.stringify(webhookData.events) : null,
1298
+ webhookData.headers ? JSON.stringify(webhookData.headers) : null,
1299
+ webhookData.active,
1300
+ webhookData.secret,
1301
+ webhookData.retryPolicy?.baseDelay || null,
1302
+ webhookData.retryPolicy?.maxRetries || null,
1303
+ now
1304
+ ]);
1305
+ const row = result.rows[0];
1306
+ return this.mapRowToWebhook(row);
1307
+ }
1308
+ else {
1309
+ // Create new webhook
1310
+ const result = await this.query(`
1311
+ INSERT INTO ${this.tableName('webhooks')}
1312
+ (id, name, url, events, headers, is_active, secret, timeout_ms, retry_attempts, created_by, created_at, updated_at)
1313
+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $11)
1314
+ RETURNING *
1315
+ `, [
1316
+ id,
1317
+ webhookData.name || 'Untitled Webhook',
1318
+ webhookData.url || '',
1319
+ JSON.stringify(webhookData.events || []),
1320
+ JSON.stringify(webhookData.headers || {}),
1321
+ webhookData.active ?? true,
1322
+ webhookData.secret || '',
1323
+ webhookData.retryPolicy?.baseDelay || 5000,
1324
+ webhookData.retryPolicy?.maxRetries || 3,
1325
+ webhookData.createdBy || 'system',
1326
+ now
1327
+ ]);
1328
+ const row = result.rows[0];
1329
+ return this.mapRowToWebhook(row);
1330
+ }
1331
+ }
1332
+ catch (error) {
1333
+ this.logger.error('Failed to save webhook', { id, webhookData, error });
1334
+ throw error;
1335
+ }
1336
+ }
1337
+ async listWebhooks(options = {}) {
1338
+ const limit = Math.min(options.limit || 50, this.MAX_LIST_LIMIT);
1339
+ const offset = options.offset || 0;
1340
+ let query = `SELECT * FROM ${this.tableName('webhooks')}`;
1341
+ const params = [];
1342
+ const conditions = [];
1343
+ // Add filtering
1344
+ if (options.active !== undefined) {
1345
+ conditions.push(`is_active = $${params.length + 1}`);
1346
+ params.push(options.active);
1347
+ }
1348
+ if (options.createdBy) {
1349
+ conditions.push(`created_by = $${params.length + 1}`);
1350
+ params.push(options.createdBy);
1351
+ }
1352
+ if (options.events && options.events.length > 0) {
1353
+ // Match webhooks that have any of the specified events
1354
+ // Uses JSONB array contains operator
1355
+ conditions.push(`events ?| $${params.length + 1}`);
1356
+ params.push(options.events);
1357
+ }
1358
+ if (conditions.length > 0) {
1359
+ query += ` WHERE ${conditions.join(' AND ')}`;
1360
+ }
1361
+ query += ` ORDER BY created_at DESC LIMIT $${params.length + 1} OFFSET $${params.length + 2}`;
1362
+ params.push(limit, offset);
1363
+ const result = await this.query(query, params);
1364
+ return result.rows.map((row) => this.mapRowToWebhook(row));
1365
+ }
1366
+ async deleteWebhook(id) {
1367
+ SecurityValidator.validateDocumentId(id);
1368
+ const result = await this.query(`DELETE FROM ${this.tableName('webhooks')} WHERE id = $1`, [id]);
1369
+ if (result.rowCount === 0) {
1370
+ this.logger.debug(`Webhook ${id} does not exist, nothing to delete`);
1371
+ }
1372
+ else {
1373
+ this.logger.info(`Webhook deleted: ${id}`);
1374
+ }
1375
+ }
1376
+ mapRowToWebhook(row) {
1377
+ return {
1378
+ id: row.id,
1379
+ name: row.name,
1380
+ url: row.url,
1381
+ events: row.events || [],
1382
+ secret: row.secret || '',
1383
+ active: row.is_active,
1384
+ headers: row.headers || {},
1385
+ retryPolicy: {
1386
+ maxRetries: row.retry_attempts || 3,
1387
+ backoffType: 'exponential',
1388
+ baseDelay: row.timeout_ms || 5000,
1389
+ maxDelay: 30000,
1390
+ retryOnStatus: [500, 502, 503, 504, 408, 429]
1391
+ },
1392
+ createdBy: row.created_by,
1393
+ createdAt: row.created_at,
1394
+ updatedAt: row.updated_at
1395
+ };
1396
+ }
1397
+ }
1398
+ /**
1399
+ * PostgreSQL transaction implementation
1400
+ */
1401
+ export class PostgresTransaction {
1402
+ client;
1403
+ constructor(client) {
1404
+ this.client = client;
1405
+ }
1406
+ async commit() {
1407
+ try {
1408
+ await this.client.query('COMMIT');
1409
+ }
1410
+ finally {
1411
+ this.client.release();
1412
+ }
1413
+ }
1414
+ async rollback() {
1415
+ try {
1416
+ await this.client.query('ROLLBACK');
1417
+ }
1418
+ finally {
1419
+ this.client.release();
1420
+ }
1421
+ }
1422
+ async execute(operations) {
1423
+ try {
1424
+ return await operations();
1425
+ }
1426
+ catch (error) {
1427
+ await this.rollback();
1428
+ throw error;
1429
+ }
1430
+ }
1431
+ }
1432
+ //# sourceMappingURL=postgres-data-adapter.js.map