@qwickapps/server 1.3.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.
- package/README.md +154 -0
- package/dist/core/control-panel.d.ts.map +1 -1
- package/dist/core/control-panel.js +30 -2
- package/dist/core/control-panel.js.map +1 -1
- package/dist/core/plugin-registry.d.ts +36 -0
- package/dist/core/plugin-registry.d.ts.map +1 -1
- package/dist/core/plugin-registry.js +26 -0
- package/dist/core/plugin-registry.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/plugins/auth/adapters/index.d.ts +1 -0
- package/dist/plugins/auth/adapters/index.d.ts.map +1 -1
- package/dist/plugins/auth/adapters/index.js +1 -0
- package/dist/plugins/auth/adapters/index.js.map +1 -1
- package/dist/plugins/auth/adapters/supabase-adapter.d.ts.map +1 -1
- package/dist/plugins/auth/adapters/supabase-adapter.js.map +1 -1
- package/dist/plugins/auth/adapters/supertokens-adapter.d.ts +18 -0
- package/dist/plugins/auth/adapters/supertokens-adapter.d.ts.map +1 -0
- package/dist/plugins/auth/adapters/supertokens-adapter.js +267 -0
- package/dist/plugins/auth/adapters/supertokens-adapter.js.map +1 -0
- package/dist/plugins/auth/env-config.d.ts +88 -0
- package/dist/plugins/auth/env-config.d.ts.map +1 -0
- package/dist/plugins/auth/env-config.js +489 -0
- package/dist/plugins/auth/env-config.js.map +1 -0
- package/dist/plugins/auth/index.d.ts +3 -1
- package/dist/plugins/auth/index.d.ts.map +1 -1
- package/dist/plugins/auth/index.js +3 -0
- package/dist/plugins/auth/index.js.map +1 -1
- package/dist/plugins/auth/supertokens-adapter.test.d.ts +10 -0
- package/dist/plugins/auth/supertokens-adapter.test.d.ts.map +1 -0
- package/dist/plugins/auth/supertokens-adapter.test.js +486 -0
- package/dist/plugins/auth/supertokens-adapter.test.js.map +1 -0
- package/dist/plugins/auth/types.d.ts +70 -0
- package/dist/plugins/auth/types.d.ts.map +1 -1
- package/dist/plugins/auth/types.js.map +1 -1
- package/dist/plugins/cache-plugin.test.js +3 -0
- package/dist/plugins/cache-plugin.test.js.map +1 -1
- package/dist/plugins/index.d.ts +4 -2
- package/dist/plugins/index.d.ts.map +1 -1
- package/dist/plugins/index.js +3 -1
- package/dist/plugins/index.js.map +1 -1
- package/dist/plugins/postgres-plugin.test.js +3 -0
- package/dist/plugins/postgres-plugin.test.js.map +1 -1
- package/dist/plugins/preferences/__tests__/deep-merge.test.d.ts +7 -0
- package/dist/plugins/preferences/__tests__/deep-merge.test.d.ts.map +1 -0
- package/dist/plugins/preferences/__tests__/deep-merge.test.js +215 -0
- package/dist/plugins/preferences/__tests__/deep-merge.test.js.map +1 -0
- package/dist/plugins/preferences/__tests__/preferences-plugin.test.d.ts +7 -0
- package/dist/plugins/preferences/__tests__/preferences-plugin.test.d.ts.map +1 -0
- package/dist/plugins/preferences/__tests__/preferences-plugin.test.js +265 -0
- package/dist/plugins/preferences/__tests__/preferences-plugin.test.js.map +1 -0
- package/dist/plugins/preferences/index.d.ts +12 -0
- package/dist/plugins/preferences/index.d.ts.map +1 -0
- package/dist/plugins/preferences/index.js +13 -0
- package/dist/plugins/preferences/index.js.map +1 -0
- package/dist/plugins/preferences/preferences-plugin.d.ts +39 -0
- package/dist/plugins/preferences/preferences-plugin.d.ts.map +1 -0
- package/dist/plugins/preferences/preferences-plugin.js +226 -0
- package/dist/plugins/preferences/preferences-plugin.js.map +1 -0
- package/dist/plugins/preferences/stores/index.d.ts +9 -0
- package/dist/plugins/preferences/stores/index.d.ts.map +1 -0
- package/dist/plugins/preferences/stores/index.js +9 -0
- package/dist/plugins/preferences/stores/index.js.map +1 -0
- package/dist/plugins/preferences/stores/postgres-store.d.ts +41 -0
- package/dist/plugins/preferences/stores/postgres-store.d.ts.map +1 -0
- package/dist/plugins/preferences/stores/postgres-store.js +181 -0
- package/dist/plugins/preferences/stores/postgres-store.js.map +1 -0
- package/dist/plugins/preferences/types.d.ts +91 -0
- package/dist/plugins/preferences/types.d.ts.map +1 -0
- package/dist/plugins/preferences/types.js +10 -0
- package/dist/plugins/preferences/types.js.map +1 -0
- package/dist/plugins/users/__tests__/users-plugin.test.d.ts +9 -0
- package/dist/plugins/users/__tests__/users-plugin.test.d.ts.map +1 -0
- package/dist/plugins/users/__tests__/users-plugin.test.js +546 -0
- package/dist/plugins/users/__tests__/users-plugin.test.js.map +1 -0
- package/dist/plugins/users/index.d.ts +2 -2
- package/dist/plugins/users/index.d.ts.map +1 -1
- package/dist/plugins/users/index.js +1 -1
- package/dist/plugins/users/index.js.map +1 -1
- package/dist/plugins/users/types.d.ts +36 -0
- package/dist/plugins/users/types.d.ts.map +1 -1
- package/dist/plugins/users/users-plugin.d.ts +8 -2
- package/dist/plugins/users/users-plugin.d.ts.map +1 -1
- package/dist/plugins/users/users-plugin.js +122 -5
- package/dist/plugins/users/users-plugin.js.map +1 -1
- package/dist-ui/assets/{index-Bsp2ntcw.js → index-BY8OxNgO.js} +112 -112
- package/dist-ui/assets/index-BY8OxNgO.js.map +1 -0
- package/dist-ui/index.html +1 -1
- package/dist-ui-lib/api/controlPanelApi.d.ts +53 -7
- package/dist-ui-lib/dashboard/WidgetComponentRegistry.d.ts +9 -5
- package/dist-ui-lib/dashboard/builtInWidgets.d.ts +7 -1
- package/dist-ui-lib/index.js +2382 -3651
- package/dist-ui-lib/index.js.map +1 -1
- package/dist-ui-lib/pages/AuthPage.d.ts +1 -0
- package/dist-ui-lib/pages/PluginsPage.d.ts +1 -0
- package/package.json +7 -2
- package/src/core/control-panel.ts +33 -2
- package/src/core/plugin-registry.ts +63 -0
- package/src/index.ts +7 -0
- package/src/plugins/auth/adapters/index.ts +1 -0
- package/src/plugins/auth/adapters/supabase-adapter.ts +22 -14
- package/src/plugins/auth/adapters/supertokens-adapter.ts +326 -0
- package/src/plugins/auth/env-config.ts +572 -0
- package/src/plugins/auth/index.ts +9 -0
- package/src/plugins/auth/supertokens-adapter.test.ts +621 -0
- package/src/plugins/auth/types.ts +80 -0
- package/src/plugins/cache-plugin.test.ts +3 -0
- package/src/plugins/index.ts +26 -0
- package/src/plugins/postgres-plugin.test.ts +3 -0
- package/src/plugins/preferences/__tests__/deep-merge.test.ts +242 -0
- package/src/plugins/preferences/__tests__/preferences-plugin.test.ts +350 -0
- package/src/plugins/preferences/index.ts +30 -0
- package/src/plugins/preferences/preferences-plugin.ts +270 -0
- package/src/plugins/preferences/stores/index.ts +9 -0
- package/src/plugins/preferences/stores/postgres-store.ts +252 -0
- package/src/plugins/preferences/types.ts +100 -0
- package/src/plugins/users/__tests__/users-plugin.test.ts +690 -0
- package/src/plugins/users/index.ts +3 -0
- package/src/plugins/users/types.ts +38 -0
- package/src/plugins/users/users-plugin.ts +142 -5
- package/ui/src/App.tsx +4 -1
- package/ui/src/api/controlPanelApi.ts +100 -1
- package/ui/src/components/ControlPanelApp.tsx +3 -0
- package/ui/src/dashboard/PluginWidgetRenderer.tsx +13 -10
- package/ui/src/dashboard/WidgetComponentRegistry.tsx +13 -9
- package/ui/src/dashboard/builtInWidgets.tsx +8 -2
- package/ui/src/pages/AuthPage.tsx +259 -0
- package/ui/src/pages/PluginsPage.tsx +394 -0
- package/ui/vite.lib.config.ts +5 -0
- package/dist-ui/assets/index-Bsp2ntcw.js.map +0 -1
|
@@ -79,10 +79,13 @@ describe('Cache Plugin', () => {
|
|
|
79
79
|
addMenuItem: vi.fn(),
|
|
80
80
|
addPage: vi.fn(),
|
|
81
81
|
addWidget: vi.fn(),
|
|
82
|
+
addConfigComponent: vi.fn(),
|
|
82
83
|
getRoutes: vi.fn().mockReturnValue([]),
|
|
83
84
|
getMenuItems: vi.fn().mockReturnValue([]),
|
|
84
85
|
getPages: vi.fn().mockReturnValue([]),
|
|
85
86
|
getWidgets: vi.fn().mockReturnValue([]),
|
|
87
|
+
getConfigComponents: vi.fn().mockReturnValue([]),
|
|
88
|
+
getPluginContributions: vi.fn().mockReturnValue({ routes: [], menuItems: [], pages: [], widgets: [], config: undefined }),
|
|
86
89
|
getConfig: vi.fn().mockReturnValue({}),
|
|
87
90
|
setConfig: vi.fn().mockResolvedValue(undefined),
|
|
88
91
|
subscribe: vi.fn().mockReturnValue(() => {}),
|
package/src/plugins/index.ts
CHANGED
|
@@ -28,6 +28,8 @@ export type { CachePluginConfig, CacheInstance } from './cache-plugin.js';
|
|
|
28
28
|
// Auth plugin
|
|
29
29
|
export {
|
|
30
30
|
createAuthPlugin,
|
|
31
|
+
createAuthPluginFromEnv,
|
|
32
|
+
getAuthStatus,
|
|
31
33
|
isAuthenticated,
|
|
32
34
|
getAuthenticatedUser,
|
|
33
35
|
getAccessToken,
|
|
@@ -37,6 +39,7 @@ export {
|
|
|
37
39
|
auth0Adapter,
|
|
38
40
|
basicAdapter,
|
|
39
41
|
supabaseAdapter,
|
|
42
|
+
supertokensAdapter,
|
|
40
43
|
isAuthenticatedRequest,
|
|
41
44
|
} from './auth/index.js';
|
|
42
45
|
export type {
|
|
@@ -47,6 +50,10 @@ export type {
|
|
|
47
50
|
Auth0AdapterConfig,
|
|
48
51
|
SupabaseAdapterConfig,
|
|
49
52
|
BasicAdapterConfig,
|
|
53
|
+
SupertokensAdapterConfig,
|
|
54
|
+
AuthPluginState,
|
|
55
|
+
AuthEnvPluginOptions,
|
|
56
|
+
AuthConfigStatus,
|
|
50
57
|
} from './auth/index.js';
|
|
51
58
|
|
|
52
59
|
// Users plugin
|
|
@@ -130,3 +137,22 @@ export type {
|
|
|
130
137
|
CachedEntitlements,
|
|
131
138
|
EntitlementStats,
|
|
132
139
|
} from './entitlements/index.js';
|
|
140
|
+
|
|
141
|
+
// Preferences plugin (depends on Users)
|
|
142
|
+
export {
|
|
143
|
+
createPreferencesPlugin,
|
|
144
|
+
getPreferencesStore,
|
|
145
|
+
getPreferences,
|
|
146
|
+
updatePreferences,
|
|
147
|
+
deletePreferences,
|
|
148
|
+
getDefaultPreferences,
|
|
149
|
+
postgresPreferencesStore,
|
|
150
|
+
deepMerge,
|
|
151
|
+
} from './preferences/index.js';
|
|
152
|
+
export type {
|
|
153
|
+
PreferencesPluginConfig,
|
|
154
|
+
PreferencesStore,
|
|
155
|
+
UserPreferences,
|
|
156
|
+
PostgresPreferencesStoreConfig,
|
|
157
|
+
PreferencesApiConfig,
|
|
158
|
+
} from './preferences/index.js';
|
|
@@ -56,10 +56,13 @@ describe('PostgreSQL Plugin', () => {
|
|
|
56
56
|
addMenuItem: vi.fn(),
|
|
57
57
|
addPage: vi.fn(),
|
|
58
58
|
addWidget: vi.fn(),
|
|
59
|
+
addConfigComponent: vi.fn(),
|
|
59
60
|
getRoutes: vi.fn().mockReturnValue([]),
|
|
60
61
|
getMenuItems: vi.fn().mockReturnValue([]),
|
|
61
62
|
getPages: vi.fn().mockReturnValue([]),
|
|
62
63
|
getWidgets: vi.fn().mockReturnValue([]),
|
|
64
|
+
getConfigComponents: vi.fn().mockReturnValue([]),
|
|
65
|
+
getPluginContributions: vi.fn().mockReturnValue({ routes: [], menuItems: [], pages: [], widgets: [], config: undefined }),
|
|
63
66
|
getConfig: vi.fn().mockReturnValue({}),
|
|
64
67
|
setConfig: vi.fn().mockResolvedValue(undefined),
|
|
65
68
|
subscribe: vi.fn().mockReturnValue(() => {}),
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deep Merge Utility Tests
|
|
3
|
+
*
|
|
4
|
+
* Unit tests for the deep merge function used by the preferences plugin.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { describe, it, expect } from 'vitest';
|
|
8
|
+
import { deepMerge } from '../stores/postgres-store.js';
|
|
9
|
+
|
|
10
|
+
describe('deepMerge', () => {
|
|
11
|
+
describe('basic merging', () => {
|
|
12
|
+
it('should merge flat objects', () => {
|
|
13
|
+
const target = { a: 1 };
|
|
14
|
+
const source = { b: 2 };
|
|
15
|
+
const result = deepMerge(target, source);
|
|
16
|
+
expect(result).toEqual({ a: 1, b: 2 });
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('should not mutate original objects', () => {
|
|
20
|
+
const target = { a: 1 };
|
|
21
|
+
const source = { b: 2 };
|
|
22
|
+
deepMerge(target, source);
|
|
23
|
+
expect(target).toEqual({ a: 1 });
|
|
24
|
+
expect(source).toEqual({ b: 2 });
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('should return a new object', () => {
|
|
28
|
+
const target = { a: 1 };
|
|
29
|
+
const source = { b: 2 };
|
|
30
|
+
const result = deepMerge(target, source);
|
|
31
|
+
expect(result).not.toBe(target);
|
|
32
|
+
expect(result).not.toBe(source);
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
describe('nested object merging', () => {
|
|
37
|
+
it('should merge nested objects recursively', () => {
|
|
38
|
+
const target = { a: { x: 1 } };
|
|
39
|
+
const source = { a: { y: 2 } };
|
|
40
|
+
const result = deepMerge(target, source);
|
|
41
|
+
expect(result).toEqual({ a: { x: 1, y: 2 } });
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('should handle deeply nested objects', () => {
|
|
45
|
+
const target = { a: { b: { c: { x: 1 } } } };
|
|
46
|
+
const source = { a: { b: { c: { y: 2 } } } };
|
|
47
|
+
const result = deepMerge(target, source);
|
|
48
|
+
expect(result).toEqual({ a: { b: { c: { x: 1, y: 2 } } } });
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('should merge multiple nested keys', () => {
|
|
52
|
+
const target = {
|
|
53
|
+
theme: 'light',
|
|
54
|
+
notifications: { email: true, push: true },
|
|
55
|
+
};
|
|
56
|
+
const source = {
|
|
57
|
+
notifications: { email: false },
|
|
58
|
+
};
|
|
59
|
+
const result = deepMerge(target, source);
|
|
60
|
+
expect(result).toEqual({
|
|
61
|
+
theme: 'light',
|
|
62
|
+
notifications: { email: false, push: true },
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
describe('value overwriting', () => {
|
|
68
|
+
it('should let source override target for same keys', () => {
|
|
69
|
+
const target = { a: 1 };
|
|
70
|
+
const source = { a: 2 };
|
|
71
|
+
const result = deepMerge(target, source);
|
|
72
|
+
expect(result).toEqual({ a: 2 });
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('should let source override target for nested keys', () => {
|
|
76
|
+
const target = { a: { x: 1 } };
|
|
77
|
+
const source = { a: { x: 2 } };
|
|
78
|
+
const result = deepMerge(target, source);
|
|
79
|
+
expect(result).toEqual({ a: { x: 2 } });
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
describe('array handling', () => {
|
|
84
|
+
it('should replace arrays (not merge)', () => {
|
|
85
|
+
const target = { a: [1, 2] };
|
|
86
|
+
const source = { a: [3, 4, 5] };
|
|
87
|
+
const result = deepMerge(target, source);
|
|
88
|
+
expect(result).toEqual({ a: [3, 4, 5] });
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('should replace array with empty array', () => {
|
|
92
|
+
const target = { a: [1, 2, 3] };
|
|
93
|
+
const source = { a: [] };
|
|
94
|
+
const result = deepMerge(target, source);
|
|
95
|
+
expect(result).toEqual({ a: [] });
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('should replace non-array with array', () => {
|
|
99
|
+
const target = { a: 'string' };
|
|
100
|
+
const source = { a: [1, 2] };
|
|
101
|
+
const result = deepMerge(target, source);
|
|
102
|
+
expect(result).toEqual({ a: [1, 2] });
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('should replace array with non-array', () => {
|
|
106
|
+
const target = { a: [1, 2] };
|
|
107
|
+
const source = { a: 'string' };
|
|
108
|
+
const result = deepMerge(target, source);
|
|
109
|
+
expect(result).toEqual({ a: 'string' });
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
describe('special values', () => {
|
|
114
|
+
it('should handle null values in source', () => {
|
|
115
|
+
const target = { a: 1 };
|
|
116
|
+
const source = { a: null };
|
|
117
|
+
const result = deepMerge(target, source);
|
|
118
|
+
expect(result).toEqual({ a: null });
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('should skip undefined values in source', () => {
|
|
122
|
+
const target = { a: 1 };
|
|
123
|
+
const source = { a: undefined };
|
|
124
|
+
const result = deepMerge(target, source);
|
|
125
|
+
expect(result).toEqual({ a: 1 });
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('should handle null in target', () => {
|
|
129
|
+
const target = { a: null };
|
|
130
|
+
const source = { a: { x: 1 } };
|
|
131
|
+
const result = deepMerge(target, source);
|
|
132
|
+
expect(result).toEqual({ a: { x: 1 } });
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it('should replace object with null', () => {
|
|
136
|
+
const target = { a: { x: 1 } };
|
|
137
|
+
const source = { a: null };
|
|
138
|
+
const result = deepMerge(target, source);
|
|
139
|
+
expect(result).toEqual({ a: null });
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
describe('edge cases', () => {
|
|
144
|
+
it('should handle empty target', () => {
|
|
145
|
+
const target = {};
|
|
146
|
+
const source = { a: 1 };
|
|
147
|
+
const result = deepMerge(target, source);
|
|
148
|
+
expect(result).toEqual({ a: 1 });
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it('should handle empty source', () => {
|
|
152
|
+
const target = { a: 1 };
|
|
153
|
+
const source = {};
|
|
154
|
+
const result = deepMerge(target, source);
|
|
155
|
+
expect(result).toEqual({ a: 1 });
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it('should handle both empty', () => {
|
|
159
|
+
const target = {};
|
|
160
|
+
const source = {};
|
|
161
|
+
const result = deepMerge(target, source);
|
|
162
|
+
expect(result).toEqual({});
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it('should handle primitive values becoming objects', () => {
|
|
166
|
+
const target = { a: 'string' };
|
|
167
|
+
const source = { a: { x: 1 } };
|
|
168
|
+
const result = deepMerge(target, source);
|
|
169
|
+
expect(result).toEqual({ a: { x: 1 } });
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it('should handle objects becoming primitive values', () => {
|
|
173
|
+
const target = { a: { x: 1 } };
|
|
174
|
+
const source = { a: 'string' };
|
|
175
|
+
const result = deepMerge(target, source);
|
|
176
|
+
expect(result).toEqual({ a: 'string' });
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
describe('real-world preference scenarios', () => {
|
|
181
|
+
it('should merge default preferences with user preferences', () => {
|
|
182
|
+
const defaults = {
|
|
183
|
+
theme: 'system',
|
|
184
|
+
notifications: {
|
|
185
|
+
email: true,
|
|
186
|
+
push: true,
|
|
187
|
+
sms: false,
|
|
188
|
+
},
|
|
189
|
+
trading: {
|
|
190
|
+
defaultSymbol: 'SPY',
|
|
191
|
+
chartInterval: '5min',
|
|
192
|
+
},
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
const userPrefs = {
|
|
196
|
+
theme: 'dark',
|
|
197
|
+
notifications: {
|
|
198
|
+
email: false,
|
|
199
|
+
},
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
const result = deepMerge(defaults, userPrefs);
|
|
203
|
+
expect(result).toEqual({
|
|
204
|
+
theme: 'dark',
|
|
205
|
+
notifications: {
|
|
206
|
+
email: false,
|
|
207
|
+
push: true,
|
|
208
|
+
sms: false,
|
|
209
|
+
},
|
|
210
|
+
trading: {
|
|
211
|
+
defaultSymbol: 'SPY',
|
|
212
|
+
chartInterval: '5min',
|
|
213
|
+
},
|
|
214
|
+
});
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
it('should handle partial updates to preferences', () => {
|
|
218
|
+
const existing = {
|
|
219
|
+
theme: 'dark',
|
|
220
|
+
notifications: {
|
|
221
|
+
email: false,
|
|
222
|
+
push: true,
|
|
223
|
+
},
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
const update = {
|
|
227
|
+
notifications: {
|
|
228
|
+
push: false,
|
|
229
|
+
},
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
const result = deepMerge(existing, update);
|
|
233
|
+
expect(result).toEqual({
|
|
234
|
+
theme: 'dark',
|
|
235
|
+
notifications: {
|
|
236
|
+
email: false,
|
|
237
|
+
push: false,
|
|
238
|
+
},
|
|
239
|
+
});
|
|
240
|
+
});
|
|
241
|
+
});
|
|
242
|
+
});
|
|
@@ -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';
|