@qwickapps/server 1.2.0 → 1.3.1

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 (299) hide show
  1. package/README.md +392 -0
  2. package/dist/core/control-panel.d.ts +7 -2
  3. package/dist/core/control-panel.d.ts.map +1 -1
  4. package/dist/core/control-panel.js +120 -54
  5. package/dist/core/control-panel.js.map +1 -1
  6. package/dist/core/gateway.d.ts +159 -79
  7. package/dist/core/gateway.d.ts.map +1 -1
  8. package/dist/core/gateway.js +679 -319
  9. package/dist/core/gateway.js.map +1 -1
  10. package/dist/core/index.d.ts +3 -1
  11. package/dist/core/index.d.ts.map +1 -1
  12. package/dist/core/index.js +2 -0
  13. package/dist/core/index.js.map +1 -1
  14. package/dist/core/plugin-registry.d.ts +307 -0
  15. package/dist/core/plugin-registry.d.ts.map +1 -0
  16. package/dist/core/plugin-registry.js +352 -0
  17. package/dist/core/plugin-registry.js.map +1 -0
  18. package/dist/core/types.d.ts +16 -33
  19. package/dist/core/types.d.ts.map +1 -1
  20. package/dist/index.d.ts +8 -5
  21. package/dist/index.d.ts.map +1 -1
  22. package/dist/index.js +15 -7
  23. package/dist/index.js.map +1 -1
  24. package/dist/plugins/auth/adapters/auth0-adapter.d.ts +14 -0
  25. package/dist/plugins/auth/adapters/auth0-adapter.d.ts.map +1 -0
  26. package/dist/plugins/auth/adapters/auth0-adapter.js +179 -0
  27. package/dist/plugins/auth/adapters/auth0-adapter.js.map +1 -0
  28. package/dist/plugins/auth/adapters/basic-adapter.d.ts +13 -0
  29. package/dist/plugins/auth/adapters/basic-adapter.d.ts.map +1 -0
  30. package/dist/plugins/auth/adapters/basic-adapter.js +51 -0
  31. package/dist/plugins/auth/adapters/basic-adapter.js.map +1 -0
  32. package/dist/plugins/auth/adapters/index.d.ts +10 -0
  33. package/dist/plugins/auth/adapters/index.d.ts.map +1 -0
  34. package/dist/plugins/auth/adapters/index.js +10 -0
  35. package/dist/plugins/auth/adapters/index.js.map +1 -0
  36. package/dist/plugins/auth/adapters/supabase-adapter.d.ts +13 -0
  37. package/dist/plugins/auth/adapters/supabase-adapter.d.ts.map +1 -0
  38. package/dist/plugins/auth/adapters/supabase-adapter.js +109 -0
  39. package/dist/plugins/auth/adapters/supabase-adapter.js.map +1 -0
  40. package/dist/plugins/auth/adapters/supertokens-adapter.d.ts +18 -0
  41. package/dist/plugins/auth/adapters/supertokens-adapter.d.ts.map +1 -0
  42. package/dist/plugins/auth/adapters/supertokens-adapter.js +267 -0
  43. package/dist/plugins/auth/adapters/supertokens-adapter.js.map +1 -0
  44. package/dist/plugins/auth/auth-plugin.d.ts +40 -0
  45. package/dist/plugins/auth/auth-plugin.d.ts.map +1 -0
  46. package/dist/plugins/auth/auth-plugin.js +255 -0
  47. package/dist/plugins/auth/auth-plugin.js.map +1 -0
  48. package/dist/plugins/auth/auth-plugin.test.d.ts +9 -0
  49. package/dist/plugins/auth/auth-plugin.test.d.ts.map +1 -0
  50. package/dist/plugins/auth/auth-plugin.test.js +147 -0
  51. package/dist/plugins/auth/auth-plugin.test.js.map +1 -0
  52. package/dist/plugins/auth/env-config.d.ts +88 -0
  53. package/dist/plugins/auth/env-config.d.ts.map +1 -0
  54. package/dist/plugins/auth/env-config.js +489 -0
  55. package/dist/plugins/auth/env-config.js.map +1 -0
  56. package/dist/plugins/auth/index.d.ts +14 -0
  57. package/dist/plugins/auth/index.d.ts.map +1 -0
  58. package/dist/plugins/auth/index.js +16 -0
  59. package/dist/plugins/auth/index.js.map +1 -0
  60. package/dist/plugins/auth/supertokens-adapter.test.d.ts +10 -0
  61. package/dist/plugins/auth/supertokens-adapter.test.d.ts.map +1 -0
  62. package/dist/plugins/auth/supertokens-adapter.test.js +486 -0
  63. package/dist/plugins/auth/supertokens-adapter.test.js.map +1 -0
  64. package/dist/plugins/auth/types.d.ts +218 -0
  65. package/dist/plugins/auth/types.d.ts.map +1 -0
  66. package/dist/plugins/auth/types.js +14 -0
  67. package/dist/plugins/auth/types.js.map +1 -0
  68. package/dist/plugins/bans/bans-plugin.d.ts +59 -0
  69. package/dist/plugins/bans/bans-plugin.d.ts.map +1 -0
  70. package/dist/plugins/bans/bans-plugin.js +428 -0
  71. package/dist/plugins/bans/bans-plugin.js.map +1 -0
  72. package/dist/plugins/bans/index.d.ts +9 -0
  73. package/dist/plugins/bans/index.d.ts.map +1 -0
  74. package/dist/plugins/bans/index.js +10 -0
  75. package/dist/plugins/bans/index.js.map +1 -0
  76. package/dist/plugins/bans/stores/index.d.ts +7 -0
  77. package/dist/plugins/bans/stores/index.d.ts.map +1 -0
  78. package/dist/plugins/bans/stores/index.js +7 -0
  79. package/dist/plugins/bans/stores/index.js.map +1 -0
  80. package/dist/plugins/bans/stores/postgres-store.d.ts +29 -0
  81. package/dist/plugins/bans/stores/postgres-store.d.ts.map +1 -0
  82. package/dist/plugins/bans/stores/postgres-store.js +132 -0
  83. package/dist/plugins/bans/stores/postgres-store.js.map +1 -0
  84. package/dist/plugins/bans/types.d.ts +128 -0
  85. package/dist/plugins/bans/types.d.ts.map +1 -0
  86. package/dist/plugins/bans/types.js +11 -0
  87. package/dist/plugins/bans/types.js.map +1 -0
  88. package/dist/plugins/cache-plugin.d.ts +14 -3
  89. package/dist/plugins/cache-plugin.d.ts.map +1 -1
  90. package/dist/plugins/cache-plugin.js +27 -7
  91. package/dist/plugins/cache-plugin.js.map +1 -1
  92. package/dist/plugins/cache-plugin.test.js +99 -32
  93. package/dist/plugins/cache-plugin.test.js.map +1 -1
  94. package/dist/plugins/config-plugin.d.ts +3 -2
  95. package/dist/plugins/config-plugin.d.ts.map +1 -1
  96. package/dist/plugins/config-plugin.js +17 -10
  97. package/dist/plugins/config-plugin.js.map +1 -1
  98. package/dist/plugins/diagnostics-plugin.d.ts +2 -2
  99. package/dist/plugins/diagnostics-plugin.d.ts.map +1 -1
  100. package/dist/plugins/diagnostics-plugin.js +17 -10
  101. package/dist/plugins/diagnostics-plugin.js.map +1 -1
  102. package/dist/plugins/entitlements/entitlements-plugin.d.ts +95 -0
  103. package/dist/plugins/entitlements/entitlements-plugin.d.ts.map +1 -0
  104. package/dist/plugins/entitlements/entitlements-plugin.js +707 -0
  105. package/dist/plugins/entitlements/entitlements-plugin.js.map +1 -0
  106. package/dist/plugins/entitlements/index.d.ts +12 -0
  107. package/dist/plugins/entitlements/index.d.ts.map +1 -0
  108. package/dist/plugins/entitlements/index.js +16 -0
  109. package/dist/plugins/entitlements/index.js.map +1 -0
  110. package/dist/plugins/entitlements/sources/index.d.ts +9 -0
  111. package/dist/plugins/entitlements/sources/index.d.ts.map +1 -0
  112. package/dist/plugins/entitlements/sources/index.js +9 -0
  113. package/dist/plugins/entitlements/sources/index.js.map +1 -0
  114. package/dist/plugins/entitlements/sources/postgres-source.d.ts +29 -0
  115. package/dist/plugins/entitlements/sources/postgres-source.d.ts.map +1 -0
  116. package/dist/plugins/entitlements/sources/postgres-source.js +169 -0
  117. package/dist/plugins/entitlements/sources/postgres-source.js.map +1 -0
  118. package/dist/plugins/entitlements/types.d.ts +232 -0
  119. package/dist/plugins/entitlements/types.d.ts.map +1 -0
  120. package/dist/plugins/entitlements/types.js +11 -0
  121. package/dist/plugins/entitlements/types.js.map +1 -0
  122. package/dist/plugins/frontend-app-plugin.d.ts +9 -3
  123. package/dist/plugins/frontend-app-plugin.d.ts.map +1 -1
  124. package/dist/plugins/frontend-app-plugin.js +14 -9
  125. package/dist/plugins/frontend-app-plugin.js.map +1 -1
  126. package/dist/plugins/health-plugin.d.ts +5 -2
  127. package/dist/plugins/health-plugin.d.ts.map +1 -1
  128. package/dist/plugins/health-plugin.js +20 -5
  129. package/dist/plugins/health-plugin.js.map +1 -1
  130. package/dist/plugins/index.d.ts +10 -2
  131. package/dist/plugins/index.d.ts.map +1 -1
  132. package/dist/plugins/index.js +10 -2
  133. package/dist/plugins/index.js.map +1 -1
  134. package/dist/plugins/logs-plugin.d.ts +3 -2
  135. package/dist/plugins/logs-plugin.d.ts.map +1 -1
  136. package/dist/plugins/logs-plugin.js +21 -12
  137. package/dist/plugins/logs-plugin.js.map +1 -1
  138. package/dist/plugins/postgres-plugin.d.ts +3 -3
  139. package/dist/plugins/postgres-plugin.d.ts.map +1 -1
  140. package/dist/plugins/postgres-plugin.js +9 -7
  141. package/dist/plugins/postgres-plugin.js.map +1 -1
  142. package/dist/plugins/postgres-plugin.test.js +50 -29
  143. package/dist/plugins/postgres-plugin.test.js.map +1 -1
  144. package/dist/plugins/preferences/__tests__/deep-merge.test.d.ts +7 -0
  145. package/dist/plugins/preferences/__tests__/deep-merge.test.d.ts.map +1 -0
  146. package/dist/plugins/preferences/__tests__/deep-merge.test.js +215 -0
  147. package/dist/plugins/preferences/__tests__/deep-merge.test.js.map +1 -0
  148. package/dist/plugins/preferences/__tests__/preferences-plugin.test.d.ts +7 -0
  149. package/dist/plugins/preferences/__tests__/preferences-plugin.test.d.ts.map +1 -0
  150. package/dist/plugins/preferences/__tests__/preferences-plugin.test.js +265 -0
  151. package/dist/plugins/preferences/__tests__/preferences-plugin.test.js.map +1 -0
  152. package/dist/plugins/preferences/index.d.ts +12 -0
  153. package/dist/plugins/preferences/index.d.ts.map +1 -0
  154. package/dist/plugins/preferences/index.js +13 -0
  155. package/dist/plugins/preferences/index.js.map +1 -0
  156. package/dist/plugins/preferences/preferences-plugin.d.ts +39 -0
  157. package/dist/plugins/preferences/preferences-plugin.d.ts.map +1 -0
  158. package/dist/plugins/preferences/preferences-plugin.js +226 -0
  159. package/dist/plugins/preferences/preferences-plugin.js.map +1 -0
  160. package/dist/plugins/preferences/stores/index.d.ts +9 -0
  161. package/dist/plugins/preferences/stores/index.d.ts.map +1 -0
  162. package/dist/plugins/preferences/stores/index.js +9 -0
  163. package/dist/plugins/preferences/stores/index.js.map +1 -0
  164. package/dist/plugins/preferences/stores/postgres-store.d.ts +41 -0
  165. package/dist/plugins/preferences/stores/postgres-store.d.ts.map +1 -0
  166. package/dist/plugins/preferences/stores/postgres-store.js +181 -0
  167. package/dist/plugins/preferences/stores/postgres-store.js.map +1 -0
  168. package/dist/plugins/preferences/types.d.ts +91 -0
  169. package/dist/plugins/preferences/types.d.ts.map +1 -0
  170. package/dist/plugins/preferences/types.js +10 -0
  171. package/dist/plugins/preferences/types.js.map +1 -0
  172. package/dist/plugins/users/__tests__/users-plugin.test.d.ts +9 -0
  173. package/dist/plugins/users/__tests__/users-plugin.test.d.ts.map +1 -0
  174. package/dist/plugins/users/__tests__/users-plugin.test.js +546 -0
  175. package/dist/plugins/users/__tests__/users-plugin.test.js.map +1 -0
  176. package/dist/plugins/users/index.d.ts +12 -0
  177. package/dist/plugins/users/index.d.ts.map +1 -0
  178. package/dist/plugins/users/index.js +13 -0
  179. package/dist/plugins/users/index.js.map +1 -0
  180. package/dist/plugins/users/stores/index.d.ts +7 -0
  181. package/dist/plugins/users/stores/index.d.ts.map +1 -0
  182. package/dist/plugins/users/stores/index.js +7 -0
  183. package/dist/plugins/users/stores/index.js.map +1 -0
  184. package/dist/plugins/users/stores/postgres-store.d.ts +28 -0
  185. package/dist/plugins/users/stores/postgres-store.d.ts.map +1 -0
  186. package/dist/plugins/users/stores/postgres-store.js +157 -0
  187. package/dist/plugins/users/stores/postgres-store.js.map +1 -0
  188. package/dist/plugins/users/types.d.ts +225 -0
  189. package/dist/plugins/users/types.d.ts.map +1 -0
  190. package/dist/plugins/users/types.js +12 -0
  191. package/dist/plugins/users/types.js.map +1 -0
  192. package/dist/plugins/users/users-plugin.d.ts +45 -0
  193. package/dist/plugins/users/users-plugin.d.ts.map +1 -0
  194. package/dist/plugins/users/users-plugin.js +359 -0
  195. package/dist/plugins/users/users-plugin.js.map +1 -0
  196. package/dist-ui/assets/index-BY8OxNgO.js +465 -0
  197. package/dist-ui/assets/index-BY8OxNgO.js.map +1 -0
  198. package/dist-ui/index.html +1 -1
  199. package/dist-ui-lib/api/controlPanelApi.d.ts +278 -0
  200. package/dist-ui-lib/components/ControlPanelApp.d.ts +61 -0
  201. package/dist-ui-lib/components/index.d.ts +18 -0
  202. package/dist-ui-lib/config/AppConfig.d.ts +7 -0
  203. package/dist-ui-lib/dashboard/DashboardWidgetRegistry.d.ts +62 -0
  204. package/dist-ui-lib/dashboard/DashboardWidgetRenderer.d.ts +8 -0
  205. package/dist-ui-lib/dashboard/PluginWidgetRenderer.d.ts +19 -0
  206. package/dist-ui-lib/dashboard/WidgetComponentRegistry.d.ts +48 -0
  207. package/dist-ui-lib/dashboard/builtInWidgets.d.ts +25 -0
  208. package/dist-ui-lib/dashboard/index.d.ts +13 -0
  209. package/dist-ui-lib/dashboard/widgets/ServiceHealthWidget.d.ts +12 -0
  210. package/dist-ui-lib/dashboard/widgets/index.d.ts +6 -0
  211. package/dist-ui-lib/index.js +5172 -0
  212. package/dist-ui-lib/index.js.map +1 -0
  213. package/dist-ui-lib/pages/AuthPage.d.ts +1 -0
  214. package/dist-ui-lib/pages/ConfigPage.d.ts +1 -0
  215. package/dist-ui-lib/pages/DashboardPage.d.ts +1 -0
  216. package/dist-ui-lib/pages/DiagnosticsPage.d.ts +1 -0
  217. package/dist-ui-lib/pages/EntitlementsPage.d.ts +17 -0
  218. package/dist-ui-lib/pages/LogsPage.d.ts +1 -0
  219. package/dist-ui-lib/pages/NotFoundPage.d.ts +1 -0
  220. package/dist-ui-lib/pages/PluginPage.d.ts +15 -0
  221. package/dist-ui-lib/pages/PluginsPage.d.ts +1 -0
  222. package/dist-ui-lib/pages/SystemPage.d.ts +1 -0
  223. package/dist-ui-lib/pages/UsersPage.d.ts +22 -0
  224. package/package.json +24 -7
  225. package/src/core/control-panel.ts +145 -61
  226. package/src/core/gateway.ts +863 -403
  227. package/src/core/index.ts +21 -2
  228. package/src/core/plugin-registry.ts +716 -0
  229. package/src/core/types.ts +31 -37
  230. package/src/index.ts +125 -19
  231. package/src/plugins/auth/adapters/auth0-adapter.ts +214 -0
  232. package/src/plugins/auth/adapters/basic-adapter.ts +61 -0
  233. package/src/plugins/auth/adapters/index.ts +10 -0
  234. package/src/plugins/auth/adapters/supabase-adapter.ts +149 -0
  235. package/src/plugins/auth/adapters/supertokens-adapter.ts +326 -0
  236. package/src/plugins/auth/auth-plugin.test.ts +176 -0
  237. package/src/plugins/auth/auth-plugin.ts +303 -0
  238. package/src/plugins/auth/env-config.ts +572 -0
  239. package/src/plugins/auth/index.ts +42 -0
  240. package/src/plugins/auth/supertokens-adapter.test.ts +621 -0
  241. package/src/plugins/auth/types.ts +245 -0
  242. package/src/plugins/bans/bans-plugin.ts +485 -0
  243. package/src/plugins/bans/index.ts +31 -0
  244. package/src/plugins/bans/stores/index.ts +7 -0
  245. package/src/plugins/bans/stores/postgres-store.ts +195 -0
  246. package/src/plugins/bans/types.ts +141 -0
  247. package/src/plugins/cache-plugin.test.ts +108 -32
  248. package/src/plugins/cache-plugin.ts +40 -9
  249. package/src/plugins/config-plugin.ts +23 -12
  250. package/src/plugins/diagnostics-plugin.ts +22 -12
  251. package/src/plugins/entitlements/entitlements-plugin.ts +820 -0
  252. package/src/plugins/entitlements/index.ts +51 -0
  253. package/src/plugins/entitlements/sources/index.ts +9 -0
  254. package/src/plugins/entitlements/sources/postgres-source.ts +253 -0
  255. package/src/plugins/entitlements/types.ts +256 -0
  256. package/src/plugins/frontend-app-plugin.ts +24 -12
  257. package/src/plugins/health-plugin.ts +27 -7
  258. package/src/plugins/index.ts +132 -4
  259. package/src/plugins/logs-plugin.ts +28 -14
  260. package/src/plugins/postgres-plugin.test.ts +52 -29
  261. package/src/plugins/postgres-plugin.ts +11 -9
  262. package/src/plugins/preferences/__tests__/deep-merge.test.ts +242 -0
  263. package/src/plugins/preferences/__tests__/preferences-plugin.test.ts +350 -0
  264. package/src/plugins/preferences/index.ts +30 -0
  265. package/src/plugins/preferences/preferences-plugin.ts +270 -0
  266. package/src/plugins/preferences/stores/index.ts +9 -0
  267. package/src/plugins/preferences/stores/postgres-store.ts +252 -0
  268. package/src/plugins/preferences/types.ts +100 -0
  269. package/src/plugins/users/__tests__/users-plugin.test.ts +690 -0
  270. package/src/plugins/users/index.ts +38 -0
  271. package/src/plugins/users/stores/index.ts +7 -0
  272. package/src/plugins/users/stores/postgres-store.ts +225 -0
  273. package/src/plugins/users/types.ts +247 -0
  274. package/src/plugins/users/users-plugin.ts +418 -0
  275. package/ui/src/App.tsx +188 -31
  276. package/ui/src/api/controlPanelApi.ts +453 -1
  277. package/ui/src/components/ControlPanelApp.tsx +212 -0
  278. package/ui/src/components/index.ts +62 -0
  279. package/ui/src/dashboard/DashboardWidgetRegistry.tsx +129 -0
  280. package/ui/src/dashboard/DashboardWidgetRenderer.tsx +34 -0
  281. package/ui/src/dashboard/PluginWidgetRenderer.tsx +118 -0
  282. package/ui/src/dashboard/WidgetComponentRegistry.tsx +120 -0
  283. package/ui/src/dashboard/builtInWidgets.tsx +35 -0
  284. package/ui/src/dashboard/index.ts +35 -0
  285. package/ui/src/dashboard/widgets/ServiceHealthWidget.tsx +140 -0
  286. package/ui/src/dashboard/widgets/index.ts +7 -0
  287. package/ui/src/pages/AuthPage.tsx +259 -0
  288. package/ui/src/pages/DashboardPage.tsx +28 -149
  289. package/ui/src/pages/EntitlementsPage.tsx +557 -0
  290. package/ui/src/pages/LogsPage.tsx +174 -8
  291. package/ui/src/pages/PluginPage.tsx +148 -0
  292. package/ui/src/pages/PluginsPage.tsx +394 -0
  293. package/ui/src/pages/SystemPage.tsx +445 -0
  294. package/ui/src/pages/UsersPage.tsx +837 -0
  295. package/ui/tsconfig.lib.json +11 -0
  296. package/ui/vite.lib.config.ts +56 -0
  297. package/dist-ui/assets/index-CW1BviRn.js +0 -465
  298. package/dist-ui/assets/index-CW1BviRn.js.map +0 -1
  299. package/ui/src/pages/HealthPage.tsx +0 -204
@@ -0,0 +1,270 @@
1
+ /**
2
+ * Preferences Plugin
3
+ *
4
+ * User preferences management plugin for @qwickapps/server.
5
+ * Provides per-user preference storage with PostgreSQL RLS for data isolation.
6
+ *
7
+ * This plugin depends on the Users Plugin for user identity.
8
+ *
9
+ * Copyright (c) 2025 QwickApps.com. All rights reserved.
10
+ */
11
+
12
+ import type { Request, Response } from 'express';
13
+ import type { Plugin, PluginConfig, PluginRegistry } from '../../core/plugin-registry.js';
14
+ import type {
15
+ PreferencesPluginConfig,
16
+ PreferencesStore,
17
+ } from './types.js';
18
+ import type { AuthenticatedRequest } from '../auth/types.js';
19
+ import { deepMerge } from './stores/postgres-store.js';
20
+
21
+ // Configuration limits
22
+ const MAX_PREFERENCES_SIZE = 100_000; // 100KB JSON string limit
23
+ const MAX_NESTING_DEPTH = 10;
24
+
25
+ /**
26
+ * Check if an object exceeds maximum nesting depth
27
+ */
28
+ function exceedsMaxDepth(obj: unknown, depth = 0): boolean {
29
+ if (depth > MAX_NESTING_DEPTH) return true;
30
+ if (obj && typeof obj === 'object' && !Array.isArray(obj)) {
31
+ return Object.values(obj as Record<string, unknown>).some(v => exceedsMaxDepth(v, depth + 1));
32
+ }
33
+ if (Array.isArray(obj)) {
34
+ return obj.some(v => exceedsMaxDepth(v, depth + 1));
35
+ }
36
+ return false;
37
+ }
38
+
39
+ // Store instance for helper access
40
+ let currentStore: PreferencesStore | null = null;
41
+ let pluginDefaults: Record<string, unknown> = {};
42
+
43
+ /**
44
+ * Create the Preferences plugin
45
+ */
46
+ export function createPreferencesPlugin(config: PreferencesPluginConfig): Plugin {
47
+ const debug = config.debug || false;
48
+ // Routes are mounted under /api by the control panel, so don't include /api in prefix
49
+ const apiPrefix = config.api?.prefix || '/preferences';
50
+ const apiEnabled = config.api?.enabled !== false;
51
+
52
+ function log(message: string, data?: Record<string, unknown>) {
53
+ if (debug) {
54
+ console.log(`[PreferencesPlugin] ${message}`, data || '');
55
+ }
56
+ }
57
+
58
+ return {
59
+ id: 'preferences',
60
+ name: 'Preferences',
61
+ version: '1.0.0',
62
+
63
+ async onStart(_pluginConfig: PluginConfig, registry: PluginRegistry): Promise<void> {
64
+ log('Starting preferences plugin');
65
+
66
+ // Check for users plugin dependency
67
+ if (!registry.hasPlugin('users')) {
68
+ throw new Error('Preferences plugin requires Users plugin to be loaded first');
69
+ }
70
+
71
+ // Initialize the store (creates tables and RLS policies if needed)
72
+ await config.store.initialize();
73
+ log('Preferences plugin migrations complete');
74
+
75
+ // Store references for helper access
76
+ currentStore = config.store;
77
+ pluginDefaults = config.defaults || {};
78
+
79
+ // Register health check
80
+ registry.registerHealthCheck({
81
+ name: 'preferences-store',
82
+ type: 'custom',
83
+ check: async () => {
84
+ try {
85
+ // Simple health check - store is accessible
86
+ // We can't actually query without a user context due to RLS,
87
+ // but we can verify the store is initialized
88
+ return { healthy: currentStore !== null };
89
+ } catch {
90
+ return { healthy: false };
91
+ }
92
+ },
93
+ });
94
+
95
+ // Add API routes if enabled
96
+ if (apiEnabled) {
97
+ // GET /preferences - Get current user's preferences
98
+ registry.addRoute({
99
+ method: 'get',
100
+ path: apiPrefix,
101
+ pluginId: 'preferences',
102
+ handler: async (req: Request, res: Response) => {
103
+ try {
104
+ const authReq = req as AuthenticatedRequest;
105
+ const userId = authReq.auth?.user?.id;
106
+
107
+ if (!userId) {
108
+ return res.status(401).json({ error: 'Authentication required' });
109
+ }
110
+
111
+ const stored = await config.store.get(userId);
112
+
113
+ // Merge with defaults (defaults as base, stored values override)
114
+ const preferences = stored
115
+ ? deepMerge(pluginDefaults, stored)
116
+ : { ...pluginDefaults };
117
+
118
+ res.json({
119
+ user_id: userId,
120
+ preferences,
121
+ });
122
+ } catch (error) {
123
+ console.error('[PreferencesPlugin] Get preferences error:', error);
124
+ res.status(500).json({ error: 'Failed to get preferences' });
125
+ }
126
+ },
127
+ });
128
+
129
+ // PUT /preferences - Update current user's preferences
130
+ registry.addRoute({
131
+ method: 'put',
132
+ path: apiPrefix,
133
+ pluginId: 'preferences',
134
+ handler: async (req: Request, res: Response) => {
135
+ try {
136
+ const authReq = req as AuthenticatedRequest;
137
+ const userId = authReq.auth?.user?.id;
138
+
139
+ if (!userId) {
140
+ return res.status(401).json({ error: 'Authentication required' });
141
+ }
142
+
143
+ const newPreferences = req.body;
144
+ if (!newPreferences || typeof newPreferences !== 'object' || Array.isArray(newPreferences)) {
145
+ return res.status(400).json({ error: 'Request body must be a JSON object' });
146
+ }
147
+
148
+ // Validate payload size
149
+ const jsonSize = JSON.stringify(newPreferences).length;
150
+ if (jsonSize > MAX_PREFERENCES_SIZE) {
151
+ return res.status(413).json({ error: 'Preferences payload too large (max 100KB)' });
152
+ }
153
+
154
+ // Validate nesting depth
155
+ if (exceedsMaxDepth(newPreferences)) {
156
+ return res.status(400).json({ error: 'Preferences object too deeply nested (max 10 levels)' });
157
+ }
158
+
159
+ const updated = await config.store.update(userId, newPreferences);
160
+
161
+ // Merge with defaults for response
162
+ const preferences = deepMerge(pluginDefaults, updated);
163
+
164
+ res.json({
165
+ user_id: userId,
166
+ preferences,
167
+ });
168
+ } catch (error) {
169
+ console.error('[PreferencesPlugin] Update preferences error:', error);
170
+ res.status(500).json({ error: 'Failed to update preferences' });
171
+ }
172
+ },
173
+ });
174
+
175
+ // DELETE /preferences - Reset preferences to defaults
176
+ registry.addRoute({
177
+ method: 'delete',
178
+ path: apiPrefix,
179
+ pluginId: 'preferences',
180
+ handler: async (req: Request, res: Response) => {
181
+ try {
182
+ const authReq = req as AuthenticatedRequest;
183
+ const userId = authReq.auth?.user?.id;
184
+
185
+ if (!userId) {
186
+ return res.status(401).json({ error: 'Authentication required' });
187
+ }
188
+
189
+ await config.store.delete(userId);
190
+
191
+ // Return 204 No Content (idempotent - success even if no row existed)
192
+ res.status(204).send();
193
+ } catch (error) {
194
+ console.error('[PreferencesPlugin] Delete preferences error:', error);
195
+ res.status(500).json({ error: 'Failed to delete preferences' });
196
+ }
197
+ },
198
+ });
199
+ }
200
+
201
+ log('Preferences plugin started');
202
+ },
203
+
204
+ async onStop(): Promise<void> {
205
+ log('Stopping preferences plugin');
206
+ await config.store.shutdown();
207
+ currentStore = null;
208
+ pluginDefaults = {};
209
+ log('Preferences plugin stopped');
210
+ },
211
+ };
212
+ }
213
+
214
+ // ========================================
215
+ // Helper Functions
216
+ // ========================================
217
+
218
+ /**
219
+ * Get the current preferences store instance
220
+ */
221
+ export function getPreferencesStore(): PreferencesStore | null {
222
+ return currentStore;
223
+ }
224
+
225
+ /**
226
+ * Get preferences for a user (merged with defaults)
227
+ */
228
+ export async function getPreferences(userId: string): Promise<Record<string, unknown>> {
229
+ if (!currentStore) {
230
+ throw new Error('Preferences plugin not initialized');
231
+ }
232
+
233
+ const stored = await currentStore.get(userId);
234
+ return stored ? deepMerge(pluginDefaults, stored) : { ...pluginDefaults };
235
+ }
236
+
237
+ /**
238
+ * Update preferences for a user
239
+ * Returns the merged preferences (stored + defaults)
240
+ */
241
+ export async function updatePreferences(
242
+ userId: string,
243
+ preferences: Record<string, unknown>
244
+ ): Promise<Record<string, unknown>> {
245
+ if (!currentStore) {
246
+ throw new Error('Preferences plugin not initialized');
247
+ }
248
+
249
+ const updated = await currentStore.update(userId, preferences);
250
+ return deepMerge(pluginDefaults, updated);
251
+ }
252
+
253
+ /**
254
+ * Delete preferences for a user (reset to defaults)
255
+ * Returns true if preferences existed and were deleted
256
+ */
257
+ export async function deletePreferences(userId: string): Promise<boolean> {
258
+ if (!currentStore) {
259
+ throw new Error('Preferences plugin not initialized');
260
+ }
261
+
262
+ return currentStore.delete(userId);
263
+ }
264
+
265
+ /**
266
+ * Get the configured default preferences
267
+ */
268
+ export function getDefaultPreferences(): Record<string, unknown> {
269
+ return { ...pluginDefaults };
270
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Preferences Stores Index
3
+ *
4
+ * Re-exports all available preferences store implementations.
5
+ *
6
+ * Copyright (c) 2025 QwickApps.com. All rights reserved.
7
+ */
8
+
9
+ export { postgresPreferencesStore, deepMerge } from './postgres-store.js';
@@ -0,0 +1,252 @@
1
+ /**
2
+ * PostgreSQL Preferences Store
3
+ *
4
+ * Preferences storage implementation using PostgreSQL with Row-Level Security (RLS).
5
+ * Requires the 'pg' package and the Users plugin to be installed.
6
+ *
7
+ * RLS Context Pattern:
8
+ * Each operation uses an explicit transaction and sets `app.current_user_id`
9
+ * as a transaction-local configuration variable. The RLS policy checks this
10
+ * variable to enforce that users can only access their own preferences.
11
+ *
12
+ * Copyright (c) 2025 QwickApps.com. All rights reserved.
13
+ */
14
+
15
+ import type {
16
+ PreferencesStore,
17
+ PostgresPreferencesStoreConfig,
18
+ } from '../types.js';
19
+
20
+ // Pool interface (from pg package)
21
+ interface PgPool {
22
+ query(text: string, values?: unknown[]): Promise<{ rows: unknown[]; rowCount: number | null }>;
23
+ connect(): Promise<PgPoolClient>;
24
+ }
25
+
26
+ interface PgPoolClient {
27
+ query(text: string, values?: unknown[]): Promise<{ rows: unknown[]; rowCount: number | null }>;
28
+ release(): void;
29
+ }
30
+
31
+ /**
32
+ * Deep merge two objects
33
+ * - Objects are recursively merged
34
+ * - Arrays are replaced (not merged)
35
+ * - Source values override target values
36
+ */
37
+ export function deepMerge(
38
+ target: Record<string, unknown>,
39
+ source: Record<string, unknown>
40
+ ): Record<string, unknown> {
41
+ const output = { ...target };
42
+
43
+ for (const key of Object.keys(source)) {
44
+ const sourceValue = source[key];
45
+ const targetValue = target[key];
46
+
47
+ // Skip undefined values in source
48
+ if (sourceValue === undefined) {
49
+ continue;
50
+ }
51
+
52
+ // If both are plain objects, merge recursively
53
+ if (
54
+ sourceValue !== null &&
55
+ typeof sourceValue === 'object' &&
56
+ !Array.isArray(sourceValue) &&
57
+ targetValue !== null &&
58
+ typeof targetValue === 'object' &&
59
+ !Array.isArray(targetValue)
60
+ ) {
61
+ output[key] = deepMerge(
62
+ targetValue as Record<string, unknown>,
63
+ sourceValue as Record<string, unknown>
64
+ );
65
+ } else {
66
+ // Otherwise, source overwrites target
67
+ output[key] = sourceValue;
68
+ }
69
+ }
70
+
71
+ return output;
72
+ }
73
+
74
+ /**
75
+ * Execute a function within an RLS-protected transaction
76
+ *
77
+ * This helper ensures that:
78
+ * 1. All queries run within the same transaction
79
+ * 2. The RLS context is set before any data access
80
+ * 3. The transaction is properly committed or rolled back
81
+ *
82
+ * @param pool PostgreSQL pool
83
+ * @param userId User ID to set as the RLS context
84
+ * @param callback Function to execute within the transaction
85
+ */
86
+ async function withRLSContext<T>(
87
+ pool: PgPool,
88
+ userId: string,
89
+ callback: (client: PgPoolClient) => Promise<T>
90
+ ): Promise<T> {
91
+ const client = await pool.connect();
92
+ try {
93
+ await client.query('BEGIN');
94
+ // Set transaction-local user context for RLS
95
+ await client.query(
96
+ "SELECT set_config('app.current_user_id', $1, true)",
97
+ [userId]
98
+ );
99
+ const result = await callback(client);
100
+ await client.query('COMMIT');
101
+ return result;
102
+ } catch (error) {
103
+ await client.query('ROLLBACK');
104
+ throw error;
105
+ } finally {
106
+ client.release();
107
+ }
108
+ }
109
+
110
+ /**
111
+ * Create a PostgreSQL preferences store with RLS
112
+ *
113
+ * @param config Configuration including a pg Pool instance
114
+ * @returns PreferencesStore implementation
115
+ *
116
+ * @example
117
+ * ```ts
118
+ * import { Pool } from 'pg';
119
+ * import { postgresPreferencesStore } from '@qwickapps/server';
120
+ *
121
+ * const pool = new Pool({ connectionString: process.env.DATABASE_URL });
122
+ * const store = postgresPreferencesStore({ pool });
123
+ *
124
+ * // Or with lazy initialization:
125
+ * const store = postgresPreferencesStore({ pool: () => getPostgres().getPool() });
126
+ * ```
127
+ */
128
+ export function postgresPreferencesStore(config: PostgresPreferencesStoreConfig): PreferencesStore {
129
+ const {
130
+ pool: poolOrFn,
131
+ tableName = 'user_preferences',
132
+ schema = 'public',
133
+ autoCreateTables = true,
134
+ enableRLS = true,
135
+ } = config;
136
+
137
+ // Helper to get pool (supports lazy initialization via function)
138
+ const getPool = (): PgPool => {
139
+ const pool = typeof poolOrFn === 'function' ? poolOrFn() : poolOrFn;
140
+ if (!pool || typeof (pool as PgPool).query !== 'function') {
141
+ throw new Error('Invalid pool: must have query method');
142
+ }
143
+ return pool as PgPool;
144
+ };
145
+
146
+ const tableFullName = `"${schema}"."${tableName}"`;
147
+
148
+ return {
149
+ name: 'postgres',
150
+
151
+ async initialize(): Promise<void> {
152
+ if (!autoCreateTables) return;
153
+
154
+ const pool = getPool();
155
+
156
+ // Create table with foreign key to users
157
+ await pool.query(`
158
+ CREATE TABLE IF NOT EXISTS ${tableFullName} (
159
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
160
+ user_id UUID NOT NULL REFERENCES "public"."users"(id) ON DELETE CASCADE,
161
+ preferences JSONB NOT NULL DEFAULT '{}',
162
+ created_at TIMESTAMPTZ DEFAULT NOW(),
163
+ updated_at TIMESTAMPTZ DEFAULT NOW(),
164
+ UNIQUE(user_id)
165
+ );
166
+
167
+ CREATE INDEX IF NOT EXISTS idx_${tableName}_user_id ON ${tableFullName}(user_id);
168
+ `);
169
+
170
+ // Enable RLS if configured
171
+ if (enableRLS) {
172
+ await pool.query(`
173
+ ALTER TABLE ${tableFullName} ENABLE ROW LEVEL SECURITY;
174
+ ALTER TABLE ${tableFullName} FORCE ROW LEVEL SECURITY;
175
+ `);
176
+
177
+ // Create or replace the RLS policy
178
+ // Drop existing policy first to avoid errors on re-initialization
179
+ await pool.query(`
180
+ DROP POLICY IF EXISTS "${tableName}_owner" ON ${tableFullName};
181
+ `);
182
+
183
+ // RLS policy with both USING (for SELECT/UPDATE/DELETE reads)
184
+ // and WITH CHECK (for INSERT/UPDATE writes) clauses
185
+ await pool.query(`
186
+ CREATE POLICY "${tableName}_owner" ON ${tableFullName}
187
+ FOR ALL
188
+ USING (user_id::text = current_setting('app.current_user_id', true))
189
+ WITH CHECK (user_id::text = current_setting('app.current_user_id', true));
190
+ `);
191
+ }
192
+ },
193
+
194
+ async get(userId: string): Promise<Record<string, unknown> | null> {
195
+ return withRLSContext(getPool(), userId, async (client) => {
196
+ const result = await client.query(
197
+ `SELECT preferences FROM ${tableFullName} WHERE user_id = $1`,
198
+ [userId]
199
+ );
200
+
201
+ if (result.rows.length === 0) {
202
+ return null;
203
+ }
204
+
205
+ return (result.rows[0] as { preferences: Record<string, unknown> }).preferences;
206
+ });
207
+ },
208
+
209
+ async update(userId: string, preferences: Record<string, unknown>): Promise<Record<string, unknown>> {
210
+ return withRLSContext(getPool(), userId, async (client) => {
211
+ // Get existing preferences within the same transaction
212
+ const existingResult = await client.query(
213
+ `SELECT preferences FROM ${tableFullName} WHERE user_id = $1`,
214
+ [userId]
215
+ );
216
+
217
+ const existing = existingResult.rows.length > 0
218
+ ? (existingResult.rows[0] as { preferences: Record<string, unknown> }).preferences
219
+ : null;
220
+
221
+ const merged = existing ? deepMerge(existing, preferences) : preferences;
222
+
223
+ // Upsert the merged preferences
224
+ await client.query(
225
+ `INSERT INTO ${tableFullName} (user_id, preferences, updated_at)
226
+ VALUES ($1, $2, NOW())
227
+ ON CONFLICT (user_id) DO UPDATE SET
228
+ preferences = $2,
229
+ updated_at = NOW()`,
230
+ [userId, JSON.stringify(merged)]
231
+ );
232
+
233
+ return merged;
234
+ });
235
+ },
236
+
237
+ async delete(userId: string): Promise<boolean> {
238
+ return withRLSContext(getPool(), userId, async (client) => {
239
+ const result = await client.query(
240
+ `DELETE FROM ${tableFullName} WHERE user_id = $1`,
241
+ [userId]
242
+ );
243
+
244
+ return (result.rowCount ?? 0) > 0;
245
+ });
246
+ },
247
+
248
+ async shutdown(): Promise<void> {
249
+ // Pool is managed externally, nothing to do here
250
+ },
251
+ };
252
+ }
@@ -0,0 +1,100 @@
1
+ /**
2
+ * Preferences Plugin Types
3
+ *
4
+ * Type definitions for user preferences management.
5
+ * Supports PostgreSQL with Row-Level Security (RLS) for data isolation.
6
+ *
7
+ * Copyright (c) 2025 QwickApps.com. All rights reserved.
8
+ */
9
+
10
+ /**
11
+ * User preferences record in the database
12
+ */
13
+ export interface UserPreferences {
14
+ /** Primary key - UUID */
15
+ id: string;
16
+ /** User ID (foreign key to users table) */
17
+ user_id: string;
18
+ /** Preferences as JSON object */
19
+ preferences: Record<string, unknown>;
20
+ /** When the preferences were created */
21
+ created_at: Date;
22
+ /** When the preferences were last updated */
23
+ updated_at: Date;
24
+ }
25
+
26
+ /**
27
+ * Preferences store interface - all storage backends must implement this
28
+ */
29
+ export interface PreferencesStore {
30
+ /** Store name (e.g., 'postgres', 'memory') */
31
+ name: string;
32
+
33
+ /**
34
+ * Initialize the store (create tables, RLS policies, etc.)
35
+ */
36
+ initialize(): Promise<void>;
37
+
38
+ /**
39
+ * Get preferences for a user
40
+ * Returns null if no preferences exist for the user
41
+ */
42
+ get(userId: string): Promise<Record<string, unknown> | null>;
43
+
44
+ /**
45
+ * Update preferences for a user (upsert with deep merge)
46
+ * Returns the merged preferences
47
+ */
48
+ update(userId: string, preferences: Record<string, unknown>): Promise<Record<string, unknown>>;
49
+
50
+ /**
51
+ * Delete preferences for a user
52
+ * Returns true if preferences were deleted, false if none existed
53
+ */
54
+ delete(userId: string): Promise<boolean>;
55
+
56
+ /**
57
+ * Shutdown the store
58
+ */
59
+ shutdown(): Promise<void>;
60
+ }
61
+
62
+ /**
63
+ * PostgreSQL preferences store configuration
64
+ */
65
+ export interface PostgresPreferencesStoreConfig {
66
+ /** PostgreSQL pool instance or a function that returns one (for lazy initialization) */
67
+ pool: unknown | (() => unknown);
68
+ /** Table name (default: 'user_preferences') */
69
+ tableName?: string;
70
+ /** Schema name (default: 'public') */
71
+ schema?: string;
72
+ /** Auto-create tables on init (default: true) */
73
+ autoCreateTables?: boolean;
74
+ /** Enable RLS (default: true) */
75
+ enableRLS?: boolean;
76
+ }
77
+
78
+ /**
79
+ * Preferences API configuration
80
+ */
81
+ export interface PreferencesApiConfig {
82
+ /** API route prefix (default: '/preferences') */
83
+ prefix?: string;
84
+ /** Enable API endpoints (default: true) */
85
+ enabled?: boolean;
86
+ }
87
+
88
+ /**
89
+ * Preferences plugin configuration
90
+ */
91
+ export interface PreferencesPluginConfig {
92
+ /** Preferences storage backend */
93
+ store: PreferencesStore;
94
+ /** Default preferences to merge with stored preferences on read */
95
+ defaults?: Record<string, unknown>;
96
+ /** API configuration */
97
+ api?: PreferencesApiConfig;
98
+ /** Enable debug logging */
99
+ debug?: boolean;
100
+ }