@qwickapps/server 1.4.0 → 1.5.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 (273) hide show
  1. package/CHANGELOG.md +507 -0
  2. package/README.md +9 -0
  3. package/dist/index.d.ts +2 -2
  4. package/dist/index.d.ts.map +1 -1
  5. package/dist/index.js +12 -2
  6. package/dist/index.js.map +1 -1
  7. package/dist/plugins/bans/bans-plugin.d.ts.map +1 -1
  8. package/dist/plugins/bans/bans-plugin.js +12 -3
  9. package/dist/plugins/bans/bans-plugin.js.map +1 -1
  10. package/dist/plugins/devices/__tests__/devices-plugin.test.d.ts +11 -0
  11. package/dist/plugins/devices/__tests__/devices-plugin.test.d.ts.map +1 -0
  12. package/dist/plugins/devices/__tests__/devices-plugin.test.js +410 -0
  13. package/dist/plugins/devices/__tests__/devices-plugin.test.js.map +1 -0
  14. package/dist/plugins/devices/__tests__/token-utils.test.d.ts +7 -0
  15. package/dist/plugins/devices/__tests__/token-utils.test.d.ts.map +1 -0
  16. package/dist/plugins/devices/__tests__/token-utils.test.js +197 -0
  17. package/dist/plugins/devices/__tests__/token-utils.test.js.map +1 -0
  18. package/dist/plugins/devices/adapters/compute-adapter.d.ts +36 -0
  19. package/dist/plugins/devices/adapters/compute-adapter.d.ts.map +1 -0
  20. package/dist/plugins/devices/adapters/compute-adapter.js +100 -0
  21. package/dist/plugins/devices/adapters/compute-adapter.js.map +1 -0
  22. package/dist/plugins/devices/adapters/index.d.ts +12 -0
  23. package/dist/plugins/devices/adapters/index.d.ts.map +1 -0
  24. package/dist/plugins/devices/adapters/index.js +10 -0
  25. package/dist/plugins/devices/adapters/index.js.map +1 -0
  26. package/dist/plugins/devices/adapters/mobile-adapter.d.ts +41 -0
  27. package/dist/plugins/devices/adapters/mobile-adapter.d.ts.map +1 -0
  28. package/dist/plugins/devices/adapters/mobile-adapter.js +131 -0
  29. package/dist/plugins/devices/adapters/mobile-adapter.js.map +1 -0
  30. package/dist/plugins/devices/devices-plugin.d.ts +70 -0
  31. package/dist/plugins/devices/devices-plugin.d.ts.map +1 -0
  32. package/dist/plugins/devices/devices-plugin.js +453 -0
  33. package/dist/plugins/devices/devices-plugin.js.map +1 -0
  34. package/dist/plugins/devices/index.d.ts +18 -0
  35. package/dist/plugins/devices/index.d.ts.map +1 -0
  36. package/dist/plugins/devices/index.js +18 -0
  37. package/dist/plugins/devices/index.js.map +1 -0
  38. package/dist/plugins/devices/stores/index.d.ts +9 -0
  39. package/dist/plugins/devices/stores/index.d.ts.map +1 -0
  40. package/dist/plugins/devices/stores/index.js +9 -0
  41. package/dist/plugins/devices/stores/index.js.map +1 -0
  42. package/dist/plugins/devices/stores/postgres-store.d.ts +26 -0
  43. package/dist/plugins/devices/stores/postgres-store.d.ts.map +1 -0
  44. package/dist/plugins/devices/stores/postgres-store.js +199 -0
  45. package/dist/plugins/devices/stores/postgres-store.js.map +1 -0
  46. package/dist/plugins/devices/token-utils.d.ts +100 -0
  47. package/dist/plugins/devices/token-utils.d.ts.map +1 -0
  48. package/dist/plugins/devices/token-utils.js +162 -0
  49. package/dist/plugins/devices/token-utils.js.map +1 -0
  50. package/dist/plugins/devices/types.d.ts +307 -0
  51. package/dist/plugins/devices/types.d.ts.map +1 -0
  52. package/dist/plugins/devices/types.js +10 -0
  53. package/dist/plugins/devices/types.js.map +1 -0
  54. package/dist/plugins/index.d.ts +14 -2
  55. package/dist/plugins/index.d.ts.map +1 -1
  56. package/dist/plugins/index.js +13 -1
  57. package/dist/plugins/index.js.map +1 -1
  58. package/dist/plugins/notifications/__tests__/notifications-manager.test.d.ts +5 -0
  59. package/dist/plugins/notifications/__tests__/notifications-manager.test.d.ts.map +1 -0
  60. package/dist/plugins/notifications/__tests__/notifications-manager.test.js +470 -0
  61. package/dist/plugins/notifications/__tests__/notifications-manager.test.js.map +1 -0
  62. package/dist/plugins/notifications/index.d.ts +71 -0
  63. package/dist/plugins/notifications/index.d.ts.map +1 -0
  64. package/dist/plugins/notifications/index.js +72 -0
  65. package/dist/plugins/notifications/index.js.map +1 -0
  66. package/dist/plugins/notifications/notifications-manager.d.ts +182 -0
  67. package/dist/plugins/notifications/notifications-manager.d.ts.map +1 -0
  68. package/dist/plugins/notifications/notifications-manager.js +610 -0
  69. package/dist/plugins/notifications/notifications-manager.js.map +1 -0
  70. package/dist/plugins/notifications/notifications-plugin.d.ts +83 -0
  71. package/dist/plugins/notifications/notifications-plugin.d.ts.map +1 -0
  72. package/dist/plugins/notifications/notifications-plugin.js +337 -0
  73. package/dist/plugins/notifications/notifications-plugin.js.map +1 -0
  74. package/dist/plugins/notifications/types.d.ts +164 -0
  75. package/dist/plugins/notifications/types.d.ts.map +1 -0
  76. package/dist/plugins/notifications/types.js +9 -0
  77. package/dist/plugins/notifications/types.js.map +1 -0
  78. package/dist/plugins/parental/__tests__/parental-plugin.test.d.ts +12 -0
  79. package/dist/plugins/parental/__tests__/parental-plugin.test.d.ts.map +1 -0
  80. package/dist/plugins/parental/__tests__/parental-plugin.test.js +349 -0
  81. package/dist/plugins/parental/__tests__/parental-plugin.test.js.map +1 -0
  82. package/dist/plugins/parental/adapters/index.d.ts +8 -0
  83. package/dist/plugins/parental/adapters/index.d.ts.map +1 -0
  84. package/dist/plugins/parental/adapters/index.js +7 -0
  85. package/dist/plugins/parental/adapters/index.js.map +1 -0
  86. package/dist/plugins/parental/adapters/kids-adapter.d.ts +24 -0
  87. package/dist/plugins/parental/adapters/kids-adapter.d.ts.map +1 -0
  88. package/dist/plugins/parental/adapters/kids-adapter.js +174 -0
  89. package/dist/plugins/parental/adapters/kids-adapter.js.map +1 -0
  90. package/dist/plugins/parental/index.d.ts +14 -0
  91. package/dist/plugins/parental/index.d.ts.map +1 -0
  92. package/dist/plugins/parental/index.js +15 -0
  93. package/dist/plugins/parental/index.js.map +1 -0
  94. package/dist/plugins/parental/parental-plugin.d.ts +88 -0
  95. package/dist/plugins/parental/parental-plugin.d.ts.map +1 -0
  96. package/dist/plugins/parental/parental-plugin.js +666 -0
  97. package/dist/plugins/parental/parental-plugin.js.map +1 -0
  98. package/dist/plugins/parental/stores/index.d.ts +7 -0
  99. package/dist/plugins/parental/stores/index.d.ts.map +1 -0
  100. package/dist/plugins/parental/stores/index.js +7 -0
  101. package/dist/plugins/parental/stores/index.js.map +1 -0
  102. package/dist/plugins/parental/stores/postgres-store.d.ts +10 -0
  103. package/dist/plugins/parental/stores/postgres-store.d.ts.map +1 -0
  104. package/dist/plugins/parental/stores/postgres-store.js +209 -0
  105. package/dist/plugins/parental/stores/postgres-store.js.map +1 -0
  106. package/dist/plugins/parental/types.d.ts +154 -0
  107. package/dist/plugins/parental/types.d.ts.map +1 -0
  108. package/dist/plugins/parental/types.js +10 -0
  109. package/dist/plugins/parental/types.js.map +1 -0
  110. package/dist/plugins/profiles/__tests__/profiles-plugin.test.d.ts +11 -0
  111. package/dist/plugins/profiles/__tests__/profiles-plugin.test.d.ts.map +1 -0
  112. package/dist/plugins/profiles/__tests__/profiles-plugin.test.js +243 -0
  113. package/dist/plugins/profiles/__tests__/profiles-plugin.test.js.map +1 -0
  114. package/dist/plugins/profiles/index.d.ts +12 -0
  115. package/dist/plugins/profiles/index.d.ts.map +1 -0
  116. package/dist/plugins/profiles/index.js +13 -0
  117. package/dist/plugins/profiles/index.js.map +1 -0
  118. package/dist/plugins/profiles/profiles-plugin.d.ts +71 -0
  119. package/dist/plugins/profiles/profiles-plugin.d.ts.map +1 -0
  120. package/dist/plugins/profiles/profiles-plugin.js +481 -0
  121. package/dist/plugins/profiles/profiles-plugin.js.map +1 -0
  122. package/dist/plugins/profiles/stores/index.d.ts +9 -0
  123. package/dist/plugins/profiles/stores/index.d.ts.map +1 -0
  124. package/dist/plugins/profiles/stores/index.js +9 -0
  125. package/dist/plugins/profiles/stores/index.js.map +1 -0
  126. package/dist/plugins/profiles/stores/postgres-store.d.ts +18 -0
  127. package/dist/plugins/profiles/stores/postgres-store.d.ts.map +1 -0
  128. package/dist/plugins/profiles/stores/postgres-store.js +310 -0
  129. package/dist/plugins/profiles/stores/postgres-store.js.map +1 -0
  130. package/dist/plugins/profiles/types.d.ts +289 -0
  131. package/dist/plugins/profiles/types.d.ts.map +1 -0
  132. package/dist/plugins/profiles/types.js +10 -0
  133. package/dist/plugins/profiles/types.js.map +1 -0
  134. package/dist/plugins/subscriptions/__tests__/subscriptions-plugin.test.d.ts +11 -0
  135. package/dist/plugins/subscriptions/__tests__/subscriptions-plugin.test.d.ts.map +1 -0
  136. package/dist/plugins/subscriptions/__tests__/subscriptions-plugin.test.js +305 -0
  137. package/dist/plugins/subscriptions/__tests__/subscriptions-plugin.test.js.map +1 -0
  138. package/dist/plugins/subscriptions/index.d.ts +12 -0
  139. package/dist/plugins/subscriptions/index.d.ts.map +1 -0
  140. package/dist/plugins/subscriptions/index.js +13 -0
  141. package/dist/plugins/subscriptions/index.js.map +1 -0
  142. package/dist/plugins/subscriptions/stores/index.d.ts +9 -0
  143. package/dist/plugins/subscriptions/stores/index.d.ts.map +1 -0
  144. package/dist/plugins/subscriptions/stores/index.js +9 -0
  145. package/dist/plugins/subscriptions/stores/index.js.map +1 -0
  146. package/dist/plugins/subscriptions/stores/postgres-store.d.ts +14 -0
  147. package/dist/plugins/subscriptions/stores/postgres-store.d.ts.map +1 -0
  148. package/dist/plugins/subscriptions/stores/postgres-store.js +359 -0
  149. package/dist/plugins/subscriptions/stores/postgres-store.js.map +1 -0
  150. package/dist/plugins/subscriptions/subscriptions-plugin.d.ts +82 -0
  151. package/dist/plugins/subscriptions/subscriptions-plugin.d.ts.map +1 -0
  152. package/dist/plugins/subscriptions/subscriptions-plugin.js +449 -0
  153. package/dist/plugins/subscriptions/subscriptions-plugin.js.map +1 -0
  154. package/dist/plugins/subscriptions/types.d.ts +308 -0
  155. package/dist/plugins/subscriptions/types.d.ts.map +1 -0
  156. package/dist/plugins/subscriptions/types.js +10 -0
  157. package/dist/plugins/subscriptions/types.js.map +1 -0
  158. package/dist/plugins/usage/__tests__/usage-plugin.test.d.ts +11 -0
  159. package/dist/plugins/usage/__tests__/usage-plugin.test.d.ts.map +1 -0
  160. package/dist/plugins/usage/__tests__/usage-plugin.test.js +218 -0
  161. package/dist/plugins/usage/__tests__/usage-plugin.test.js.map +1 -0
  162. package/dist/plugins/usage/index.d.ts +12 -0
  163. package/dist/plugins/usage/index.d.ts.map +1 -0
  164. package/dist/plugins/usage/index.js +13 -0
  165. package/dist/plugins/usage/index.js.map +1 -0
  166. package/dist/plugins/usage/stores/index.d.ts +9 -0
  167. package/dist/plugins/usage/stores/index.d.ts.map +1 -0
  168. package/dist/plugins/usage/stores/index.js +9 -0
  169. package/dist/plugins/usage/stores/index.js.map +1 -0
  170. package/dist/plugins/usage/stores/postgres-store.d.ts +14 -0
  171. package/dist/plugins/usage/stores/postgres-store.d.ts.map +1 -0
  172. package/dist/plugins/usage/stores/postgres-store.js +146 -0
  173. package/dist/plugins/usage/stores/postgres-store.js.map +1 -0
  174. package/dist/plugins/usage/types.d.ts +195 -0
  175. package/dist/plugins/usage/types.d.ts.map +1 -0
  176. package/dist/plugins/usage/types.js +10 -0
  177. package/dist/plugins/usage/types.js.map +1 -0
  178. package/dist/plugins/usage/usage-plugin.d.ts +51 -0
  179. package/dist/plugins/usage/usage-plugin.d.ts.map +1 -0
  180. package/dist/plugins/usage/usage-plugin.js +412 -0
  181. package/dist/plugins/usage/usage-plugin.js.map +1 -0
  182. package/dist/plugins/users/__tests__/postgres-store.test.d.ts +10 -0
  183. package/dist/plugins/users/__tests__/postgres-store.test.d.ts.map +1 -0
  184. package/dist/plugins/users/__tests__/postgres-store.test.js +229 -0
  185. package/dist/plugins/users/__tests__/postgres-store.test.js.map +1 -0
  186. package/dist/plugins/users/__tests__/users-plugin.test.js +3 -0
  187. package/dist/plugins/users/__tests__/users-plugin.test.js.map +1 -1
  188. package/dist/plugins/users/index.d.ts +2 -2
  189. package/dist/plugins/users/index.d.ts.map +1 -1
  190. package/dist/plugins/users/index.js +1 -1
  191. package/dist/plugins/users/index.js.map +1 -1
  192. package/dist/plugins/users/stores/postgres-store.d.ts.map +1 -1
  193. package/dist/plugins/users/stores/postgres-store.js +76 -0
  194. package/dist/plugins/users/stores/postgres-store.js.map +1 -1
  195. package/dist/plugins/users/types.d.ts +74 -6
  196. package/dist/plugins/users/types.d.ts.map +1 -1
  197. package/dist/plugins/users/users-plugin.d.ts +15 -1
  198. package/dist/plugins/users/users-plugin.d.ts.map +1 -1
  199. package/dist/plugins/users/users-plugin.js +29 -0
  200. package/dist/plugins/users/users-plugin.js.map +1 -1
  201. package/dist-ui/assets/index-CynOqPkb.js +469 -0
  202. package/dist-ui/assets/index-CynOqPkb.js.map +1 -0
  203. package/dist-ui/index.html +1 -1
  204. package/dist-ui-lib/api/controlPanelApi.d.ts +46 -0
  205. package/dist-ui-lib/components/StatCard.d.ts +16 -0
  206. package/dist-ui-lib/dashboard/widgets/NotificationsStatsWidget.d.ts +12 -0
  207. package/dist-ui-lib/dashboard/widgets/index.d.ts +1 -0
  208. package/dist-ui-lib/index.js +1822 -1611
  209. package/dist-ui-lib/index.js.map +1 -1
  210. package/dist-ui-lib/pages/NotificationsPage.d.ts +9 -0
  211. package/dist-ui-lib/utils/formatters.d.ts +19 -0
  212. package/package.json +3 -2
  213. package/src/index.ts +178 -0
  214. package/src/plugins/bans/bans-plugin.ts +15 -3
  215. package/src/plugins/devices/__tests__/devices-plugin.test.ts +551 -0
  216. package/src/plugins/devices/__tests__/token-utils.test.ts +264 -0
  217. package/src/plugins/devices/adapters/compute-adapter.ts +139 -0
  218. package/src/plugins/devices/adapters/index.ts +13 -0
  219. package/src/plugins/devices/adapters/mobile-adapter.ts +179 -0
  220. package/src/plugins/devices/devices-plugin.ts +538 -0
  221. package/src/plugins/devices/index.ts +69 -0
  222. package/src/plugins/devices/stores/index.ts +9 -0
  223. package/src/plugins/devices/stores/postgres-store.ts +304 -0
  224. package/src/plugins/devices/token-utils.ts +213 -0
  225. package/src/plugins/devices/types.ts +351 -0
  226. package/src/plugins/index.ts +218 -0
  227. package/src/plugins/notifications/__tests__/notifications-manager.test.ts +637 -0
  228. package/src/plugins/notifications/index.ts +91 -0
  229. package/src/plugins/notifications/notifications-manager.ts +773 -0
  230. package/src/plugins/notifications/notifications-plugin.ts +398 -0
  231. package/src/plugins/notifications/types.ts +207 -0
  232. package/src/plugins/parental/__tests__/parental-plugin.test.ts +465 -0
  233. package/src/plugins/parental/adapters/index.ts +8 -0
  234. package/src/plugins/parental/adapters/kids-adapter.ts +206 -0
  235. package/src/plugins/parental/index.ts +55 -0
  236. package/src/plugins/parental/parental-plugin.ts +759 -0
  237. package/src/plugins/parental/stores/index.ts +7 -0
  238. package/src/plugins/parental/stores/postgres-store.ts +304 -0
  239. package/src/plugins/parental/types.ts +180 -0
  240. package/src/plugins/profiles/__tests__/profiles-plugin.test.ts +321 -0
  241. package/src/plugins/profiles/index.ts +49 -0
  242. package/src/plugins/profiles/profiles-plugin.ts +546 -0
  243. package/src/plugins/profiles/stores/index.ts +9 -0
  244. package/src/plugins/profiles/stores/postgres-store.ts +439 -0
  245. package/src/plugins/profiles/types.ts +338 -0
  246. package/src/plugins/subscriptions/__tests__/subscriptions-plugin.test.ts +404 -0
  247. package/src/plugins/subscriptions/index.ts +51 -0
  248. package/src/plugins/subscriptions/stores/index.ts +9 -0
  249. package/src/plugins/subscriptions/stores/postgres-store.ts +482 -0
  250. package/src/plugins/subscriptions/subscriptions-plugin.ts +530 -0
  251. package/src/plugins/subscriptions/types.ts +355 -0
  252. package/src/plugins/usage/__tests__/usage-plugin.test.ts +288 -0
  253. package/src/plugins/usage/index.ts +39 -0
  254. package/src/plugins/usage/stores/index.ts +9 -0
  255. package/src/plugins/usage/stores/postgres-store.ts +213 -0
  256. package/src/plugins/usage/types.ts +222 -0
  257. package/src/plugins/usage/usage-plugin.ts +484 -0
  258. package/src/plugins/users/__tests__/postgres-store.test.ts +326 -0
  259. package/src/plugins/users/__tests__/users-plugin.test.ts +3 -0
  260. package/src/plugins/users/index.ts +6 -0
  261. package/src/plugins/users/stores/postgres-store.ts +104 -0
  262. package/src/plugins/users/types.ts +82 -6
  263. package/src/plugins/users/users-plugin.ts +37 -0
  264. package/ui/src/App.tsx +5 -1
  265. package/ui/src/api/controlPanelApi.ts +103 -6
  266. package/ui/src/components/StatCard.tsx +58 -0
  267. package/ui/src/dashboard/builtInWidgets.tsx +3 -1
  268. package/ui/src/dashboard/widgets/NotificationsStatsWidget.tsx +167 -0
  269. package/ui/src/dashboard/widgets/index.ts +1 -0
  270. package/ui/src/pages/NotificationsPage.tsx +417 -0
  271. package/ui/src/utils/formatters.ts +33 -0
  272. package/dist-ui/assets/index-D7DoZ9rL.js +0 -478
  273. package/dist-ui/assets/index-D7DoZ9rL.js.map +0 -1
@@ -0,0 +1,412 @@
1
+ /**
2
+ * Usage Plugin
3
+ *
4
+ * Usage tracking plugin for @qwickapps/server.
5
+ * Tracks daily/monthly feature usage and enforces limits.
6
+ *
7
+ * Copyright (c) 2025 QwickApps.com. All rights reserved.
8
+ */
9
+ // Import subscription helpers if available
10
+ let getFeatureLimitFn = null;
11
+ // Store instance for helper access
12
+ let currentStore = null;
13
+ let currentConfig = null;
14
+ let cleanupIntervalId = null;
15
+ /**
16
+ * Get current date in YYYY-MM-DD format
17
+ */
18
+ function getCurrentDate() {
19
+ return new Date().toISOString().split('T')[0];
20
+ }
21
+ /**
22
+ * Get tomorrow's date at midnight (for reset time)
23
+ */
24
+ function getTomorrowMidnight() {
25
+ const tomorrow = new Date();
26
+ tomorrow.setDate(tomorrow.getDate() + 1);
27
+ tomorrow.setHours(0, 0, 0, 0);
28
+ return tomorrow;
29
+ }
30
+ /**
31
+ * Create the Usage plugin
32
+ */
33
+ export function createUsagePlugin(config) {
34
+ const debug = config.debug || false;
35
+ const apiPrefix = config.api?.prefix || '/usage';
36
+ function log(message, data) {
37
+ if (debug) {
38
+ console.log(`[UsagePlugin] ${message}`, data || '');
39
+ }
40
+ }
41
+ return {
42
+ id: 'usage',
43
+ name: 'Usage',
44
+ version: '1.0.0',
45
+ async onStart(_pluginConfig, registry) {
46
+ log('Starting usage plugin');
47
+ // Initialize the store (creates tables if needed)
48
+ await config.store.initialize();
49
+ log('Usage plugin migrations complete');
50
+ // Store references for helper access
51
+ currentStore = config.store;
52
+ currentConfig = config;
53
+ // Try to get the feature limit function from subscriptions plugin
54
+ try {
55
+ const subscriptions = await import('../subscriptions/index.js');
56
+ getFeatureLimitFn = subscriptions.getFeatureLimit;
57
+ log('Subscriptions plugin integration enabled');
58
+ }
59
+ catch {
60
+ log('Subscriptions plugin not available, limits will not be enforced');
61
+ }
62
+ // Register health check
63
+ registry.registerHealthCheck({
64
+ name: 'usage-store',
65
+ type: 'custom',
66
+ check: async () => {
67
+ try {
68
+ // Simple health check
69
+ return { healthy: true };
70
+ }
71
+ catch {
72
+ return { healthy: false };
73
+ }
74
+ },
75
+ });
76
+ // Run cleanup on startup if configured
77
+ if (config.cleanup?.runOnStartup) {
78
+ const dailyDays = config.cleanup.dailyRetentionDays || 90;
79
+ const monthlyMonths = config.cleanup.monthlyRetentionMonths || 24;
80
+ const dailyDeleted = await config.store.cleanupOldDaily(dailyDays);
81
+ const monthlyDeleted = await config.store.cleanupOldMonthly(monthlyMonths);
82
+ log('Startup cleanup complete', { dailyDeleted, monthlyDeleted });
83
+ }
84
+ // Set up periodic cleanup if configured
85
+ if (config.cleanup?.cleanupIntervalHours && config.cleanup.cleanupIntervalHours > 0) {
86
+ const intervalMs = config.cleanup.cleanupIntervalHours * 60 * 60 * 1000;
87
+ cleanupIntervalId = setInterval(async () => {
88
+ try {
89
+ const dailyDays = config.cleanup?.dailyRetentionDays || 90;
90
+ const monthlyMonths = config.cleanup?.monthlyRetentionMonths || 24;
91
+ const dailyDeleted = await config.store.cleanupOldDaily(dailyDays);
92
+ const monthlyDeleted = await config.store.cleanupOldMonthly(monthlyMonths);
93
+ log('Periodic cleanup complete', { dailyDeleted, monthlyDeleted });
94
+ }
95
+ catch (error) {
96
+ console.error('[UsagePlugin] Cleanup error:', error);
97
+ }
98
+ }, intervalMs);
99
+ }
100
+ // Add API routes if enabled
101
+ if (config.api?.enabled !== false) {
102
+ // Get daily usage summary
103
+ registry.addRoute({
104
+ method: 'get',
105
+ path: `${apiPrefix}/user/:userId/daily`,
106
+ pluginId: 'usage',
107
+ handler: async (req, res) => {
108
+ try {
109
+ const { userId } = req.params;
110
+ const date = req.query.date || getCurrentDate();
111
+ const summary = await getDailyUsageSummary(userId, date);
112
+ res.json(summary);
113
+ }
114
+ catch (error) {
115
+ console.error('[UsagePlugin] Get daily usage error:', error);
116
+ res.status(500).json({ error: 'Failed to get daily usage' });
117
+ }
118
+ },
119
+ });
120
+ // Get usage for a specific feature
121
+ registry.addRoute({
122
+ method: 'get',
123
+ path: `${apiPrefix}/user/:userId/feature/:featureCode`,
124
+ pluginId: 'usage',
125
+ handler: async (req, res) => {
126
+ try {
127
+ const { userId, featureCode } = req.params;
128
+ const status = await getFeatureUsageStatus(userId, featureCode);
129
+ res.json(status);
130
+ }
131
+ catch (error) {
132
+ console.error('[UsagePlugin] Get feature usage error:', error);
133
+ res.status(500).json({ error: 'Failed to get feature usage' });
134
+ }
135
+ },
136
+ });
137
+ // Increment usage (with limit check)
138
+ registry.addRoute({
139
+ method: 'post',
140
+ path: `${apiPrefix}/user/:userId/feature/:featureCode/increment`,
141
+ pluginId: 'usage',
142
+ handler: async (req, res) => {
143
+ try {
144
+ const { userId, featureCode } = req.params;
145
+ const amount = parseInt(req.body.amount) || 1;
146
+ const result = await incrementUsage(userId, featureCode, amount);
147
+ res.json(result);
148
+ }
149
+ catch (error) {
150
+ console.error('[UsagePlugin] Increment usage error:', error);
151
+ res.status(500).json({ error: 'Failed to increment usage' });
152
+ }
153
+ },
154
+ });
155
+ // Check if usage is within limit (without incrementing)
156
+ registry.addRoute({
157
+ method: 'get',
158
+ path: `${apiPrefix}/user/:userId/feature/:featureCode/check`,
159
+ pluginId: 'usage',
160
+ handler: async (req, res) => {
161
+ try {
162
+ const { userId, featureCode } = req.params;
163
+ const amount = parseInt(req.query.amount) || 1;
164
+ const result = await checkUsageLimit(userId, featureCode, amount);
165
+ res.json(result);
166
+ }
167
+ catch (error) {
168
+ console.error('[UsagePlugin] Check usage error:', error);
169
+ res.status(500).json({ error: 'Failed to check usage' });
170
+ }
171
+ },
172
+ });
173
+ }
174
+ log('Usage plugin started');
175
+ },
176
+ async onStop() {
177
+ log('Stopping usage plugin');
178
+ // Clear cleanup interval
179
+ if (cleanupIntervalId) {
180
+ clearInterval(cleanupIntervalId);
181
+ cleanupIntervalId = null;
182
+ }
183
+ await config.store.shutdown();
184
+ currentStore = null;
185
+ currentConfig = null;
186
+ log('Usage plugin stopped');
187
+ },
188
+ };
189
+ }
190
+ // ═══════════════════════════════════════════════════════════════════════════
191
+ // Helper Functions
192
+ // ═══════════════════════════════════════════════════════════════════════════
193
+ /**
194
+ * Get the current usage store instance
195
+ */
196
+ export function getUsageStore() {
197
+ return currentStore;
198
+ }
199
+ /**
200
+ * Get current daily usage for a feature
201
+ */
202
+ export async function getDailyUsage(userId, featureCode) {
203
+ if (!currentStore) {
204
+ throw new Error('Usage plugin not initialized');
205
+ }
206
+ const usage = await currentStore.getDailyUsage(userId, featureCode);
207
+ return usage?.count || 0;
208
+ }
209
+ /**
210
+ * Increment usage and check limit
211
+ */
212
+ export async function incrementUsage(userId, featureCode, amount = 1) {
213
+ if (!currentStore) {
214
+ throw new Error('Usage plugin not initialized');
215
+ }
216
+ // Get current usage
217
+ const currentUsage = await getDailyUsage(userId, featureCode);
218
+ // Get limit from subscriptions plugin
219
+ let limit = null;
220
+ if (getFeatureLimitFn) {
221
+ limit = await getFeatureLimitFn(userId, featureCode);
222
+ }
223
+ // Check if increment is allowed
224
+ // limit = -1 means unlimited, null means no subscription/feature disabled
225
+ if (limit === null) {
226
+ return {
227
+ allowed: false,
228
+ current_count: currentUsage,
229
+ limit: null,
230
+ remaining: null,
231
+ reason: 'Feature not available in your subscription',
232
+ };
233
+ }
234
+ if (limit === 0) {
235
+ return {
236
+ allowed: false,
237
+ current_count: currentUsage,
238
+ limit: 0,
239
+ remaining: 0,
240
+ reason: 'Feature is disabled',
241
+ };
242
+ }
243
+ // Check if within limit (-1 = unlimited)
244
+ if (limit !== -1 && currentUsage + amount > limit) {
245
+ return {
246
+ allowed: false,
247
+ current_count: currentUsage,
248
+ limit,
249
+ remaining: Math.max(0, limit - currentUsage),
250
+ resets_at: getTomorrowMidnight(),
251
+ reason: 'Daily limit reached',
252
+ };
253
+ }
254
+ // Increment usage
255
+ const newCount = await currentStore.incrementDaily(userId, featureCode, amount);
256
+ return {
257
+ allowed: true,
258
+ current_count: newCount,
259
+ limit,
260
+ remaining: limit === -1 ? -1 : Math.max(0, limit - newCount),
261
+ resets_at: getTomorrowMidnight(),
262
+ };
263
+ }
264
+ /**
265
+ * Check if usage is within limit (without incrementing)
266
+ */
267
+ export async function checkUsageLimit(userId, featureCode, amount = 1) {
268
+ if (!currentStore) {
269
+ throw new Error('Usage plugin not initialized');
270
+ }
271
+ const currentUsage = await getDailyUsage(userId, featureCode);
272
+ // Get limit from subscriptions plugin
273
+ let limit = null;
274
+ if (getFeatureLimitFn) {
275
+ limit = await getFeatureLimitFn(userId, featureCode);
276
+ }
277
+ if (limit === null) {
278
+ return {
279
+ allowed: false,
280
+ current_count: currentUsage,
281
+ limit: null,
282
+ remaining: null,
283
+ reason: 'Feature not available in your subscription',
284
+ };
285
+ }
286
+ if (limit === 0) {
287
+ return {
288
+ allowed: false,
289
+ current_count: currentUsage,
290
+ limit: 0,
291
+ remaining: 0,
292
+ reason: 'Feature is disabled',
293
+ };
294
+ }
295
+ // Check if within limit
296
+ const wouldExceed = limit !== -1 && currentUsage + amount > limit;
297
+ return {
298
+ allowed: !wouldExceed,
299
+ current_count: currentUsage,
300
+ limit,
301
+ remaining: limit === -1 ? -1 : Math.max(0, limit - currentUsage),
302
+ resets_at: getTomorrowMidnight(),
303
+ reason: wouldExceed ? 'Would exceed daily limit' : undefined,
304
+ };
305
+ }
306
+ /**
307
+ * Get usage status for a specific feature
308
+ */
309
+ export async function getFeatureUsageStatus(userId, featureCode) {
310
+ if (!currentStore) {
311
+ throw new Error('Usage plugin not initialized');
312
+ }
313
+ const currentUsage = await getDailyUsage(userId, featureCode);
314
+ // Get limit from subscriptions plugin
315
+ let limit = null;
316
+ if (getFeatureLimitFn) {
317
+ limit = await getFeatureLimitFn(userId, featureCode);
318
+ }
319
+ let remaining = null;
320
+ let percentageUsed = null;
321
+ if (limit !== null) {
322
+ if (limit === -1) {
323
+ remaining = -1; // Unlimited
324
+ }
325
+ else if (limit > 0) {
326
+ remaining = Math.max(0, limit - currentUsage);
327
+ percentageUsed = Math.min(100, Math.round((currentUsage / limit) * 100));
328
+ }
329
+ else {
330
+ remaining = 0;
331
+ percentageUsed = 100;
332
+ }
333
+ }
334
+ return {
335
+ feature_code: featureCode,
336
+ current: currentUsage,
337
+ limit,
338
+ remaining,
339
+ resets_at: getTomorrowMidnight(),
340
+ percentage_used: percentageUsed,
341
+ };
342
+ }
343
+ /**
344
+ * Get daily usage summary for all features
345
+ */
346
+ export async function getDailyUsageSummary(userId, date) {
347
+ if (!currentStore) {
348
+ throw new Error('Usage plugin not initialized');
349
+ }
350
+ const targetDate = date || getCurrentDate();
351
+ const dailyUsages = await currentStore.getAllDailyUsage(userId, targetDate);
352
+ // Get status for each feature
353
+ const features = await Promise.all(dailyUsages.map(async (usage) => {
354
+ let limit = null;
355
+ if (getFeatureLimitFn) {
356
+ limit = await getFeatureLimitFn(userId, usage.feature_code);
357
+ }
358
+ let remaining = null;
359
+ let percentageUsed = null;
360
+ if (limit !== null) {
361
+ if (limit === -1) {
362
+ remaining = -1;
363
+ }
364
+ else if (limit > 0) {
365
+ remaining = Math.max(0, limit - usage.count);
366
+ percentageUsed = Math.min(100, Math.round((usage.count / limit) * 100));
367
+ }
368
+ else {
369
+ remaining = 0;
370
+ percentageUsed = 100;
371
+ }
372
+ }
373
+ return {
374
+ feature_code: usage.feature_code,
375
+ current: usage.count,
376
+ limit,
377
+ remaining,
378
+ resets_at: getTomorrowMidnight(),
379
+ percentage_used: percentageUsed,
380
+ };
381
+ }));
382
+ return {
383
+ user_id: userId,
384
+ period: 'daily',
385
+ period_value: targetDate,
386
+ features,
387
+ };
388
+ }
389
+ /**
390
+ * Reset usage for a feature (admin function)
391
+ */
392
+ export async function resetUsage(userId, featureCode) {
393
+ if (!currentStore) {
394
+ throw new Error('Usage plugin not initialized');
395
+ }
396
+ await currentStore.resetDailyUsage(userId, featureCode);
397
+ }
398
+ /**
399
+ * Get remaining quota for a feature
400
+ */
401
+ export async function getRemainingQuota(userId, featureCode) {
402
+ const status = await getFeatureUsageStatus(userId, featureCode);
403
+ return status.remaining;
404
+ }
405
+ /**
406
+ * Check if user can use a feature (has remaining quota)
407
+ */
408
+ export async function canUseFeature(userId, featureCode, amount = 1) {
409
+ const result = await checkUsageLimit(userId, featureCode, amount);
410
+ return result.allowed;
411
+ }
412
+ //# sourceMappingURL=usage-plugin.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"usage-plugin.js","sourceRoot":"","sources":["../../../src/plugins/usage/usage-plugin.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,10 @@
1
+ /**
2
+ * PostgreSQL User Store Tests
3
+ *
4
+ * Unit tests for the PostgreSQL user store implementation,
5
+ * focusing on getByIdentifier() and linkIdentifiers() methods.
6
+ *
7
+ * Copyright (c) 2025 QwickApps.com. All rights reserved.
8
+ */
9
+ export {};
10
+ //# sourceMappingURL=postgres-store.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"postgres-store.test.d.ts","sourceRoot":"","sources":["../../../../src/plugins/users/__tests__/postgres-store.test.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG"}
@@ -0,0 +1,229 @@
1
+ /**
2
+ * PostgreSQL User Store Tests
3
+ *
4
+ * Unit tests for the PostgreSQL user store implementation,
5
+ * focusing on getByIdentifier() and linkIdentifiers() methods.
6
+ *
7
+ * Copyright (c) 2025 QwickApps.com. All rights reserved.
8
+ */
9
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
10
+ import { postgresUserStore } from '../stores/postgres-store.js';
11
+ // Mock user data
12
+ const mockUser = {
13
+ id: 'test-user-id-123',
14
+ email: 'test@example.com',
15
+ name: 'Test User',
16
+ external_id: 'auth0|abc123',
17
+ provider: 'auth0',
18
+ picture: 'https://example.com/avatar.jpg',
19
+ metadata: {
20
+ identifiers: {
21
+ auth0_user_id: 'auth0|abc123',
22
+ wp_user_id: 42,
23
+ keap_contact_id: 12345,
24
+ },
25
+ },
26
+ created_at: new Date('2025-01-01'),
27
+ updated_at: new Date('2025-01-01'),
28
+ last_login_at: new Date('2025-12-13'),
29
+ };
30
+ // Mock pg pool
31
+ const createMockPool = () => {
32
+ const mockQuery = vi.fn();
33
+ return {
34
+ query: mockQuery,
35
+ _mockQuery: mockQuery, // Expose for test assertions
36
+ };
37
+ };
38
+ describe('PostgreSQL User Store', () => {
39
+ let mockPool;
40
+ let store;
41
+ beforeEach(() => {
42
+ mockPool = createMockPool();
43
+ store = postgresUserStore({
44
+ pool: mockPool,
45
+ autoCreateTables: false, // Skip table creation in tests
46
+ });
47
+ });
48
+ describe('getByIdentifier()', () => {
49
+ it('should throw error when no identifier is provided (UT-001)', async () => {
50
+ const emptyIdentifiers = {};
51
+ await expect(store.getByIdentifier(emptyIdentifiers)).rejects.toThrow('At least one identifier must be provided');
52
+ // Should not make any DB queries
53
+ expect(mockPool._mockQuery).not.toHaveBeenCalled();
54
+ });
55
+ it('should find user by email first (priority 1) (UT-002)', async () => {
56
+ mockPool._mockQuery.mockResolvedValueOnce({ rows: [mockUser] });
57
+ const identifiers = {
58
+ email: 'test@example.com',
59
+ auth0_user_id: 'auth0|abc123',
60
+ wp_user_id: 42,
61
+ };
62
+ const result = await store.getByIdentifier(identifiers);
63
+ expect(result).toEqual(mockUser);
64
+ expect(mockPool._mockQuery).toHaveBeenCalledTimes(1);
65
+ expect(mockPool._mockQuery).toHaveBeenCalledWith(expect.stringContaining('LOWER(email) = LOWER($1)'), ['test@example.com']);
66
+ });
67
+ it('should find user by auth0_user_id if email not found (priority 2) (UT-003)', async () => {
68
+ // Email query returns no results
69
+ mockPool._mockQuery.mockResolvedValueOnce({ rows: [] });
70
+ // auth0_user_id query in metadata returns user
71
+ mockPool._mockQuery.mockResolvedValueOnce({ rows: [mockUser] });
72
+ const identifiers = {
73
+ email: 'nonexistent@example.com',
74
+ auth0_user_id: 'auth0|abc123',
75
+ };
76
+ const result = await store.getByIdentifier(identifiers);
77
+ expect(result).toEqual(mockUser);
78
+ expect(mockPool._mockQuery).toHaveBeenCalledTimes(2);
79
+ expect(mockPool._mockQuery).toHaveBeenNthCalledWith(2, expect.stringContaining("metadata->'identifiers'->>'auth0_user_id'"), ['auth0|abc123']);
80
+ });
81
+ it('should check legacy external_id for auth0 users (UT-004)', async () => {
82
+ // No email provided, so skip email check
83
+ // auth0_user_id in metadata returns no results
84
+ mockPool._mockQuery.mockResolvedValueOnce({ rows: [] });
85
+ // Legacy external_id query returns user
86
+ mockPool._mockQuery.mockResolvedValueOnce({ rows: [mockUser] });
87
+ const identifiers = {
88
+ auth0_user_id: 'auth0|abc123',
89
+ };
90
+ const result = await store.getByIdentifier(identifiers);
91
+ expect(result).toEqual(mockUser);
92
+ expect(mockPool._mockQuery).toHaveBeenCalledTimes(2);
93
+ expect(mockPool._mockQuery).toHaveBeenNthCalledWith(2, expect.stringContaining('external_id = $1'), ['auth0|abc123']);
94
+ });
95
+ it('should find user by wp_user_id (priority 3) (UT-005)', async () => {
96
+ // All higher priority queries return no results
97
+ mockPool._mockQuery.mockResolvedValueOnce({ rows: [] }); // email
98
+ mockPool._mockQuery.mockResolvedValueOnce({ rows: [] }); // auth0 metadata
99
+ mockPool._mockQuery.mockResolvedValueOnce({ rows: [] }); // auth0 external_id
100
+ mockPool._mockQuery.mockResolvedValueOnce({ rows: [mockUser] }); // wp_user_id
101
+ const identifiers = {
102
+ email: 'nonexistent@example.com',
103
+ auth0_user_id: 'auth0|xyz',
104
+ wp_user_id: 42,
105
+ };
106
+ const result = await store.getByIdentifier(identifiers);
107
+ expect(result).toEqual(mockUser);
108
+ expect(mockPool._mockQuery).toHaveBeenNthCalledWith(4, expect.stringContaining("(metadata->'identifiers'->>'wp_user_id')::int"), [42]);
109
+ });
110
+ it('should find user by keap_contact_id (priority 4) (UT-006)', async () => {
111
+ // All higher priority queries return no results
112
+ mockPool._mockQuery.mockResolvedValueOnce({ rows: [] }); // wp_user_id
113
+ mockPool._mockQuery.mockResolvedValueOnce({ rows: [mockUser] }); // keap_contact_id
114
+ const identifiers = {
115
+ wp_user_id: 999,
116
+ keap_contact_id: 12345,
117
+ };
118
+ const result = await store.getByIdentifier(identifiers);
119
+ expect(result).toEqual(mockUser);
120
+ expect(mockPool._mockQuery).toHaveBeenNthCalledWith(2, expect.stringContaining("(metadata->'identifiers'->>'keap_contact_id')::int"), [12345]);
121
+ });
122
+ it('should return null if no user found by any identifier (UT-007)', async () => {
123
+ // All queries return no results
124
+ mockPool._mockQuery.mockResolvedValue({ rows: [] });
125
+ const identifiers = {
126
+ email: 'nonexistent@example.com',
127
+ auth0_user_id: 'auth0|nonexistent',
128
+ wp_user_id: 99999,
129
+ keap_contact_id: 99999,
130
+ };
131
+ const result = await store.getByIdentifier(identifiers);
132
+ expect(result).toBeNull();
133
+ });
134
+ it('should handle wp_user_id of 0 as valid identifier (UT-008)', async () => {
135
+ // Only wp_user_id provided (no email or auth0_user_id)
136
+ // So only 1 query for wp_user_id should be made
137
+ mockPool._mockQuery.mockResolvedValueOnce({ rows: [mockUser] });
138
+ const identifiers = {
139
+ wp_user_id: 0,
140
+ };
141
+ const result = await store.getByIdentifier(identifiers);
142
+ expect(result).toEqual(mockUser);
143
+ expect(mockPool._mockQuery).toHaveBeenCalledTimes(1);
144
+ expect(mockPool._mockQuery).toHaveBeenCalledWith(expect.stringContaining("(metadata->'identifiers'->>'wp_user_id')::int"), [0]);
145
+ });
146
+ it('should handle keap_contact_id of 0 as valid identifier (UT-009)', async () => {
147
+ // Only keap_contact_id provided (no email, auth0_user_id, or wp_user_id)
148
+ // So only 1 query for keap_contact_id should be made
149
+ mockPool._mockQuery.mockResolvedValueOnce({ rows: [mockUser] });
150
+ const identifiers = {
151
+ keap_contact_id: 0,
152
+ };
153
+ const result = await store.getByIdentifier(identifiers);
154
+ expect(result).toEqual(mockUser);
155
+ expect(mockPool._mockQuery).toHaveBeenCalledTimes(1);
156
+ expect(mockPool._mockQuery).toHaveBeenCalledWith(expect.stringContaining("(metadata->'identifiers'->>'keap_contact_id')::int"), [0]);
157
+ });
158
+ });
159
+ describe('linkIdentifiers()', () => {
160
+ it('should not make DB query when no identifiers provided (UT-010)', async () => {
161
+ await store.linkIdentifiers('user-123', {});
162
+ expect(mockPool._mockQuery).not.toHaveBeenCalled();
163
+ });
164
+ it('should update metadata with single identifier (UT-011)', async () => {
165
+ mockPool._mockQuery.mockResolvedValueOnce({ rows: [], rowCount: 1 });
166
+ await store.linkIdentifiers('user-123', {
167
+ wp_user_id: 42,
168
+ });
169
+ expect(mockPool._mockQuery).toHaveBeenCalledTimes(1);
170
+ expect(mockPool._mockQuery).toHaveBeenCalledWith(expect.stringContaining('jsonb_set'), [JSON.stringify({ wp_user_id: 42 }), 'user-123']);
171
+ });
172
+ it('should update metadata with multiple identifiers (UT-012)', async () => {
173
+ mockPool._mockQuery.mockResolvedValueOnce({ rows: [], rowCount: 1 });
174
+ await store.linkIdentifiers('user-123', {
175
+ wp_user_id: 42,
176
+ auth0_user_id: 'auth0|abc123',
177
+ keap_contact_id: 12345,
178
+ });
179
+ expect(mockPool._mockQuery).toHaveBeenCalledTimes(1);
180
+ const [, args] = mockPool._mockQuery.mock.calls[0];
181
+ const identifiersJson = JSON.parse(args[0]);
182
+ expect(identifiersJson).toEqual({
183
+ wp_user_id: 42,
184
+ auth0_user_id: 'auth0|abc123',
185
+ keap_contact_id: 12345,
186
+ });
187
+ expect(args[1]).toBe('user-123');
188
+ });
189
+ it('should preserve existing identifiers (uses COALESCE) (UT-013)', async () => {
190
+ mockPool._mockQuery.mockResolvedValueOnce({ rows: [], rowCount: 1 });
191
+ await store.linkIdentifiers('user-123', {
192
+ wp_user_id: 42,
193
+ });
194
+ // Verify the query uses COALESCE to preserve existing values
195
+ expect(mockPool._mockQuery).toHaveBeenCalledWith(expect.stringContaining("COALESCE(metadata->'identifiers', '{}'::jsonb)"), expect.any(Array));
196
+ });
197
+ it('should handle undefined values correctly (UT-014)', async () => {
198
+ mockPool._mockQuery.mockResolvedValueOnce({ rows: [], rowCount: 1 });
199
+ await store.linkIdentifiers('user-123', {
200
+ wp_user_id: undefined,
201
+ auth0_user_id: 'auth0|abc123',
202
+ keap_contact_id: undefined,
203
+ });
204
+ expect(mockPool._mockQuery).toHaveBeenCalledTimes(1);
205
+ const [, args] = mockPool._mockQuery.mock.calls[0];
206
+ const identifiersJson = JSON.parse(args[0]);
207
+ // Should only include defined values
208
+ expect(identifiersJson).toEqual({
209
+ auth0_user_id: 'auth0|abc123',
210
+ });
211
+ });
212
+ });
213
+ describe('getByIds()', () => {
214
+ it('should return empty array for empty input (UT-015)', async () => {
215
+ const result = await store.getByIds([]);
216
+ expect(result).toEqual([]);
217
+ expect(mockPool._mockQuery).not.toHaveBeenCalled();
218
+ });
219
+ it('should batch query multiple users (UT-016)', async () => {
220
+ const mockUsers = [mockUser, { ...mockUser, id: 'user-2' }];
221
+ mockPool._mockQuery.mockResolvedValueOnce({ rows: mockUsers });
222
+ const result = await store.getByIds(['user-1', 'user-2']);
223
+ expect(result).toEqual(mockUsers);
224
+ expect(mockPool._mockQuery).toHaveBeenCalledTimes(1);
225
+ expect(mockPool._mockQuery).toHaveBeenCalledWith(expect.stringContaining('id = ANY($1)'), [['user-1', 'user-2']]);
226
+ });
227
+ });
228
+ });
229
+ //# sourceMappingURL=postgres-store.test.js.map