@qwickapps/server 1.3.0 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (241) hide show
  1. package/README.md +311 -0
  2. package/dist/core/control-panel.d.ts.map +1 -1
  3. package/dist/core/control-panel.js +144 -2
  4. package/dist/core/control-panel.js.map +1 -1
  5. package/dist/core/plugin-registry.d.ts +36 -0
  6. package/dist/core/plugin-registry.d.ts.map +1 -1
  7. package/dist/core/plugin-registry.js +26 -0
  8. package/dist/core/plugin-registry.js.map +1 -1
  9. package/dist/core/types.d.ts +19 -0
  10. package/dist/core/types.d.ts.map +1 -1
  11. package/dist/index.d.ts +2 -2
  12. package/dist/index.d.ts.map +1 -1
  13. package/dist/index.js +4 -2
  14. package/dist/index.js.map +1 -1
  15. package/dist/plugins/auth/adapter-wrapper.d.ts +47 -0
  16. package/dist/plugins/auth/adapter-wrapper.d.ts.map +1 -0
  17. package/dist/plugins/auth/adapter-wrapper.js +166 -0
  18. package/dist/plugins/auth/adapter-wrapper.js.map +1 -0
  19. package/dist/plugins/auth/adapter-wrapper.test.d.ts +7 -0
  20. package/dist/plugins/auth/adapter-wrapper.test.d.ts.map +1 -0
  21. package/dist/plugins/auth/adapter-wrapper.test.js +303 -0
  22. package/dist/plugins/auth/adapter-wrapper.test.js.map +1 -0
  23. package/dist/plugins/auth/adapters/index.d.ts +1 -0
  24. package/dist/plugins/auth/adapters/index.d.ts.map +1 -1
  25. package/dist/plugins/auth/adapters/index.js +1 -0
  26. package/dist/plugins/auth/adapters/index.js.map +1 -1
  27. package/dist/plugins/auth/adapters/supabase-adapter.d.ts.map +1 -1
  28. package/dist/plugins/auth/adapters/supabase-adapter.js.map +1 -1
  29. package/dist/plugins/auth/adapters/supertokens-adapter.d.ts +18 -0
  30. package/dist/plugins/auth/adapters/supertokens-adapter.d.ts.map +1 -0
  31. package/dist/plugins/auth/adapters/supertokens-adapter.js +267 -0
  32. package/dist/plugins/auth/adapters/supertokens-adapter.js.map +1 -0
  33. package/dist/plugins/auth/config-store.d.ts +11 -0
  34. package/dist/plugins/auth/config-store.d.ts.map +1 -0
  35. package/dist/plugins/auth/config-store.js +232 -0
  36. package/dist/plugins/auth/config-store.js.map +1 -0
  37. package/dist/plugins/auth/config-store.test.d.ts +7 -0
  38. package/dist/plugins/auth/config-store.test.d.ts.map +1 -0
  39. package/dist/plugins/auth/config-store.test.js +299 -0
  40. package/dist/plugins/auth/config-store.test.js.map +1 -0
  41. package/dist/plugins/auth/env-config.d.ts +138 -0
  42. package/dist/plugins/auth/env-config.d.ts.map +1 -0
  43. package/dist/plugins/auth/env-config.js +1122 -0
  44. package/dist/plugins/auth/env-config.js.map +1 -0
  45. package/dist/plugins/auth/index.d.ts +7 -1
  46. package/dist/plugins/auth/index.d.ts.map +1 -1
  47. package/dist/plugins/auth/index.js +7 -0
  48. package/dist/plugins/auth/index.js.map +1 -1
  49. package/dist/plugins/auth/supertokens-adapter.test.d.ts +10 -0
  50. package/dist/plugins/auth/supertokens-adapter.test.d.ts.map +1 -0
  51. package/dist/plugins/auth/supertokens-adapter.test.js +486 -0
  52. package/dist/plugins/auth/supertokens-adapter.test.js.map +1 -0
  53. package/dist/plugins/auth/types.d.ts +176 -0
  54. package/dist/plugins/auth/types.d.ts.map +1 -1
  55. package/dist/plugins/auth/types.js.map +1 -1
  56. package/dist/plugins/cache-plugin.test.js +3 -0
  57. package/dist/plugins/cache-plugin.test.js.map +1 -1
  58. package/dist/plugins/index.d.ts +6 -2
  59. package/dist/plugins/index.d.ts.map +1 -1
  60. package/dist/plugins/index.js +5 -1
  61. package/dist/plugins/index.js.map +1 -1
  62. package/dist/plugins/postgres-plugin.test.js +3 -0
  63. package/dist/plugins/postgres-plugin.test.js.map +1 -1
  64. package/dist/plugins/preferences/__tests__/deep-merge.test.d.ts +7 -0
  65. package/dist/plugins/preferences/__tests__/deep-merge.test.d.ts.map +1 -0
  66. package/dist/plugins/preferences/__tests__/deep-merge.test.js +215 -0
  67. package/dist/plugins/preferences/__tests__/deep-merge.test.js.map +1 -0
  68. package/dist/plugins/preferences/__tests__/preferences-plugin.test.d.ts +7 -0
  69. package/dist/plugins/preferences/__tests__/preferences-plugin.test.d.ts.map +1 -0
  70. package/dist/plugins/preferences/__tests__/preferences-plugin.test.js +265 -0
  71. package/dist/plugins/preferences/__tests__/preferences-plugin.test.js.map +1 -0
  72. package/dist/plugins/preferences/index.d.ts +12 -0
  73. package/dist/plugins/preferences/index.d.ts.map +1 -0
  74. package/dist/plugins/preferences/index.js +13 -0
  75. package/dist/plugins/preferences/index.js.map +1 -0
  76. package/dist/plugins/preferences/preferences-plugin.d.ts +39 -0
  77. package/dist/plugins/preferences/preferences-plugin.d.ts.map +1 -0
  78. package/dist/plugins/preferences/preferences-plugin.js +226 -0
  79. package/dist/plugins/preferences/preferences-plugin.js.map +1 -0
  80. package/dist/plugins/preferences/stores/index.d.ts +9 -0
  81. package/dist/plugins/preferences/stores/index.d.ts.map +1 -0
  82. package/dist/plugins/preferences/stores/index.js +9 -0
  83. package/dist/plugins/preferences/stores/index.js.map +1 -0
  84. package/dist/plugins/preferences/stores/postgres-store.d.ts +41 -0
  85. package/dist/plugins/preferences/stores/postgres-store.d.ts.map +1 -0
  86. package/dist/plugins/preferences/stores/postgres-store.js +181 -0
  87. package/dist/plugins/preferences/stores/postgres-store.js.map +1 -0
  88. package/dist/plugins/preferences/types.d.ts +91 -0
  89. package/dist/plugins/preferences/types.d.ts.map +1 -0
  90. package/dist/plugins/preferences/types.js +10 -0
  91. package/dist/plugins/preferences/types.js.map +1 -0
  92. package/dist/plugins/rate-limit/__tests__/rate-limit-plugin.test.d.ts +7 -0
  93. package/dist/plugins/rate-limit/__tests__/rate-limit-plugin.test.d.ts.map +1 -0
  94. package/dist/plugins/rate-limit/__tests__/rate-limit-plugin.test.js +220 -0
  95. package/dist/plugins/rate-limit/__tests__/rate-limit-plugin.test.js.map +1 -0
  96. package/dist/plugins/rate-limit/cleanup.d.ts +40 -0
  97. package/dist/plugins/rate-limit/cleanup.d.ts.map +1 -0
  98. package/dist/plugins/rate-limit/cleanup.js +72 -0
  99. package/dist/plugins/rate-limit/cleanup.js.map +1 -0
  100. package/dist/plugins/rate-limit/env-config.d.ts +91 -0
  101. package/dist/plugins/rate-limit/env-config.d.ts.map +1 -0
  102. package/dist/plugins/rate-limit/env-config.js +318 -0
  103. package/dist/plugins/rate-limit/env-config.js.map +1 -0
  104. package/dist/plugins/rate-limit/index.d.ts +76 -0
  105. package/dist/plugins/rate-limit/index.d.ts.map +1 -0
  106. package/dist/plugins/rate-limit/index.js +79 -0
  107. package/dist/plugins/rate-limit/index.js.map +1 -0
  108. package/dist/plugins/rate-limit/middleware.d.ts +40 -0
  109. package/dist/plugins/rate-limit/middleware.d.ts.map +1 -0
  110. package/dist/plugins/rate-limit/middleware.js +169 -0
  111. package/dist/plugins/rate-limit/middleware.js.map +1 -0
  112. package/dist/plugins/rate-limit/rate-limit-plugin.d.ts +44 -0
  113. package/dist/plugins/rate-limit/rate-limit-plugin.d.ts.map +1 -0
  114. package/dist/plugins/rate-limit/rate-limit-plugin.js +354 -0
  115. package/dist/plugins/rate-limit/rate-limit-plugin.js.map +1 -0
  116. package/dist/plugins/rate-limit/rate-limit-service.d.ts +110 -0
  117. package/dist/plugins/rate-limit/rate-limit-service.d.ts.map +1 -0
  118. package/dist/plugins/rate-limit/rate-limit-service.js +172 -0
  119. package/dist/plugins/rate-limit/rate-limit-service.js.map +1 -0
  120. package/dist/plugins/rate-limit/stores/cache-store.d.ts +33 -0
  121. package/dist/plugins/rate-limit/stores/cache-store.d.ts.map +1 -0
  122. package/dist/plugins/rate-limit/stores/cache-store.js +225 -0
  123. package/dist/plugins/rate-limit/stores/cache-store.js.map +1 -0
  124. package/dist/plugins/rate-limit/stores/index.d.ts +8 -0
  125. package/dist/plugins/rate-limit/stores/index.d.ts.map +1 -0
  126. package/dist/plugins/rate-limit/stores/index.js +8 -0
  127. package/dist/plugins/rate-limit/stores/index.js.map +1 -0
  128. package/dist/plugins/rate-limit/stores/postgres-store.d.ts +34 -0
  129. package/dist/plugins/rate-limit/stores/postgres-store.d.ts.map +1 -0
  130. package/dist/plugins/rate-limit/stores/postgres-store.js +320 -0
  131. package/dist/plugins/rate-limit/stores/postgres-store.js.map +1 -0
  132. package/dist/plugins/rate-limit/strategies/fixed-window.d.ts +21 -0
  133. package/dist/plugins/rate-limit/strategies/fixed-window.d.ts.map +1 -0
  134. package/dist/plugins/rate-limit/strategies/fixed-window.js +97 -0
  135. package/dist/plugins/rate-limit/strategies/fixed-window.js.map +1 -0
  136. package/dist/plugins/rate-limit/strategies/index.d.ts +14 -0
  137. package/dist/plugins/rate-limit/strategies/index.d.ts.map +1 -0
  138. package/dist/plugins/rate-limit/strategies/index.js +27 -0
  139. package/dist/plugins/rate-limit/strategies/index.js.map +1 -0
  140. package/dist/plugins/rate-limit/strategies/sliding-window.d.ts +22 -0
  141. package/dist/plugins/rate-limit/strategies/sliding-window.d.ts.map +1 -0
  142. package/dist/plugins/rate-limit/strategies/sliding-window.js +122 -0
  143. package/dist/plugins/rate-limit/strategies/sliding-window.js.map +1 -0
  144. package/dist/plugins/rate-limit/strategies/token-bucket.d.ts +28 -0
  145. package/dist/plugins/rate-limit/strategies/token-bucket.d.ts.map +1 -0
  146. package/dist/plugins/rate-limit/strategies/token-bucket.js +121 -0
  147. package/dist/plugins/rate-limit/strategies/token-bucket.js.map +1 -0
  148. package/dist/plugins/rate-limit/types.d.ts +265 -0
  149. package/dist/plugins/rate-limit/types.d.ts.map +1 -0
  150. package/dist/plugins/rate-limit/types.js +9 -0
  151. package/dist/plugins/rate-limit/types.js.map +1 -0
  152. package/dist/plugins/users/__tests__/users-plugin.test.d.ts +9 -0
  153. package/dist/plugins/users/__tests__/users-plugin.test.d.ts.map +1 -0
  154. package/dist/plugins/users/__tests__/users-plugin.test.js +546 -0
  155. package/dist/plugins/users/__tests__/users-plugin.test.js.map +1 -0
  156. package/dist/plugins/users/index.d.ts +2 -2
  157. package/dist/plugins/users/index.d.ts.map +1 -1
  158. package/dist/plugins/users/index.js +1 -1
  159. package/dist/plugins/users/index.js.map +1 -1
  160. package/dist/plugins/users/types.d.ts +36 -0
  161. package/dist/plugins/users/types.d.ts.map +1 -1
  162. package/dist/plugins/users/users-plugin.d.ts +8 -2
  163. package/dist/plugins/users/users-plugin.d.ts.map +1 -1
  164. package/dist/plugins/users/users-plugin.js +122 -5
  165. package/dist/plugins/users/users-plugin.js.map +1 -1
  166. package/dist-ui/assets/index-D7DoZ9rL.js +478 -0
  167. package/dist-ui/assets/index-D7DoZ9rL.js.map +1 -0
  168. package/dist-ui/index.html +1 -1
  169. package/dist-ui-lib/api/controlPanelApi.d.ts +194 -7
  170. package/dist-ui-lib/dashboard/WidgetComponentRegistry.d.ts +9 -5
  171. package/dist-ui-lib/dashboard/builtInWidgets.d.ts +7 -1
  172. package/dist-ui-lib/dashboard/widgets/AuthStatusWidget.d.ts +9 -0
  173. package/dist-ui-lib/dashboard/widgets/IntegrationStatusWidget.d.ts +9 -0
  174. package/dist-ui-lib/dashboard/widgets/index.d.ts +2 -0
  175. package/dist-ui-lib/index.js +3665 -3945
  176. package/dist-ui-lib/index.js.map +1 -1
  177. package/dist-ui-lib/pages/AuthPage.d.ts +1 -0
  178. package/dist-ui-lib/pages/IntegrationsPage.d.ts +1 -0
  179. package/dist-ui-lib/pages/PluginsPage.d.ts +1 -0
  180. package/dist-ui-lib/pages/RateLimitPage.d.ts +1 -0
  181. package/package.json +7 -2
  182. package/src/core/control-panel.ts +161 -2
  183. package/src/core/plugin-registry.ts +63 -0
  184. package/src/core/types.ts +17 -0
  185. package/src/index.ts +45 -0
  186. package/src/plugins/auth/adapter-wrapper.test.ts +395 -0
  187. package/src/plugins/auth/adapter-wrapper.ts +205 -0
  188. package/src/plugins/auth/adapters/index.ts +1 -0
  189. package/src/plugins/auth/adapters/supabase-adapter.ts +22 -14
  190. package/src/plugins/auth/adapters/supertokens-adapter.ts +326 -0
  191. package/src/plugins/auth/config-store.test.ts +417 -0
  192. package/src/plugins/auth/config-store.ts +305 -0
  193. package/src/plugins/auth/env-config.ts +1279 -0
  194. package/src/plugins/auth/index.ts +30 -0
  195. package/src/plugins/auth/supertokens-adapter.test.ts +621 -0
  196. package/src/plugins/auth/types.ts +218 -0
  197. package/src/plugins/cache-plugin.test.ts +3 -0
  198. package/src/plugins/index.ts +75 -0
  199. package/src/plugins/postgres-plugin.test.ts +3 -0
  200. package/src/plugins/preferences/__tests__/deep-merge.test.ts +242 -0
  201. package/src/plugins/preferences/__tests__/preferences-plugin.test.ts +350 -0
  202. package/src/plugins/preferences/index.ts +30 -0
  203. package/src/plugins/preferences/preferences-plugin.ts +270 -0
  204. package/src/plugins/preferences/stores/index.ts +9 -0
  205. package/src/plugins/preferences/stores/postgres-store.ts +252 -0
  206. package/src/plugins/preferences/types.ts +100 -0
  207. package/src/plugins/rate-limit/__tests__/rate-limit-plugin.test.ts +259 -0
  208. package/src/plugins/rate-limit/cleanup.ts +117 -0
  209. package/src/plugins/rate-limit/env-config.ts +400 -0
  210. package/src/plugins/rate-limit/index.ts +128 -0
  211. package/src/plugins/rate-limit/middleware.ts +212 -0
  212. package/src/plugins/rate-limit/rate-limit-plugin.ts +400 -0
  213. package/src/plugins/rate-limit/rate-limit-service.ts +228 -0
  214. package/src/plugins/rate-limit/stores/cache-store.ts +261 -0
  215. package/src/plugins/rate-limit/stores/index.ts +8 -0
  216. package/src/plugins/rate-limit/stores/postgres-store.ts +402 -0
  217. package/src/plugins/rate-limit/strategies/fixed-window.ts +116 -0
  218. package/src/plugins/rate-limit/strategies/index.ts +30 -0
  219. package/src/plugins/rate-limit/strategies/sliding-window.ts +157 -0
  220. package/src/plugins/rate-limit/strategies/token-bucket.ts +154 -0
  221. package/src/plugins/rate-limit/types.ts +338 -0
  222. package/src/plugins/users/__tests__/users-plugin.test.ts +690 -0
  223. package/src/plugins/users/index.ts +3 -0
  224. package/src/plugins/users/types.ts +38 -0
  225. package/src/plugins/users/users-plugin.ts +142 -5
  226. package/ui/src/App.tsx +35 -14
  227. package/ui/src/api/controlPanelApi.ts +326 -1
  228. package/ui/src/components/ControlPanelApp.tsx +3 -0
  229. package/ui/src/dashboard/PluginWidgetRenderer.tsx +13 -10
  230. package/ui/src/dashboard/WidgetComponentRegistry.tsx +13 -9
  231. package/ui/src/dashboard/builtInWidgets.tsx +13 -3
  232. package/ui/src/dashboard/widgets/AuthStatusWidget.tsx +143 -0
  233. package/ui/src/dashboard/widgets/IntegrationStatusWidget.tsx +135 -0
  234. package/ui/src/dashboard/widgets/index.ts +2 -0
  235. package/ui/src/pages/AuthPage.tsx +1103 -0
  236. package/ui/src/pages/IntegrationsPage.tsx +288 -0
  237. package/ui/src/pages/PluginsPage.tsx +394 -0
  238. package/ui/src/pages/RateLimitPage.tsx +292 -0
  239. package/ui/vite.lib.config.ts +5 -0
  240. package/dist-ui/assets/index-Bsp2ntcw.js +0 -465
  241. package/dist-ui/assets/index-Bsp2ntcw.js.map +0 -1
@@ -0,0 +1,350 @@
1
+ /**
2
+ * Preferences Plugin Tests
3
+ *
4
+ * Unit tests for the preferences plugin using mocked dependencies.
5
+ */
6
+
7
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
8
+ import {
9
+ createPreferencesPlugin,
10
+ getPreferencesStore,
11
+ getPreferences,
12
+ updatePreferences,
13
+ deletePreferences,
14
+ getDefaultPreferences,
15
+ } from '../preferences-plugin.js';
16
+ import type { PreferencesStore, PreferencesPluginConfig } from '../types.js';
17
+ import type { PluginRegistry } from '../../../core/plugin-registry.js';
18
+
19
+ describe('Preferences Plugin', () => {
20
+ // Mock store
21
+ const createMockStore = (): PreferencesStore => ({
22
+ name: 'mock',
23
+ initialize: vi.fn().mockResolvedValue(undefined),
24
+ get: vi.fn().mockResolvedValue(null),
25
+ update: vi.fn().mockResolvedValue({}),
26
+ delete: vi.fn().mockResolvedValue(true),
27
+ shutdown: vi.fn().mockResolvedValue(undefined),
28
+ });
29
+
30
+ // Mock registry
31
+ const createMockRegistry = (hasUsers = true): PluginRegistry =>
32
+ ({
33
+ hasPlugin: vi.fn().mockImplementation((id: string) => id === 'users' && hasUsers),
34
+ getPlugin: vi.fn().mockReturnValue(null),
35
+ listPlugins: vi.fn().mockReturnValue([]),
36
+ addRoute: vi.fn(),
37
+ addMenuItem: vi.fn(),
38
+ addPage: vi.fn(),
39
+ addWidget: vi.fn(),
40
+ getRoutes: vi.fn().mockReturnValue([]),
41
+ getMenuItems: vi.fn().mockReturnValue([]),
42
+ getPages: vi.fn().mockReturnValue([]),
43
+ getWidgets: vi.fn().mockReturnValue([]),
44
+ getConfig: vi.fn().mockReturnValue({}),
45
+ setConfig: vi.fn().mockResolvedValue(undefined),
46
+ subscribe: vi.fn().mockReturnValue(() => {}),
47
+ emit: vi.fn(),
48
+ registerHealthCheck: vi.fn(),
49
+ getApp: vi.fn().mockReturnValue({} as any),
50
+ getRouter: vi.fn().mockReturnValue({} as any),
51
+ getLogger: vi.fn().mockReturnValue({
52
+ debug: vi.fn(),
53
+ info: vi.fn(),
54
+ warn: vi.fn(),
55
+ error: vi.fn(),
56
+ }),
57
+ }) as unknown as PluginRegistry;
58
+
59
+ let mockStore: PreferencesStore;
60
+ let mockRegistry: PluginRegistry;
61
+
62
+ beforeEach(() => {
63
+ vi.clearAllMocks();
64
+ mockStore = createMockStore();
65
+ mockRegistry = createMockRegistry();
66
+ });
67
+
68
+ afterEach(async () => {
69
+ // Clean up by stopping the plugin if it was started
70
+ const store = getPreferencesStore();
71
+ if (store) {
72
+ const plugin = createPreferencesPlugin({ store: mockStore });
73
+ await plugin.onStop();
74
+ }
75
+ });
76
+
77
+ describe('createPreferencesPlugin', () => {
78
+ it('should create a plugin with correct id', () => {
79
+ const plugin = createPreferencesPlugin({ store: mockStore });
80
+ expect(plugin.id).toBe('preferences');
81
+ });
82
+
83
+ it('should create a plugin with correct name', () => {
84
+ const plugin = createPreferencesPlugin({ store: mockStore });
85
+ expect(plugin.name).toBe('Preferences');
86
+ });
87
+
88
+ it('should create a plugin with version', () => {
89
+ const plugin = createPreferencesPlugin({ store: mockStore });
90
+ expect(plugin.version).toBe('1.0.0');
91
+ });
92
+ });
93
+
94
+ describe('onStart', () => {
95
+ it('should throw error if users plugin is not loaded', async () => {
96
+ const registryWithoutUsers = createMockRegistry(false);
97
+ const plugin = createPreferencesPlugin({ store: mockStore });
98
+
99
+ await expect(plugin.onStart({}, registryWithoutUsers)).rejects.toThrow(
100
+ 'Preferences plugin requires Users plugin to be loaded first'
101
+ );
102
+ });
103
+
104
+ it('should initialize store on start', async () => {
105
+ const plugin = createPreferencesPlugin({ store: mockStore });
106
+ await plugin.onStart({}, mockRegistry);
107
+
108
+ expect(mockStore.initialize).toHaveBeenCalled();
109
+ });
110
+
111
+ it('should register health check', async () => {
112
+ const plugin = createPreferencesPlugin({ store: mockStore });
113
+ await plugin.onStart({}, mockRegistry);
114
+
115
+ expect(mockRegistry.registerHealthCheck).toHaveBeenCalledWith(
116
+ expect.objectContaining({
117
+ name: 'preferences-store',
118
+ type: 'custom',
119
+ })
120
+ );
121
+ });
122
+
123
+ it('should register API routes by default', async () => {
124
+ const plugin = createPreferencesPlugin({ store: mockStore });
125
+ await plugin.onStart({}, mockRegistry);
126
+
127
+ expect(mockRegistry.addRoute).toHaveBeenCalledTimes(3); // GET, PUT, DELETE
128
+ });
129
+
130
+ it('should register routes with correct paths', async () => {
131
+ const plugin = createPreferencesPlugin({ store: mockStore });
132
+ await plugin.onStart({}, mockRegistry);
133
+
134
+ const calls = (mockRegistry.addRoute as any).mock.calls;
135
+
136
+ // Check GET route
137
+ expect(calls[0][0]).toMatchObject({
138
+ method: 'get',
139
+ path: '/preferences',
140
+ pluginId: 'preferences',
141
+ });
142
+
143
+ // Check PUT route
144
+ expect(calls[1][0]).toMatchObject({
145
+ method: 'put',
146
+ path: '/preferences',
147
+ pluginId: 'preferences',
148
+ });
149
+
150
+ // Check DELETE route
151
+ expect(calls[2][0]).toMatchObject({
152
+ method: 'delete',
153
+ path: '/preferences',
154
+ pluginId: 'preferences',
155
+ });
156
+ });
157
+
158
+ it('should use custom API prefix when provided', async () => {
159
+ const plugin = createPreferencesPlugin({
160
+ store: mockStore,
161
+ api: { prefix: '/user-prefs' },
162
+ });
163
+ await plugin.onStart({}, mockRegistry);
164
+
165
+ const calls = (mockRegistry.addRoute as any).mock.calls;
166
+ expect(calls[0][0].path).toBe('/user-prefs');
167
+ });
168
+
169
+ it('should not register routes when API is disabled', async () => {
170
+ const plugin = createPreferencesPlugin({
171
+ store: mockStore,
172
+ api: { enabled: false },
173
+ });
174
+ await plugin.onStart({}, mockRegistry);
175
+
176
+ expect(mockRegistry.addRoute).not.toHaveBeenCalled();
177
+ });
178
+
179
+ it('should set store reference for helper functions', async () => {
180
+ const plugin = createPreferencesPlugin({ store: mockStore });
181
+ await plugin.onStart({}, mockRegistry);
182
+
183
+ expect(getPreferencesStore()).toBe(mockStore);
184
+ });
185
+ });
186
+
187
+ describe('onStop', () => {
188
+ it('should shutdown store', async () => {
189
+ const plugin = createPreferencesPlugin({ store: mockStore });
190
+ await plugin.onStart({}, mockRegistry);
191
+ await plugin.onStop();
192
+
193
+ expect(mockStore.shutdown).toHaveBeenCalled();
194
+ });
195
+
196
+ it('should clear store reference', async () => {
197
+ const plugin = createPreferencesPlugin({ store: mockStore });
198
+ await plugin.onStart({}, mockRegistry);
199
+ await plugin.onStop();
200
+
201
+ expect(getPreferencesStore()).toBeNull();
202
+ });
203
+ });
204
+
205
+ describe('helper functions', () => {
206
+ describe('getPreferencesStore', () => {
207
+ it('should return null when plugin not started', () => {
208
+ expect(getPreferencesStore()).toBeNull();
209
+ });
210
+
211
+ it('should return store when plugin started', async () => {
212
+ const plugin = createPreferencesPlugin({ store: mockStore });
213
+ await plugin.onStart({}, mockRegistry);
214
+
215
+ expect(getPreferencesStore()).toBe(mockStore);
216
+ });
217
+ });
218
+
219
+ describe('getPreferences', () => {
220
+ it('should throw when plugin not initialized', async () => {
221
+ await expect(getPreferences('user-id')).rejects.toThrow(
222
+ 'Preferences plugin not initialized'
223
+ );
224
+ });
225
+
226
+ it('should return defaults when no stored preferences', async () => {
227
+ const defaults = { theme: 'light' };
228
+ const plugin = createPreferencesPlugin({ store: mockStore, defaults });
229
+ await plugin.onStart({}, mockRegistry);
230
+
231
+ (mockStore.get as any).mockResolvedValue(null);
232
+
233
+ const result = await getPreferences('user-id');
234
+ expect(result).toEqual({ theme: 'light' });
235
+ });
236
+
237
+ it('should merge stored preferences with defaults', async () => {
238
+ const defaults = { theme: 'light', notifications: { email: true } };
239
+ const plugin = createPreferencesPlugin({ store: mockStore, defaults });
240
+ await plugin.onStart({}, mockRegistry);
241
+
242
+ (mockStore.get as any).mockResolvedValue({ theme: 'dark' });
243
+
244
+ const result = await getPreferences('user-id');
245
+ expect(result).toEqual({
246
+ theme: 'dark',
247
+ notifications: { email: true },
248
+ });
249
+ });
250
+
251
+ it('should call store.get with userId', async () => {
252
+ const plugin = createPreferencesPlugin({ store: mockStore });
253
+ await plugin.onStart({}, mockRegistry);
254
+
255
+ await getPreferences('test-user-id');
256
+
257
+ expect(mockStore.get).toHaveBeenCalledWith('test-user-id');
258
+ });
259
+ });
260
+
261
+ describe('updatePreferences', () => {
262
+ it('should throw when plugin not initialized', async () => {
263
+ await expect(updatePreferences('user-id', {})).rejects.toThrow(
264
+ 'Preferences plugin not initialized'
265
+ );
266
+ });
267
+
268
+ it('should call store.update with userId and preferences', async () => {
269
+ const plugin = createPreferencesPlugin({ store: mockStore });
270
+ await plugin.onStart({}, mockRegistry);
271
+
272
+ const prefs = { theme: 'dark' };
273
+ (mockStore.update as any).mockResolvedValue(prefs);
274
+
275
+ await updatePreferences('test-user-id', prefs);
276
+
277
+ expect(mockStore.update).toHaveBeenCalledWith('test-user-id', prefs);
278
+ });
279
+
280
+ it('should merge result with defaults', async () => {
281
+ const defaults = { notifications: { email: true } };
282
+ const plugin = createPreferencesPlugin({ store: mockStore, defaults });
283
+ await plugin.onStart({}, mockRegistry);
284
+
285
+ (mockStore.update as any).mockResolvedValue({ theme: 'dark' });
286
+
287
+ const result = await updatePreferences('user-id', { theme: 'dark' });
288
+ expect(result).toEqual({
289
+ theme: 'dark',
290
+ notifications: { email: true },
291
+ });
292
+ });
293
+ });
294
+
295
+ describe('deletePreferences', () => {
296
+ it('should throw when plugin not initialized', async () => {
297
+ await expect(deletePreferences('user-id')).rejects.toThrow(
298
+ 'Preferences plugin not initialized'
299
+ );
300
+ });
301
+
302
+ it('should call store.delete with userId', async () => {
303
+ const plugin = createPreferencesPlugin({ store: mockStore });
304
+ await plugin.onStart({}, mockRegistry);
305
+
306
+ await deletePreferences('test-user-id');
307
+
308
+ expect(mockStore.delete).toHaveBeenCalledWith('test-user-id');
309
+ });
310
+
311
+ it('should return store.delete result', async () => {
312
+ const plugin = createPreferencesPlugin({ store: mockStore });
313
+ await plugin.onStart({}, mockRegistry);
314
+
315
+ (mockStore.delete as any).mockResolvedValue(true);
316
+ expect(await deletePreferences('user-id')).toBe(true);
317
+
318
+ (mockStore.delete as any).mockResolvedValue(false);
319
+ expect(await deletePreferences('user-id')).toBe(false);
320
+ });
321
+ });
322
+
323
+ describe('getDefaultPreferences', () => {
324
+ it('should return empty object when no defaults configured', async () => {
325
+ const plugin = createPreferencesPlugin({ store: mockStore });
326
+ await plugin.onStart({}, mockRegistry);
327
+
328
+ expect(getDefaultPreferences()).toEqual({});
329
+ });
330
+
331
+ it('should return configured defaults', async () => {
332
+ const defaults = { theme: 'light', lang: 'en' };
333
+ const plugin = createPreferencesPlugin({ store: mockStore, defaults });
334
+ await plugin.onStart({}, mockRegistry);
335
+
336
+ expect(getDefaultPreferences()).toEqual(defaults);
337
+ });
338
+
339
+ it('should return a copy (not reference)', async () => {
340
+ const defaults = { theme: 'light' };
341
+ const plugin = createPreferencesPlugin({ store: mockStore, defaults });
342
+ await plugin.onStart({}, mockRegistry);
343
+
344
+ const result = getDefaultPreferences();
345
+ expect(result).not.toBe(defaults);
346
+ expect(result).toEqual(defaults);
347
+ });
348
+ });
349
+ });
350
+ });
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Preferences Plugin Index
3
+ *
4
+ * User preferences management plugin with PostgreSQL RLS.
5
+ * Depends on the Users Plugin for user identity.
6
+ *
7
+ * Copyright (c) 2025 QwickApps.com. All rights reserved.
8
+ */
9
+
10
+ // Main plugin
11
+ export {
12
+ createPreferencesPlugin,
13
+ getPreferencesStore,
14
+ getPreferences,
15
+ updatePreferences,
16
+ deletePreferences,
17
+ getDefaultPreferences,
18
+ } from './preferences-plugin.js';
19
+
20
+ // Types
21
+ export type {
22
+ PreferencesPluginConfig,
23
+ PreferencesStore,
24
+ UserPreferences,
25
+ PostgresPreferencesStoreConfig,
26
+ PreferencesApiConfig,
27
+ } from './types.js';
28
+
29
+ // Stores
30
+ export { postgresPreferencesStore, deepMerge } from './stores/index.js';
@@ -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';