@qwickapps/server 1.4.0 → 1.5.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.
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +12 -2
- package/dist/index.js.map +1 -1
- package/dist/plugins/bans/bans-plugin.d.ts.map +1 -1
- package/dist/plugins/bans/bans-plugin.js +12 -3
- package/dist/plugins/bans/bans-plugin.js.map +1 -1
- package/dist/plugins/devices/__tests__/devices-plugin.test.d.ts +11 -0
- package/dist/plugins/devices/__tests__/devices-plugin.test.d.ts.map +1 -0
- package/dist/plugins/devices/__tests__/devices-plugin.test.js +410 -0
- package/dist/plugins/devices/__tests__/devices-plugin.test.js.map +1 -0
- package/dist/plugins/devices/__tests__/token-utils.test.d.ts +7 -0
- package/dist/plugins/devices/__tests__/token-utils.test.d.ts.map +1 -0
- package/dist/plugins/devices/__tests__/token-utils.test.js +197 -0
- package/dist/plugins/devices/__tests__/token-utils.test.js.map +1 -0
- package/dist/plugins/devices/adapters/compute-adapter.d.ts +36 -0
- package/dist/plugins/devices/adapters/compute-adapter.d.ts.map +1 -0
- package/dist/plugins/devices/adapters/compute-adapter.js +100 -0
- package/dist/plugins/devices/adapters/compute-adapter.js.map +1 -0
- package/dist/plugins/devices/adapters/index.d.ts +12 -0
- package/dist/plugins/devices/adapters/index.d.ts.map +1 -0
- package/dist/plugins/devices/adapters/index.js +10 -0
- package/dist/plugins/devices/adapters/index.js.map +1 -0
- package/dist/plugins/devices/adapters/mobile-adapter.d.ts +41 -0
- package/dist/plugins/devices/adapters/mobile-adapter.d.ts.map +1 -0
- package/dist/plugins/devices/adapters/mobile-adapter.js +131 -0
- package/dist/plugins/devices/adapters/mobile-adapter.js.map +1 -0
- package/dist/plugins/devices/devices-plugin.d.ts +70 -0
- package/dist/plugins/devices/devices-plugin.d.ts.map +1 -0
- package/dist/plugins/devices/devices-plugin.js +453 -0
- package/dist/plugins/devices/devices-plugin.js.map +1 -0
- package/dist/plugins/devices/index.d.ts +18 -0
- package/dist/plugins/devices/index.d.ts.map +1 -0
- package/dist/plugins/devices/index.js +18 -0
- package/dist/plugins/devices/index.js.map +1 -0
- package/dist/plugins/devices/stores/index.d.ts +9 -0
- package/dist/plugins/devices/stores/index.d.ts.map +1 -0
- package/dist/plugins/devices/stores/index.js +9 -0
- package/dist/plugins/devices/stores/index.js.map +1 -0
- package/dist/plugins/devices/stores/postgres-store.d.ts +26 -0
- package/dist/plugins/devices/stores/postgres-store.d.ts.map +1 -0
- package/dist/plugins/devices/stores/postgres-store.js +199 -0
- package/dist/plugins/devices/stores/postgres-store.js.map +1 -0
- package/dist/plugins/devices/token-utils.d.ts +100 -0
- package/dist/plugins/devices/token-utils.d.ts.map +1 -0
- package/dist/plugins/devices/token-utils.js +162 -0
- package/dist/plugins/devices/token-utils.js.map +1 -0
- package/dist/plugins/devices/types.d.ts +307 -0
- package/dist/plugins/devices/types.d.ts.map +1 -0
- package/dist/plugins/devices/types.js +10 -0
- package/dist/plugins/devices/types.js.map +1 -0
- package/dist/plugins/index.d.ts +14 -2
- package/dist/plugins/index.d.ts.map +1 -1
- package/dist/plugins/index.js +13 -1
- package/dist/plugins/index.js.map +1 -1
- package/dist/plugins/notifications/__tests__/notifications-manager.test.d.ts +5 -0
- package/dist/plugins/notifications/__tests__/notifications-manager.test.d.ts.map +1 -0
- package/dist/plugins/notifications/__tests__/notifications-manager.test.js +470 -0
- package/dist/plugins/notifications/__tests__/notifications-manager.test.js.map +1 -0
- package/dist/plugins/notifications/index.d.ts +71 -0
- package/dist/plugins/notifications/index.d.ts.map +1 -0
- package/dist/plugins/notifications/index.js +72 -0
- package/dist/plugins/notifications/index.js.map +1 -0
- package/dist/plugins/notifications/notifications-manager.d.ts +182 -0
- package/dist/plugins/notifications/notifications-manager.d.ts.map +1 -0
- package/dist/plugins/notifications/notifications-manager.js +610 -0
- package/dist/plugins/notifications/notifications-manager.js.map +1 -0
- package/dist/plugins/notifications/notifications-plugin.d.ts +83 -0
- package/dist/plugins/notifications/notifications-plugin.d.ts.map +1 -0
- package/dist/plugins/notifications/notifications-plugin.js +337 -0
- package/dist/plugins/notifications/notifications-plugin.js.map +1 -0
- package/dist/plugins/notifications/types.d.ts +164 -0
- package/dist/plugins/notifications/types.d.ts.map +1 -0
- package/dist/plugins/notifications/types.js +9 -0
- package/dist/plugins/notifications/types.js.map +1 -0
- package/dist/plugins/parental/__tests__/parental-plugin.test.d.ts +12 -0
- package/dist/plugins/parental/__tests__/parental-plugin.test.d.ts.map +1 -0
- package/dist/plugins/parental/__tests__/parental-plugin.test.js +349 -0
- package/dist/plugins/parental/__tests__/parental-plugin.test.js.map +1 -0
- package/dist/plugins/parental/adapters/index.d.ts +8 -0
- package/dist/plugins/parental/adapters/index.d.ts.map +1 -0
- package/dist/plugins/parental/adapters/index.js +7 -0
- package/dist/plugins/parental/adapters/index.js.map +1 -0
- package/dist/plugins/parental/adapters/kids-adapter.d.ts +24 -0
- package/dist/plugins/parental/adapters/kids-adapter.d.ts.map +1 -0
- package/dist/plugins/parental/adapters/kids-adapter.js +174 -0
- package/dist/plugins/parental/adapters/kids-adapter.js.map +1 -0
- package/dist/plugins/parental/index.d.ts +14 -0
- package/dist/plugins/parental/index.d.ts.map +1 -0
- package/dist/plugins/parental/index.js +15 -0
- package/dist/plugins/parental/index.js.map +1 -0
- package/dist/plugins/parental/parental-plugin.d.ts +88 -0
- package/dist/plugins/parental/parental-plugin.d.ts.map +1 -0
- package/dist/plugins/parental/parental-plugin.js +666 -0
- package/dist/plugins/parental/parental-plugin.js.map +1 -0
- package/dist/plugins/parental/stores/index.d.ts +7 -0
- package/dist/plugins/parental/stores/index.d.ts.map +1 -0
- package/dist/plugins/parental/stores/index.js +7 -0
- package/dist/plugins/parental/stores/index.js.map +1 -0
- package/dist/plugins/parental/stores/postgres-store.d.ts +10 -0
- package/dist/plugins/parental/stores/postgres-store.d.ts.map +1 -0
- package/dist/plugins/parental/stores/postgres-store.js +209 -0
- package/dist/plugins/parental/stores/postgres-store.js.map +1 -0
- package/dist/plugins/parental/types.d.ts +154 -0
- package/dist/plugins/parental/types.d.ts.map +1 -0
- package/dist/plugins/parental/types.js +10 -0
- package/dist/plugins/parental/types.js.map +1 -0
- package/dist/plugins/profiles/__tests__/profiles-plugin.test.d.ts +11 -0
- package/dist/plugins/profiles/__tests__/profiles-plugin.test.d.ts.map +1 -0
- package/dist/plugins/profiles/__tests__/profiles-plugin.test.js +243 -0
- package/dist/plugins/profiles/__tests__/profiles-plugin.test.js.map +1 -0
- package/dist/plugins/profiles/index.d.ts +12 -0
- package/dist/plugins/profiles/index.d.ts.map +1 -0
- package/dist/plugins/profiles/index.js +13 -0
- package/dist/plugins/profiles/index.js.map +1 -0
- package/dist/plugins/profiles/profiles-plugin.d.ts +71 -0
- package/dist/plugins/profiles/profiles-plugin.d.ts.map +1 -0
- package/dist/plugins/profiles/profiles-plugin.js +481 -0
- package/dist/plugins/profiles/profiles-plugin.js.map +1 -0
- package/dist/plugins/profiles/stores/index.d.ts +9 -0
- package/dist/plugins/profiles/stores/index.d.ts.map +1 -0
- package/dist/plugins/profiles/stores/index.js +9 -0
- package/dist/plugins/profiles/stores/index.js.map +1 -0
- package/dist/plugins/profiles/stores/postgres-store.d.ts +18 -0
- package/dist/plugins/profiles/stores/postgres-store.d.ts.map +1 -0
- package/dist/plugins/profiles/stores/postgres-store.js +310 -0
- package/dist/plugins/profiles/stores/postgres-store.js.map +1 -0
- package/dist/plugins/profiles/types.d.ts +289 -0
- package/dist/plugins/profiles/types.d.ts.map +1 -0
- package/dist/plugins/profiles/types.js +10 -0
- package/dist/plugins/profiles/types.js.map +1 -0
- package/dist/plugins/subscriptions/__tests__/subscriptions-plugin.test.d.ts +11 -0
- package/dist/plugins/subscriptions/__tests__/subscriptions-plugin.test.d.ts.map +1 -0
- package/dist/plugins/subscriptions/__tests__/subscriptions-plugin.test.js +305 -0
- package/dist/plugins/subscriptions/__tests__/subscriptions-plugin.test.js.map +1 -0
- package/dist/plugins/subscriptions/index.d.ts +12 -0
- package/dist/plugins/subscriptions/index.d.ts.map +1 -0
- package/dist/plugins/subscriptions/index.js +13 -0
- package/dist/plugins/subscriptions/index.js.map +1 -0
- package/dist/plugins/subscriptions/stores/index.d.ts +9 -0
- package/dist/plugins/subscriptions/stores/index.d.ts.map +1 -0
- package/dist/plugins/subscriptions/stores/index.js +9 -0
- package/dist/plugins/subscriptions/stores/index.js.map +1 -0
- package/dist/plugins/subscriptions/stores/postgres-store.d.ts +14 -0
- package/dist/plugins/subscriptions/stores/postgres-store.d.ts.map +1 -0
- package/dist/plugins/subscriptions/stores/postgres-store.js +359 -0
- package/dist/plugins/subscriptions/stores/postgres-store.js.map +1 -0
- package/dist/plugins/subscriptions/subscriptions-plugin.d.ts +82 -0
- package/dist/plugins/subscriptions/subscriptions-plugin.d.ts.map +1 -0
- package/dist/plugins/subscriptions/subscriptions-plugin.js +449 -0
- package/dist/plugins/subscriptions/subscriptions-plugin.js.map +1 -0
- package/dist/plugins/subscriptions/types.d.ts +308 -0
- package/dist/plugins/subscriptions/types.d.ts.map +1 -0
- package/dist/plugins/subscriptions/types.js +10 -0
- package/dist/plugins/subscriptions/types.js.map +1 -0
- package/dist/plugins/usage/__tests__/usage-plugin.test.d.ts +11 -0
- package/dist/plugins/usage/__tests__/usage-plugin.test.d.ts.map +1 -0
- package/dist/plugins/usage/__tests__/usage-plugin.test.js +218 -0
- package/dist/plugins/usage/__tests__/usage-plugin.test.js.map +1 -0
- package/dist/plugins/usage/index.d.ts +12 -0
- package/dist/plugins/usage/index.d.ts.map +1 -0
- package/dist/plugins/usage/index.js +13 -0
- package/dist/plugins/usage/index.js.map +1 -0
- package/dist/plugins/usage/stores/index.d.ts +9 -0
- package/dist/plugins/usage/stores/index.d.ts.map +1 -0
- package/dist/plugins/usage/stores/index.js +9 -0
- package/dist/plugins/usage/stores/index.js.map +1 -0
- package/dist/plugins/usage/stores/postgres-store.d.ts +14 -0
- package/dist/plugins/usage/stores/postgres-store.d.ts.map +1 -0
- package/dist/plugins/usage/stores/postgres-store.js +146 -0
- package/dist/plugins/usage/stores/postgres-store.js.map +1 -0
- package/dist/plugins/usage/types.d.ts +195 -0
- package/dist/plugins/usage/types.d.ts.map +1 -0
- package/dist/plugins/usage/types.js +10 -0
- package/dist/plugins/usage/types.js.map +1 -0
- package/dist/plugins/usage/usage-plugin.d.ts +51 -0
- package/dist/plugins/usage/usage-plugin.d.ts.map +1 -0
- package/dist/plugins/usage/usage-plugin.js +412 -0
- package/dist/plugins/usage/usage-plugin.js.map +1 -0
- package/dist/plugins/users/__tests__/postgres-store.test.d.ts +10 -0
- package/dist/plugins/users/__tests__/postgres-store.test.d.ts.map +1 -0
- package/dist/plugins/users/__tests__/postgres-store.test.js +229 -0
- package/dist/plugins/users/__tests__/postgres-store.test.js.map +1 -0
- package/dist/plugins/users/__tests__/users-plugin.test.js +3 -0
- package/dist/plugins/users/__tests__/users-plugin.test.js.map +1 -1
- 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/stores/postgres-store.d.ts.map +1 -1
- package/dist/plugins/users/stores/postgres-store.js +76 -0
- package/dist/plugins/users/stores/postgres-store.js.map +1 -1
- package/dist/plugins/users/types.d.ts +74 -6
- package/dist/plugins/users/types.d.ts.map +1 -1
- package/dist/plugins/users/users-plugin.d.ts +15 -1
- package/dist/plugins/users/users-plugin.d.ts.map +1 -1
- package/dist/plugins/users/users-plugin.js +29 -0
- package/dist/plugins/users/users-plugin.js.map +1 -1
- package/dist-ui/assets/index-CynOqPkb.js +469 -0
- package/dist-ui/assets/index-CynOqPkb.js.map +1 -0
- package/dist-ui/index.html +1 -1
- package/dist-ui-lib/api/controlPanelApi.d.ts +46 -0
- package/dist-ui-lib/components/StatCard.d.ts +16 -0
- package/dist-ui-lib/dashboard/widgets/NotificationsStatsWidget.d.ts +12 -0
- package/dist-ui-lib/dashboard/widgets/index.d.ts +1 -0
- package/dist-ui-lib/index.js +1822 -1611
- package/dist-ui-lib/index.js.map +1 -1
- package/dist-ui-lib/pages/NotificationsPage.d.ts +9 -0
- package/dist-ui-lib/utils/formatters.d.ts +19 -0
- package/package.json +1 -1
- package/src/index.ts +178 -0
- package/src/plugins/bans/bans-plugin.ts +15 -3
- package/src/plugins/devices/__tests__/devices-plugin.test.ts +551 -0
- package/src/plugins/devices/__tests__/token-utils.test.ts +264 -0
- package/src/plugins/devices/adapters/compute-adapter.ts +139 -0
- package/src/plugins/devices/adapters/index.ts +13 -0
- package/src/plugins/devices/adapters/mobile-adapter.ts +179 -0
- package/src/plugins/devices/devices-plugin.ts +538 -0
- package/src/plugins/devices/index.ts +69 -0
- package/src/plugins/devices/stores/index.ts +9 -0
- package/src/plugins/devices/stores/postgres-store.ts +304 -0
- package/src/plugins/devices/token-utils.ts +213 -0
- package/src/plugins/devices/types.ts +351 -0
- package/src/plugins/index.ts +218 -0
- package/src/plugins/notifications/__tests__/notifications-manager.test.ts +637 -0
- package/src/plugins/notifications/index.ts +91 -0
- package/src/plugins/notifications/notifications-manager.ts +773 -0
- package/src/plugins/notifications/notifications-plugin.ts +398 -0
- package/src/plugins/notifications/types.ts +207 -0
- package/src/plugins/parental/__tests__/parental-plugin.test.ts +465 -0
- package/src/plugins/parental/adapters/index.ts +8 -0
- package/src/plugins/parental/adapters/kids-adapter.ts +206 -0
- package/src/plugins/parental/index.ts +55 -0
- package/src/plugins/parental/parental-plugin.ts +759 -0
- package/src/plugins/parental/stores/index.ts +7 -0
- package/src/plugins/parental/stores/postgres-store.ts +304 -0
- package/src/plugins/parental/types.ts +180 -0
- package/src/plugins/profiles/__tests__/profiles-plugin.test.ts +321 -0
- package/src/plugins/profiles/index.ts +49 -0
- package/src/plugins/profiles/profiles-plugin.ts +546 -0
- package/src/plugins/profiles/stores/index.ts +9 -0
- package/src/plugins/profiles/stores/postgres-store.ts +439 -0
- package/src/plugins/profiles/types.ts +338 -0
- package/src/plugins/subscriptions/__tests__/subscriptions-plugin.test.ts +404 -0
- package/src/plugins/subscriptions/index.ts +51 -0
- package/src/plugins/subscriptions/stores/index.ts +9 -0
- package/src/plugins/subscriptions/stores/postgres-store.ts +482 -0
- package/src/plugins/subscriptions/subscriptions-plugin.ts +530 -0
- package/src/plugins/subscriptions/types.ts +355 -0
- package/src/plugins/usage/__tests__/usage-plugin.test.ts +288 -0
- package/src/plugins/usage/index.ts +39 -0
- package/src/plugins/usage/stores/index.ts +9 -0
- package/src/plugins/usage/stores/postgres-store.ts +213 -0
- package/src/plugins/usage/types.ts +222 -0
- package/src/plugins/usage/usage-plugin.ts +484 -0
- package/src/plugins/users/__tests__/postgres-store.test.ts +326 -0
- package/src/plugins/users/__tests__/users-plugin.test.ts +3 -0
- package/src/plugins/users/index.ts +6 -0
- package/src/plugins/users/stores/postgres-store.ts +104 -0
- package/src/plugins/users/types.ts +82 -6
- package/src/plugins/users/users-plugin.ts +37 -0
- package/ui/src/App.tsx +5 -1
- package/ui/src/api/controlPanelApi.ts +103 -6
- package/ui/src/components/StatCard.tsx +58 -0
- package/ui/src/dashboard/builtInWidgets.tsx +3 -1
- package/ui/src/dashboard/widgets/NotificationsStatsWidget.tsx +167 -0
- package/ui/src/dashboard/widgets/index.ts +1 -0
- package/ui/src/pages/NotificationsPage.tsx +417 -0
- package/ui/src/utils/formatters.ts +33 -0
- package/dist-ui/assets/index-D7DoZ9rL.js +0 -478
- package/dist-ui/assets/index-D7DoZ9rL.js.map +0 -1
|
@@ -0,0 +1,439 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PostgreSQL Profile Store
|
|
3
|
+
*
|
|
4
|
+
* Profile storage implementation using PostgreSQL.
|
|
5
|
+
* Includes automatic age group calculation.
|
|
6
|
+
*
|
|
7
|
+
* Copyright (c) 2025 QwickApps.com. All rights reserved.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type {
|
|
11
|
+
ProfileStore,
|
|
12
|
+
Profile,
|
|
13
|
+
CreateProfileInput,
|
|
14
|
+
UpdateProfileInput,
|
|
15
|
+
ProfileSearchParams,
|
|
16
|
+
ProfileListResponse,
|
|
17
|
+
PostgresProfileStoreConfig,
|
|
18
|
+
AgeGroup,
|
|
19
|
+
AgeThresholds,
|
|
20
|
+
} from '../types.js';
|
|
21
|
+
|
|
22
|
+
// Pool interface (from pg package)
|
|
23
|
+
interface PgPool {
|
|
24
|
+
query(text: string, values?: unknown[]): Promise<{ rows: unknown[]; rowCount: number | null }>;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Default age thresholds
|
|
28
|
+
const DEFAULT_AGE_THRESHOLDS: AgeThresholds = {
|
|
29
|
+
child: 12,
|
|
30
|
+
teen: 17,
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Calculate age group from birth date or age
|
|
35
|
+
*/
|
|
36
|
+
function calculateAgeGroup(
|
|
37
|
+
birthDate?: Date | null,
|
|
38
|
+
age?: number | null,
|
|
39
|
+
thresholds: AgeThresholds = DEFAULT_AGE_THRESHOLDS
|
|
40
|
+
): AgeGroup | null {
|
|
41
|
+
let calculatedAge: number | null = null;
|
|
42
|
+
|
|
43
|
+
if (birthDate) {
|
|
44
|
+
const today = new Date();
|
|
45
|
+
const birth = new Date(birthDate);
|
|
46
|
+
calculatedAge = today.getFullYear() - birth.getFullYear();
|
|
47
|
+
const monthDiff = today.getMonth() - birth.getMonth();
|
|
48
|
+
if (monthDiff < 0 || (monthDiff === 0 && today.getDate() < birth.getDate())) {
|
|
49
|
+
calculatedAge--;
|
|
50
|
+
}
|
|
51
|
+
} else if (age !== undefined && age !== null) {
|
|
52
|
+
calculatedAge = age;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (calculatedAge === null) {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (calculatedAge <= thresholds.child) {
|
|
60
|
+
return 'child';
|
|
61
|
+
} else if (calculatedAge <= thresholds.teen) {
|
|
62
|
+
return 'teen';
|
|
63
|
+
} else {
|
|
64
|
+
return 'adult';
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Create a PostgreSQL profile store
|
|
70
|
+
*
|
|
71
|
+
* @param config Configuration including a pg Pool instance
|
|
72
|
+
* @param ageThresholds Optional age thresholds for categorization
|
|
73
|
+
* @returns ProfileStore implementation
|
|
74
|
+
*/
|
|
75
|
+
export function postgresProfileStore(
|
|
76
|
+
config: PostgresProfileStoreConfig,
|
|
77
|
+
ageThresholds: AgeThresholds = DEFAULT_AGE_THRESHOLDS
|
|
78
|
+
): ProfileStore {
|
|
79
|
+
const {
|
|
80
|
+
pool: poolOrFn,
|
|
81
|
+
tableName = 'profiles',
|
|
82
|
+
schema = 'public',
|
|
83
|
+
autoCreateTables = true,
|
|
84
|
+
} = config;
|
|
85
|
+
|
|
86
|
+
// Helper to get pool (supports lazy initialization via function)
|
|
87
|
+
const getPool = (): PgPool => {
|
|
88
|
+
const pool = typeof poolOrFn === 'function' ? poolOrFn() : poolOrFn;
|
|
89
|
+
return pool as PgPool;
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const profilesTableFull = `"${schema}"."${tableName}"`;
|
|
93
|
+
|
|
94
|
+
return {
|
|
95
|
+
name: 'postgres',
|
|
96
|
+
|
|
97
|
+
async initialize(): Promise<void> {
|
|
98
|
+
if (!autoCreateTables) return;
|
|
99
|
+
|
|
100
|
+
// Create profiles table
|
|
101
|
+
await getPool().query(`
|
|
102
|
+
CREATE TABLE IF NOT EXISTS ${profilesTableFull} (
|
|
103
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
104
|
+
org_id UUID,
|
|
105
|
+
user_id UUID NOT NULL,
|
|
106
|
+
name VARCHAR(100) NOT NULL,
|
|
107
|
+
avatar VARCHAR(255),
|
|
108
|
+
|
|
109
|
+
-- Age-based features
|
|
110
|
+
birth_date DATE,
|
|
111
|
+
age INTEGER,
|
|
112
|
+
age_group VARCHAR(20) CHECK (age_group IN ('child', 'teen', 'adult')),
|
|
113
|
+
|
|
114
|
+
-- Content/access control
|
|
115
|
+
content_filter_level VARCHAR(20) DEFAULT 'moderate'
|
|
116
|
+
CHECK (content_filter_level IN ('strict', 'moderate', 'minimal', 'none')),
|
|
117
|
+
|
|
118
|
+
-- Time restrictions
|
|
119
|
+
daily_time_limit_minutes INTEGER,
|
|
120
|
+
allowed_hours_start TIME,
|
|
121
|
+
allowed_hours_end TIME,
|
|
122
|
+
|
|
123
|
+
-- Status
|
|
124
|
+
is_active BOOLEAN DEFAULT true,
|
|
125
|
+
is_default BOOLEAN DEFAULT false,
|
|
126
|
+
|
|
127
|
+
-- Extensibility
|
|
128
|
+
metadata JSONB DEFAULT '{}',
|
|
129
|
+
|
|
130
|
+
created_at TIMESTAMPTZ DEFAULT NOW(),
|
|
131
|
+
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
|
132
|
+
deleted_at TIMESTAMPTZ
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
CREATE INDEX IF NOT EXISTS idx_${tableName}_org ON ${profilesTableFull}(org_id) WHERE deleted_at IS NULL;
|
|
136
|
+
CREATE INDEX IF NOT EXISTS idx_${tableName}_user ON ${profilesTableFull}(user_id) WHERE deleted_at IS NULL;
|
|
137
|
+
CREATE INDEX IF NOT EXISTS idx_${tableName}_age_group ON ${profilesTableFull}(age_group) WHERE age_group IS NOT NULL AND deleted_at IS NULL;
|
|
138
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_${tableName}_default ON ${profilesTableFull}(user_id) WHERE is_default = true AND deleted_at IS NULL;
|
|
139
|
+
`);
|
|
140
|
+
},
|
|
141
|
+
|
|
142
|
+
async getById(id: string): Promise<Profile | null> {
|
|
143
|
+
const result = await getPool().query(
|
|
144
|
+
`SELECT * FROM ${profilesTableFull} WHERE id = $1 AND deleted_at IS NULL`,
|
|
145
|
+
[id]
|
|
146
|
+
);
|
|
147
|
+
return (result.rows[0] as Profile) || null;
|
|
148
|
+
},
|
|
149
|
+
|
|
150
|
+
async create(input: CreateProfileInput): Promise<Profile> {
|
|
151
|
+
// Calculate age group
|
|
152
|
+
const ageGroup = calculateAgeGroup(input.birth_date, input.age, ageThresholds);
|
|
153
|
+
|
|
154
|
+
// If this is set as default, unset other defaults first
|
|
155
|
+
if (input.is_default) {
|
|
156
|
+
await getPool().query(
|
|
157
|
+
`UPDATE ${profilesTableFull} SET is_default = false, updated_at = NOW()
|
|
158
|
+
WHERE user_id = $1 AND is_default = true AND deleted_at IS NULL`,
|
|
159
|
+
[input.user_id]
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const result = await getPool().query(
|
|
164
|
+
`INSERT INTO ${profilesTableFull}
|
|
165
|
+
(org_id, user_id, name, avatar, birth_date, age, age_group,
|
|
166
|
+
content_filter_level, daily_time_limit_minutes,
|
|
167
|
+
allowed_hours_start, allowed_hours_end, is_default, metadata)
|
|
168
|
+
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13)
|
|
169
|
+
RETURNING *`,
|
|
170
|
+
[
|
|
171
|
+
input.org_id || null,
|
|
172
|
+
input.user_id,
|
|
173
|
+
input.name,
|
|
174
|
+
input.avatar || null,
|
|
175
|
+
input.birth_date || null,
|
|
176
|
+
input.age || null,
|
|
177
|
+
ageGroup,
|
|
178
|
+
input.content_filter_level || 'moderate',
|
|
179
|
+
input.daily_time_limit_minutes || null,
|
|
180
|
+
input.allowed_hours_start || null,
|
|
181
|
+
input.allowed_hours_end || null,
|
|
182
|
+
input.is_default || false,
|
|
183
|
+
JSON.stringify(input.metadata || {}),
|
|
184
|
+
]
|
|
185
|
+
);
|
|
186
|
+
return result.rows[0] as Profile;
|
|
187
|
+
},
|
|
188
|
+
|
|
189
|
+
async update(id: string, input: UpdateProfileInput): Promise<Profile | null> {
|
|
190
|
+
// First get the current profile to get user_id
|
|
191
|
+
const current = await this.getById(id);
|
|
192
|
+
if (!current) {
|
|
193
|
+
return null;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const updates: string[] = [];
|
|
197
|
+
const values: unknown[] = [];
|
|
198
|
+
let paramIndex = 1;
|
|
199
|
+
|
|
200
|
+
// Handle is_default first (needs to unset others)
|
|
201
|
+
if (input.is_default === true) {
|
|
202
|
+
await getPool().query(
|
|
203
|
+
`UPDATE ${profilesTableFull} SET is_default = false, updated_at = NOW()
|
|
204
|
+
WHERE user_id = $1 AND id != $2 AND is_default = true AND deleted_at IS NULL`,
|
|
205
|
+
[current.user_id, id]
|
|
206
|
+
);
|
|
207
|
+
updates.push(`is_default = $${paramIndex++}`);
|
|
208
|
+
values.push(true);
|
|
209
|
+
} else if (input.is_default === false) {
|
|
210
|
+
updates.push(`is_default = $${paramIndex++}`);
|
|
211
|
+
values.push(false);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (input.name !== undefined) {
|
|
215
|
+
updates.push(`name = $${paramIndex++}`);
|
|
216
|
+
values.push(input.name);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (input.avatar !== undefined) {
|
|
220
|
+
updates.push(`avatar = $${paramIndex++}`);
|
|
221
|
+
values.push(input.avatar);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Handle birth_date (null means clear it)
|
|
225
|
+
if (input.birth_date !== undefined) {
|
|
226
|
+
updates.push(`birth_date = $${paramIndex++}`);
|
|
227
|
+
values.push(input.birth_date);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Handle age (null means clear it)
|
|
231
|
+
if (input.age !== undefined) {
|
|
232
|
+
updates.push(`age = $${paramIndex++}`);
|
|
233
|
+
values.push(input.age);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Recalculate age group if birth_date or age changed
|
|
237
|
+
if (input.birth_date !== undefined || input.age !== undefined) {
|
|
238
|
+
const newBirthDate = input.birth_date !== undefined ? input.birth_date : current.birth_date;
|
|
239
|
+
const newAge = input.age !== undefined ? input.age : current.age;
|
|
240
|
+
const ageGroup = calculateAgeGroup(newBirthDate, newAge, ageThresholds);
|
|
241
|
+
updates.push(`age_group = $${paramIndex++}`);
|
|
242
|
+
values.push(ageGroup);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
if (input.content_filter_level !== undefined) {
|
|
246
|
+
updates.push(`content_filter_level = $${paramIndex++}`);
|
|
247
|
+
values.push(input.content_filter_level);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
if (input.daily_time_limit_minutes !== undefined) {
|
|
251
|
+
updates.push(`daily_time_limit_minutes = $${paramIndex++}`);
|
|
252
|
+
values.push(input.daily_time_limit_minutes);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
if (input.allowed_hours_start !== undefined) {
|
|
256
|
+
updates.push(`allowed_hours_start = $${paramIndex++}`);
|
|
257
|
+
values.push(input.allowed_hours_start);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
if (input.allowed_hours_end !== undefined) {
|
|
261
|
+
updates.push(`allowed_hours_end = $${paramIndex++}`);
|
|
262
|
+
values.push(input.allowed_hours_end);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
if (input.is_active !== undefined) {
|
|
266
|
+
updates.push(`is_active = $${paramIndex++}`);
|
|
267
|
+
values.push(input.is_active);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
if (input.metadata !== undefined) {
|
|
271
|
+
updates.push(`metadata = $${paramIndex++}`);
|
|
272
|
+
values.push(JSON.stringify(input.metadata));
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
if (updates.length === 0) {
|
|
276
|
+
return current;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
updates.push(`updated_at = NOW()`);
|
|
280
|
+
values.push(id);
|
|
281
|
+
|
|
282
|
+
const result = await getPool().query(
|
|
283
|
+
`UPDATE ${profilesTableFull}
|
|
284
|
+
SET ${updates.join(', ')}
|
|
285
|
+
WHERE id = $${paramIndex} AND deleted_at IS NULL
|
|
286
|
+
RETURNING *`,
|
|
287
|
+
values
|
|
288
|
+
);
|
|
289
|
+
return (result.rows[0] as Profile) || null;
|
|
290
|
+
},
|
|
291
|
+
|
|
292
|
+
async delete(id: string): Promise<boolean> {
|
|
293
|
+
// Soft delete
|
|
294
|
+
const result = await getPool().query(
|
|
295
|
+
`UPDATE ${profilesTableFull}
|
|
296
|
+
SET deleted_at = NOW(), is_active = false, is_default = false, updated_at = NOW()
|
|
297
|
+
WHERE id = $1 AND deleted_at IS NULL`,
|
|
298
|
+
[id]
|
|
299
|
+
);
|
|
300
|
+
return (result.rowCount ?? 0) > 0;
|
|
301
|
+
},
|
|
302
|
+
|
|
303
|
+
async search(params: ProfileSearchParams): Promise<ProfileListResponse> {
|
|
304
|
+
const {
|
|
305
|
+
org_id,
|
|
306
|
+
user_id,
|
|
307
|
+
age_group,
|
|
308
|
+
is_active,
|
|
309
|
+
query,
|
|
310
|
+
page = 1,
|
|
311
|
+
limit = 20,
|
|
312
|
+
sortBy = 'created_at',
|
|
313
|
+
sortOrder = 'desc',
|
|
314
|
+
} = params;
|
|
315
|
+
|
|
316
|
+
const conditions: string[] = ['deleted_at IS NULL'];
|
|
317
|
+
const values: unknown[] = [];
|
|
318
|
+
let paramIndex = 1;
|
|
319
|
+
|
|
320
|
+
if (org_id) {
|
|
321
|
+
conditions.push(`org_id = $${paramIndex++}`);
|
|
322
|
+
values.push(org_id);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
if (user_id) {
|
|
326
|
+
conditions.push(`user_id = $${paramIndex++}`);
|
|
327
|
+
values.push(user_id);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
if (age_group) {
|
|
331
|
+
conditions.push(`age_group = $${paramIndex++}`);
|
|
332
|
+
values.push(age_group);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
if (is_active !== undefined) {
|
|
336
|
+
conditions.push(`is_active = $${paramIndex++}`);
|
|
337
|
+
values.push(is_active);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
if (query) {
|
|
341
|
+
conditions.push(`LOWER(name) LIKE $${paramIndex++}`);
|
|
342
|
+
values.push(`%${query.toLowerCase()}%`);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
const whereClause = `WHERE ${conditions.join(' AND ')}`;
|
|
346
|
+
|
|
347
|
+
// Validate sort column to prevent SQL injection
|
|
348
|
+
const validSortColumns = ['name', 'created_at', 'age'];
|
|
349
|
+
const sortColumn = validSortColumns.includes(sortBy) ? sortBy : 'created_at';
|
|
350
|
+
const sortDir = sortOrder === 'asc' ? 'ASC' : 'DESC';
|
|
351
|
+
|
|
352
|
+
const offset = (page - 1) * limit;
|
|
353
|
+
|
|
354
|
+
// Get total count
|
|
355
|
+
const countResult = await getPool().query(
|
|
356
|
+
`SELECT COUNT(*) FROM ${profilesTableFull} ${whereClause}`,
|
|
357
|
+
values
|
|
358
|
+
);
|
|
359
|
+
const total = parseInt((countResult.rows[0] as { count: string }).count, 10);
|
|
360
|
+
|
|
361
|
+
// Get profiles
|
|
362
|
+
const result = await getPool().query(
|
|
363
|
+
`SELECT * FROM ${profilesTableFull} ${whereClause}
|
|
364
|
+
ORDER BY ${sortColumn} ${sortDir}
|
|
365
|
+
LIMIT $${paramIndex} OFFSET $${paramIndex + 1}`,
|
|
366
|
+
[...values, limit, offset]
|
|
367
|
+
);
|
|
368
|
+
|
|
369
|
+
return {
|
|
370
|
+
profiles: result.rows as Profile[],
|
|
371
|
+
total,
|
|
372
|
+
page,
|
|
373
|
+
limit,
|
|
374
|
+
totalPages: Math.ceil(total / limit),
|
|
375
|
+
};
|
|
376
|
+
},
|
|
377
|
+
|
|
378
|
+
async listByUser(userId: string): Promise<Profile[]> {
|
|
379
|
+
const result = await getPool().query(
|
|
380
|
+
`SELECT * FROM ${profilesTableFull}
|
|
381
|
+
WHERE user_id = $1 AND deleted_at IS NULL
|
|
382
|
+
ORDER BY is_default DESC, created_at ASC`,
|
|
383
|
+
[userId]
|
|
384
|
+
);
|
|
385
|
+
return result.rows as Profile[];
|
|
386
|
+
},
|
|
387
|
+
|
|
388
|
+
async getDefaultProfile(userId: string): Promise<Profile | null> {
|
|
389
|
+
const result = await getPool().query(
|
|
390
|
+
`SELECT * FROM ${profilesTableFull}
|
|
391
|
+
WHERE user_id = $1 AND is_default = true AND deleted_at IS NULL`,
|
|
392
|
+
[userId]
|
|
393
|
+
);
|
|
394
|
+
return (result.rows[0] as Profile) || null;
|
|
395
|
+
},
|
|
396
|
+
|
|
397
|
+
async getProfileCount(userId: string): Promise<number> {
|
|
398
|
+
const result = await getPool().query(
|
|
399
|
+
`SELECT COUNT(*) FROM ${profilesTableFull}
|
|
400
|
+
WHERE user_id = $1 AND deleted_at IS NULL`,
|
|
401
|
+
[userId]
|
|
402
|
+
);
|
|
403
|
+
const row = result.rows[0] as { count: string } | undefined;
|
|
404
|
+
return row ? parseInt(row.count, 10) : 0;
|
|
405
|
+
},
|
|
406
|
+
|
|
407
|
+
async getByAgeGroup(userId: string, ageGroup: AgeGroup): Promise<Profile[]> {
|
|
408
|
+
const result = await getPool().query(
|
|
409
|
+
`SELECT * FROM ${profilesTableFull}
|
|
410
|
+
WHERE user_id = $1 AND age_group = $2 AND deleted_at IS NULL
|
|
411
|
+
ORDER BY created_at ASC`,
|
|
412
|
+
[userId, ageGroup]
|
|
413
|
+
);
|
|
414
|
+
return result.rows as Profile[];
|
|
415
|
+
},
|
|
416
|
+
|
|
417
|
+
async setDefaultProfile(profileId: string, userId: string): Promise<boolean> {
|
|
418
|
+
// Unset current default
|
|
419
|
+
await getPool().query(
|
|
420
|
+
`UPDATE ${profilesTableFull} SET is_default = false, updated_at = NOW()
|
|
421
|
+
WHERE user_id = $1 AND is_default = true AND deleted_at IS NULL`,
|
|
422
|
+
[userId]
|
|
423
|
+
);
|
|
424
|
+
|
|
425
|
+
// Set new default
|
|
426
|
+
const result = await getPool().query(
|
|
427
|
+
`UPDATE ${profilesTableFull} SET is_default = true, updated_at = NOW()
|
|
428
|
+
WHERE id = $1 AND user_id = $2 AND deleted_at IS NULL`,
|
|
429
|
+
[profileId, userId]
|
|
430
|
+
);
|
|
431
|
+
|
|
432
|
+
return (result.rowCount ?? 0) > 0;
|
|
433
|
+
},
|
|
434
|
+
|
|
435
|
+
async shutdown(): Promise<void> {
|
|
436
|
+
// Pool is managed externally, nothing to do here
|
|
437
|
+
},
|
|
438
|
+
};
|
|
439
|
+
}
|