feat: Implement GraphQL API with optimized dashboard queries
Implemented complete GraphQL API with Apollo Server for efficient data fetching: Backend Changes: - Installed @nestjs/graphql@13.2.0, @nestjs/apollo@13.2.1, graphql@16.11.0, dataloader@2.2.3 - Configured Apollo Server with auto schema generation (src/schema.gql) - GraphQL Playground enabled in non-production environments - JWT authentication via GqlAuthGuard - Custom error formatting GraphQL Types (src/graphql/types/): - UserType with family relationships - ChildType with birthDate, gender, photoUrl - FamilyMemberType with role and user relation - ActivityGQLType with startedAt, endedAt, metadata - DashboardType aggregating all dashboard data - DailySummaryType with activity counts and totals - Enum types: ActivityType, FamilyRole, Gender, FeedingMethod, DiaperType Dashboard Resolver (src/graphql/resolvers/dashboard.resolver.ts): - Query: dashboard(childId?: ID) returns DashboardType - Single optimized query replacing 4+ REST API calls: * GET /api/v1/children * GET /api/v1/tracking/child/:id/recent * GET /api/v1/tracking/child/:id/summary/today * GET /api/v1/families/:id/members - Aggregates children, activities, family members, summaries in one query - ResolveField decorators for child and logger relations - Calculates daily summary (feeding, sleep, diaper, medication counts) - Uses Between for date range filtering - Handles metadata extraction for activity details DataLoader Implementation (src/graphql/dataloaders/): - ChildDataLoader: batchChildren, batchChildrenByFamily - UserDataLoader: batchUsers - REQUEST scope for per-request instance - Prevents N+1 query problem when resolving relations - Uses TypeORM In() for batch loading GraphQL Module (src/graphql/graphql.module.ts): - Exports ChildDataLoader and UserDataLoader - TypeORM integration with Child, Activity, FamilyMember, User entities - DashboardResolver provider Example Queries (src/graphql/example-queries.gql): - GetDashboard with childId parameter - GetDashboardAllChildren for listing - Documented usage and expected results Files Created (11 total): - src/graphql/types/ (5 files) - src/graphql/dataloaders/ (2 files) - src/graphql/resolvers/ (1 file) - src/graphql/guards/ (1 file) - src/graphql/graphql.module.ts - src/graphql/example-queries.gql Performance Improvements: - Dashboard load reduced from 4+ REST calls to 1 GraphQL query - DataLoader batching eliminates N+1 queries - Client can request only needed fields - Reduced network overhead and latency Usage: - Endpoint: http://localhost:3020/graphql - Playground: http://localhost:3020/graphql (dev only) - Authentication: JWT token in Authorization header 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -86,7 +86,7 @@ This document identifies features specified in the documentation that are not ye
|
||||
5. **Security Hardening** - CORS configuration, comprehensive input validation, XSS headers
|
||||
|
||||
**Medium Priority (Post-Launch)**:
|
||||
1. **GraphQL API** - Complex queries for dashboard optimization
|
||||
1. ~~**GraphQL API**~~ - ✅ COMPLETED (October 2, 2025) - Complex queries for dashboard optimization with DataLoader for N+1 prevention
|
||||
2. **Voice Processing** - Whisper API integration, multi-language voice recognition
|
||||
3. **Analytics & Predictions** - Pattern detection, ML-based next event predictions
|
||||
4. **PWA Features** - Service worker configuration, offline pages, install prompts
|
||||
@@ -159,27 +159,75 @@ This document identifies features specified in the documentation that are not ye
|
||||
- Priority: High
|
||||
- Impact: Account security and COPPA compliance
|
||||
|
||||
### 1.2 GraphQL Implementation (MEDIUM Priority)
|
||||
### 1.2 GraphQL Implementation ✅ COMPLETED (October 2, 2025)
|
||||
|
||||
**Source**: `maternal-app-api-spec.md`, `maternal-app-tech-stack.md`
|
||||
|
||||
1. **GraphQL Endpoint**
|
||||
- Status: Dependencies installed (@nestjs/graphql) but not configured
|
||||
- Current: REST API only
|
||||
- Needed: GraphQL endpoint at /graphql with schema
|
||||
- Priority: Medium
|
||||
- Impact: Efficient complex data fetching for dashboard
|
||||
1. **GraphQL Endpoint** ✅ COMPLETED
|
||||
- Status: **IMPLEMENTED**
|
||||
- Current: GraphQL endpoint at /graphql with auto-generated schema
|
||||
- Implemented:
|
||||
* **Apollo Server Integration** (app.module.ts:35-57):
|
||||
- ApolloDriver with @nestjs/apollo@13.2.1
|
||||
- Auto schema generation at src/schema.gql
|
||||
- GraphQL Playground enabled in non-production
|
||||
- JWT authentication via GqlAuthGuard
|
||||
- Custom error formatting
|
||||
* **GraphQL Types** (src/graphql/types/):
|
||||
- UserType, ChildType, FamilyMemberType, ActivityGQLType
|
||||
- DashboardType with DailySummaryType
|
||||
- Enum types: ActivityType, FamilyRole, Gender, FeedingMethod, DiaperType
|
||||
- All types with proper Field decorators
|
||||
* **Dashboard Resolver** (dashboard.resolver.ts):
|
||||
- Query: dashboard(childId?: ID) returns DashboardType
|
||||
- Single optimized query replacing 4+ REST endpoints
|
||||
- Aggregates children, activities, family members, summaries
|
||||
- ResolveField for child and logger relations
|
||||
- Calculates daily summary (feeding, sleep, diaper, medication counts)
|
||||
* **DataLoader for N+1 Prevention** (src/graphql/dataloaders/):
|
||||
- ChildDataLoader: batchChildren, batchChildrenByFamily
|
||||
- UserDataLoader: batchUsers
|
||||
- REQUEST scope for per-request batching
|
||||
- Prevents N+1 query problem for relations
|
||||
* **GraphQL Module** (graphql.module.ts):
|
||||
- Exports DataLoaders for dependency injection
|
||||
- TypeORM integration with Child, Activity, FamilyMember, User
|
||||
- DashboardResolver provider
|
||||
- Files Created:
|
||||
* src/graphql/types/ (5 files: user, child, family, activity, dashboard)
|
||||
* src/graphql/dataloaders/ (2 files: child, user)
|
||||
* src/graphql/resolvers/ (1 file: dashboard)
|
||||
* src/graphql/guards/ (1 file: gql-auth)
|
||||
* src/graphql/graphql.module.ts
|
||||
* src/graphql/example-queries.gql
|
||||
- Example Query:
|
||||
```graphql
|
||||
query GetDashboard($childId: ID) {
|
||||
dashboard(childId: $childId) {
|
||||
children { id name birthDate }
|
||||
selectedChild { id name }
|
||||
recentActivities { id type startedAt logger { name } }
|
||||
todaySummary { feedingCount sleepCount diaperCount }
|
||||
familyMembers { userId role user { name } }
|
||||
totalChildren
|
||||
totalActivitiesToday
|
||||
}
|
||||
}
|
||||
```
|
||||
- Performance: Single query replaces 4+ REST calls
|
||||
- Priority: Medium ✅ **COMPLETE**
|
||||
- Impact: Dashboard load time reduced, efficient data fetching
|
||||
|
||||
2. **GraphQL Subscriptions**
|
||||
- Status: Not implemented
|
||||
- Current: WebSocket for real-time sync
|
||||
- Current: WebSocket for real-time sync (Socket.io)
|
||||
- Needed: GraphQL subscriptions for real-time data
|
||||
- Priority: Low
|
||||
- Priority: Low (deferred - Socket.io working well)
|
||||
- Impact: Alternative real-time implementation
|
||||
|
||||
3. **Complex Dashboard Queries**
|
||||
- Status: Not implemented
|
||||
- Current: Multiple REST calls for dashboard data
|
||||
3. **Complex Dashboard Queries** ✅ COMPLETED
|
||||
- Status: **IMPLEMENTED via GraphQL dashboard query**
|
||||
- Current: Single GraphQL query aggregates all dashboard data
|
||||
- Needed: Single GraphQL query for entire dashboard
|
||||
- Priority: Medium
|
||||
- Impact: Performance optimization, reduced API calls
|
||||
|
||||
496
maternal-app/maternal-app-backend/package-lock.json
generated
496
maternal-app/maternal-app-backend/package-lock.json
generated
@@ -15,6 +15,7 @@
|
||||
"@aws-sdk/s3-request-presigner": "^3.899.0",
|
||||
"@langchain/core": "^0.3.78",
|
||||
"@langchain/openai": "^0.6.14",
|
||||
"@nestjs/apollo": "^13.2.1",
|
||||
"@nestjs/common": "^11.1.6",
|
||||
"@nestjs/config": "^4.0.2",
|
||||
"@nestjs/core": "^11.1.6",
|
||||
@@ -39,6 +40,8 @@
|
||||
"cache-manager-redis-yet": "^5.1.5",
|
||||
"class-transformer": "^0.5.1",
|
||||
"class-validator": "^0.14.2",
|
||||
"dataloader": "^2.2.3",
|
||||
"date-fns": "^4.1.0",
|
||||
"dotenv": "^17.2.3",
|
||||
"form-data": "^4.0.4",
|
||||
"graphql": "^16.11.0",
|
||||
@@ -66,7 +69,7 @@
|
||||
"devDependencies": {
|
||||
"@nestjs/cli": "^10.0.0",
|
||||
"@nestjs/schematics": "^10.0.0",
|
||||
"@nestjs/testing": "^10.0.0",
|
||||
"@nestjs/testing": "^11.1.6",
|
||||
"@types/bcrypt": "^6.0.0",
|
||||
"@types/express": "^5.0.0",
|
||||
"@types/jest": "^29.5.2",
|
||||
@@ -119,6 +122,30 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@angular-devkit/core/node_modules/ajv": {
|
||||
"version": "8.12.0",
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz",
|
||||
"integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fast-deep-equal": "^3.1.1",
|
||||
"json-schema-traverse": "^1.0.0",
|
||||
"require-from-string": "^2.0.2",
|
||||
"uri-js": "^4.2.2"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/epoberezkin"
|
||||
}
|
||||
},
|
||||
"node_modules/@angular-devkit/core/node_modules/json-schema-traverse": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
|
||||
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@angular-devkit/core/node_modules/rxjs": {
|
||||
"version": "7.8.1",
|
||||
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz",
|
||||
@@ -334,6 +361,22 @@
|
||||
"graphql": "14.x || 15.x || 16.x"
|
||||
}
|
||||
},
|
||||
"node_modules/@apollo/server-plugin-landing-page-graphql-playground": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@apollo/server-plugin-landing-page-graphql-playground/-/server-plugin-landing-page-graphql-playground-4.0.1.tgz",
|
||||
"integrity": "sha512-tWhQzD7DtiTO/wfbGvasryz7eJSuEh9XJHgRTMZI7+Wu/omylG5gH6K6ksg1Vccg8/Xuglfi2f1M5Nm/IlBBGw==",
|
||||
"deprecated": "The use of GraphQL Playground in Apollo Server was supported in previous versions, but this is no longer the case as of December 31, 2022. This package exists for v4 migration purposes only. We do not intend to resolve security issues or other bugs with this package if they arise, so please migrate away from this to [Apollo Server's default Explorer](https://www.apollographql.com/docs/apollo-server/api/plugin/landing-pages) as soon as possible.",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@apollographql/graphql-playground-html": "1.6.29"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@apollo/server": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@apollo/server/node_modules/uuid": {
|
||||
"version": "11.1.0",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz",
|
||||
@@ -501,6 +544,15 @@
|
||||
"node": ">=16"
|
||||
}
|
||||
},
|
||||
"node_modules/@apollographql/graphql-playground-html": {
|
||||
"version": "1.6.29",
|
||||
"resolved": "https://registry.npmjs.org/@apollographql/graphql-playground-html/-/graphql-playground-html-1.6.29.tgz",
|
||||
"integrity": "sha512-xCcXpoz52rI4ksJSdOCxeOCn2DLocxwHf9dVT/Q90Pte1LX+LY+91SFtJF3KXVHH8kEin+g1KKCQPKBjZJfWNA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"xss": "^1.0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/@aws-crypto/crc32": {
|
||||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@aws-crypto/crc32/-/crc32-5.2.0.tgz",
|
||||
@@ -1996,7 +2048,7 @@
|
||||
"version": "0.8.1",
|
||||
"resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz",
|
||||
"integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/trace-mapping": "0.3.9"
|
||||
@@ -2009,7 +2061,7 @@
|
||||
"version": "0.3.9",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz",
|
||||
"integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/resolve-uri": "^3.0.3",
|
||||
@@ -2141,23 +2193,6 @@
|
||||
"url": "https://opencollective.com/eslint"
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint/eslintrc/node_modules/ajv": {
|
||||
"version": "6.12.6",
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
|
||||
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fast-deep-equal": "^3.1.1",
|
||||
"fast-json-stable-stringify": "^2.0.0",
|
||||
"json-schema-traverse": "^0.4.1",
|
||||
"uri-js": "^4.2.2"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/epoberezkin"
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint/eslintrc/node_modules/brace-expansion": {
|
||||
"version": "1.1.12",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
|
||||
@@ -2192,13 +2227,6 @@
|
||||
"node": ">= 4"
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint/eslintrc/node_modules/json-schema-traverse": {
|
||||
"version": "0.4.1",
|
||||
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
|
||||
"integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@eslint/eslintrc/node_modules/minimatch": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
||||
@@ -3378,7 +3406,7 @@
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
|
||||
"integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
@@ -3399,7 +3427,7 @@
|
||||
"version": "1.5.5",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
|
||||
"integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@jridgewell/trace-mapping": {
|
||||
@@ -3560,6 +3588,42 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/@nestjs/apollo": {
|
||||
"version": "13.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@nestjs/apollo/-/apollo-13.2.1.tgz",
|
||||
"integrity": "sha512-BJPNw8xqs4DfdEEmjaAbI6cIJsHouWjcZN70BKTPl8rZcw4Tf61RonqFRn0F/rr/aiccWGAuXJuWY4dPsgah4Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@apollo/server-plugin-landing-page-graphql-playground": "4.0.1",
|
||||
"iterall": "1.3.0",
|
||||
"lodash.omit": "4.5.0",
|
||||
"tslib": "2.8.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@apollo/gateway": "^2.0.0",
|
||||
"@apollo/server": "^5.0.0",
|
||||
"@apollo/subgraph": "^2.0.0",
|
||||
"@as-integrations/fastify": "^2.1.1 || ^3.0.0",
|
||||
"@nestjs/common": "^11.0.1",
|
||||
"@nestjs/core": "^11.0.1",
|
||||
"@nestjs/graphql": "^13.0.0",
|
||||
"graphql": "^16.10.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@apollo/gateway": {
|
||||
"optional": true
|
||||
},
|
||||
"@apollo/subgraph": {
|
||||
"optional": true
|
||||
},
|
||||
"@as-integrations/express5": {
|
||||
"optional": true
|
||||
},
|
||||
"@as-integrations/fastify": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@nestjs/cli": {
|
||||
"version": "10.4.9",
|
||||
"resolved": "https://registry.npmjs.org/@nestjs/cli/-/cli-10.4.9.tgz",
|
||||
@@ -4091,9 +4155,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@nestjs/testing": {
|
||||
"version": "10.4.20",
|
||||
"resolved": "https://registry.npmjs.org/@nestjs/testing/-/testing-10.4.20.tgz",
|
||||
"integrity": "sha512-nMkRDukDKskdPruM6EsgMq7yJua+CPZM6I6FrLP8yXw8BiVSPv9Nm0CtcGGwt3kgZF9hfxKjGqLjsvVBsv6Vfw==",
|
||||
"version": "11.1.6",
|
||||
"resolved": "https://registry.npmjs.org/@nestjs/testing/-/testing-11.1.6.tgz",
|
||||
"integrity": "sha512-srYzzDNxGvVCe1j0SpTS9/ix75PKt6Sn6iMaH1rpJ6nj2g8vwNrhK0CoJJXvpCYgrnI+2WES2pprYnq8rAMYHA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -4104,10 +4168,10 @@
|
||||
"url": "https://opencollective.com/nest"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@nestjs/common": "^10.0.0",
|
||||
"@nestjs/core": "^10.0.0",
|
||||
"@nestjs/microservices": "^10.0.0",
|
||||
"@nestjs/platform-express": "^10.0.0"
|
||||
"@nestjs/common": "^11.0.0",
|
||||
"@nestjs/core": "^11.0.0",
|
||||
"@nestjs/microservices": "^11.0.0",
|
||||
"@nestjs/platform-express": "^11.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@nestjs/microservices": {
|
||||
@@ -5059,30 +5123,26 @@
|
||||
"integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/@redis/bloom": {
|
||||
"version": "5.8.3",
|
||||
"resolved": "https://registry.npmjs.org/@redis/bloom/-/bloom-5.8.3.tgz",
|
||||
"integrity": "sha512-1eldTzHvdW3Oi0TReb8m1yiFt8ZwyF6rv1NpZyG5R4TpCwuAdKQetBKoCw7D96tNFgsVVd6eL+NaGZZCqhRg4g==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@redis/client": "^5.8.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@redis/client": {
|
||||
"version": "5.8.3",
|
||||
"resolved": "https://registry.npmjs.org/@redis/client/-/client-5.8.3.tgz",
|
||||
"integrity": "sha512-MZVUE+l7LmMIYlIjubPosruJ9ltSLGFmJqsXApTqPLyHLjsJUSAbAJb/A3N34fEqean4ddiDkdWzNu4ZKPvRUg==",
|
||||
"version": "1.6.1",
|
||||
"resolved": "https://registry.npmjs.org/@redis/client/-/client-1.6.1.tgz",
|
||||
"integrity": "sha512-/KCsg3xSlR+nCK8/8ZYSknYxvXHwubJrU82F3Lm1Fp6789VQ0/3RJKfsmRXjqfaTA++23CvC3hqmqe/2GEt6Kw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cluster-key-slot": "1.1.2"
|
||||
"cluster-key-slot": "1.1.2",
|
||||
"generic-pool": "3.9.0",
|
||||
"yallist": "4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18"
|
||||
"node": ">=14"
|
||||
}
|
||||
},
|
||||
"node_modules/@redis/client/node_modules/yallist": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
|
||||
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/@redis/graph": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@redis/graph/-/graph-1.1.1.tgz",
|
||||
@@ -5092,42 +5152,6 @@
|
||||
"@redis/client": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@redis/json": {
|
||||
"version": "5.8.3",
|
||||
"resolved": "https://registry.npmjs.org/@redis/json/-/json-5.8.3.tgz",
|
||||
"integrity": "sha512-DRR09fy/u8gynHGJ4gzXYeM7D8nlS6EMv5o+h20ndTJiAc7RGR01fdk2FNjnn1Nz5PjgGGownF+s72bYG4nZKQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@redis/client": "^5.8.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@redis/search": {
|
||||
"version": "5.8.3",
|
||||
"resolved": "https://registry.npmjs.org/@redis/search/-/search-5.8.3.tgz",
|
||||
"integrity": "sha512-EMIvEeGRR2I0BJEz4PV88DyCuPmMT1rDtznlsHY3cKSDcc9vj0Q411jUnX0iU2vVowUgWn/cpySKjpXdZ8m+5g==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@redis/client": "^5.8.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@redis/time-series": {
|
||||
"version": "5.8.3",
|
||||
"resolved": "https://registry.npmjs.org/@redis/time-series/-/time-series-5.8.3.tgz",
|
||||
"integrity": "sha512-5Jwy3ilsUYQjzpE7WZ1lEeG1RkqQ5kHtwV1p8yxXHSEmyUbC/T/AVgyjMcm52Olj/Ov/mhDKjx6ndYUi14bXsw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@redis/client": "^5.8.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@sentry-internal/node-cpu-profiler": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry-internal/node-cpu-profiler/-/node-cpu-profiler-2.2.0.tgz",
|
||||
@@ -6083,28 +6107,28 @@
|
||||
"version": "1.0.11",
|
||||
"resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz",
|
||||
"integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@tsconfig/node12": {
|
||||
"version": "1.0.11",
|
||||
"resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz",
|
||||
"integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@tsconfig/node14": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz",
|
||||
"integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@tsconfig/node16": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz",
|
||||
"integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/babel__core": {
|
||||
@@ -7054,7 +7078,7 @@
|
||||
"version": "8.3.4",
|
||||
"resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz",
|
||||
"integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"acorn": "^8.11.0"
|
||||
@@ -7064,15 +7088,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/ajv": {
|
||||
"version": "8.12.0",
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz",
|
||||
"integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==",
|
||||
"version": "6.12.6",
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
|
||||
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fast-deep-equal": "^3.1.1",
|
||||
"json-schema-traverse": "^1.0.0",
|
||||
"require-from-string": "^2.0.2",
|
||||
"fast-json-stable-stringify": "^2.0.0",
|
||||
"json-schema-traverse": "^0.4.1",
|
||||
"uri-js": "^4.2.2"
|
||||
},
|
||||
"funding": {
|
||||
@@ -7098,6 +7122,30 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/ajv-formats/node_modules/ajv": {
|
||||
"version": "8.17.1",
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz",
|
||||
"integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
"fast-uri": "^3.0.1",
|
||||
"json-schema-traverse": "^1.0.0",
|
||||
"require-from-string": "^2.0.2"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/epoberezkin"
|
||||
}
|
||||
},
|
||||
"node_modules/ajv-formats/node_modules/json-schema-traverse": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
|
||||
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/ajv-keywords": {
|
||||
"version": "3.5.2",
|
||||
"resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz",
|
||||
@@ -7226,7 +7274,7 @@
|
||||
"version": "4.1.3",
|
||||
"resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz",
|
||||
"integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/argparse": {
|
||||
@@ -7809,20 +7857,6 @@
|
||||
"@redis/client": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/cache-manager-redis-yet/node_modules/@redis/client": {
|
||||
"version": "1.6.1",
|
||||
"resolved": "https://registry.npmjs.org/@redis/client/-/client-1.6.1.tgz",
|
||||
"integrity": "sha512-/KCsg3xSlR+nCK8/8ZYSknYxvXHwubJrU82F3Lm1Fp6789VQ0/3RJKfsmRXjqfaTA++23CvC3hqmqe/2GEt6Kw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cluster-key-slot": "1.1.2",
|
||||
"generic-pool": "3.9.0",
|
||||
"yallist": "4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
}
|
||||
},
|
||||
"node_modules/cache-manager-redis-yet/node_modules/@redis/json": {
|
||||
"version": "1.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@redis/json/-/json-1.0.7.tgz",
|
||||
@@ -7894,12 +7928,6 @@
|
||||
"@redis/time-series": "1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/cache-manager-redis-yet/node_modules/yallist": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
|
||||
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/cache-manager/node_modules/keyv": {
|
||||
"version": "5.5.3",
|
||||
"resolved": "https://registry.npmjs.org/keyv/-/keyv-5.5.3.tgz",
|
||||
@@ -8435,7 +8463,7 @@
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz",
|
||||
"integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/cron": {
|
||||
@@ -8483,6 +8511,28 @@
|
||||
"integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/cssfilter": {
|
||||
"version": "0.0.10",
|
||||
"resolved": "https://registry.npmjs.org/cssfilter/-/cssfilter-0.0.10.tgz",
|
||||
"integrity": "sha512-FAaLDaplstoRsDR8XGYH51znUN0UY7nMc6Z9/fvE8EXGwvJE9hu7W2vHwx1+bd6gCYnln9nLbzxFTrcO9YQDZw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/dataloader": {
|
||||
"version": "2.2.3",
|
||||
"resolved": "https://registry.npmjs.org/dataloader/-/dataloader-2.2.3.tgz",
|
||||
"integrity": "sha512-y2krtASINtPFS1rSDjacrFgn1dcUuoREVabwlOGOe4SdxenREqwjwjElAdwvbGM7kgZz9a3KVicWR7vcz8rnzA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/date-fns": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz",
|
||||
"integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/kossnocorp"
|
||||
}
|
||||
},
|
||||
"node_modules/dayjs": {
|
||||
"version": "1.11.18",
|
||||
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.18.tgz",
|
||||
@@ -8643,7 +8693,7 @@
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz",
|
||||
"integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"engines": {
|
||||
"node": ">=0.3.1"
|
||||
@@ -9101,23 +9151,6 @@
|
||||
"url": "https://opencollective.com/eslint"
|
||||
}
|
||||
},
|
||||
"node_modules/eslint/node_modules/ajv": {
|
||||
"version": "6.12.6",
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
|
||||
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fast-deep-equal": "^3.1.1",
|
||||
"fast-json-stable-stringify": "^2.0.0",
|
||||
"json-schema-traverse": "^0.4.1",
|
||||
"uri-js": "^4.2.2"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/epoberezkin"
|
||||
}
|
||||
},
|
||||
"node_modules/eslint/node_modules/brace-expansion": {
|
||||
"version": "1.1.12",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
|
||||
@@ -9165,13 +9198,6 @@
|
||||
"node": ">= 4"
|
||||
}
|
||||
},
|
||||
"node_modules/eslint/node_modules/json-schema-traverse": {
|
||||
"version": "0.4.1",
|
||||
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
|
||||
"integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/eslint/node_modules/minimatch": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
||||
@@ -9421,6 +9447,23 @@
|
||||
"integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/fast-uri": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz",
|
||||
"integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/fastify"
|
||||
},
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/fastify"
|
||||
}
|
||||
],
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/fast-xml-parser": {
|
||||
"version": "5.2.5",
|
||||
"resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.2.5.tgz",
|
||||
@@ -10056,7 +10099,7 @@
|
||||
"version": "4.7.8",
|
||||
"resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz",
|
||||
"integrity": "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"minimist": "^1.2.5",
|
||||
@@ -10078,7 +10121,7 @@
|
||||
"version": "0.6.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
|
||||
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
@@ -11414,9 +11457,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/json-schema-traverse": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
|
||||
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
|
||||
"version": "0.4.1",
|
||||
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
|
||||
"integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
@@ -11863,6 +11906,13 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.omit": {
|
||||
"version": "4.5.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.omit/-/lodash.omit-4.5.0.tgz",
|
||||
"integrity": "sha512-XeqSp49hNGmlkj2EJlfrQFIzQ6lXdNro9sddtQzcJY8QaoC2GO0DT7xaIokHeyM+mIT0mPMlPvkYzg2xCuHdZg==",
|
||||
"deprecated": "This package is deprecated. Use destructuring assignment syntax instead.",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.once": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz",
|
||||
@@ -11976,7 +12026,7 @@
|
||||
"version": "1.3.6",
|
||||
"resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz",
|
||||
"integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/makeerror": {
|
||||
@@ -12213,7 +12263,7 @@
|
||||
"version": "2.6.2",
|
||||
"resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz",
|
||||
"integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/node-abi": {
|
||||
@@ -13472,6 +13522,66 @@
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/redis/node_modules/@redis/bloom": {
|
||||
"version": "5.8.3",
|
||||
"resolved": "https://registry.npmjs.org/@redis/bloom/-/bloom-5.8.3.tgz",
|
||||
"integrity": "sha512-1eldTzHvdW3Oi0TReb8m1yiFt8ZwyF6rv1NpZyG5R4TpCwuAdKQetBKoCw7D96tNFgsVVd6eL+NaGZZCqhRg4g==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@redis/client": "^5.8.3"
|
||||
}
|
||||
},
|
||||
"node_modules/redis/node_modules/@redis/client": {
|
||||
"version": "5.8.3",
|
||||
"resolved": "https://registry.npmjs.org/@redis/client/-/client-5.8.3.tgz",
|
||||
"integrity": "sha512-MZVUE+l7LmMIYlIjubPosruJ9ltSLGFmJqsXApTqPLyHLjsJUSAbAJb/A3N34fEqean4ddiDkdWzNu4ZKPvRUg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cluster-key-slot": "1.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18"
|
||||
}
|
||||
},
|
||||
"node_modules/redis/node_modules/@redis/json": {
|
||||
"version": "5.8.3",
|
||||
"resolved": "https://registry.npmjs.org/@redis/json/-/json-5.8.3.tgz",
|
||||
"integrity": "sha512-DRR09fy/u8gynHGJ4gzXYeM7D8nlS6EMv5o+h20ndTJiAc7RGR01fdk2FNjnn1Nz5PjgGGownF+s72bYG4nZKQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@redis/client": "^5.8.3"
|
||||
}
|
||||
},
|
||||
"node_modules/redis/node_modules/@redis/search": {
|
||||
"version": "5.8.3",
|
||||
"resolved": "https://registry.npmjs.org/@redis/search/-/search-5.8.3.tgz",
|
||||
"integrity": "sha512-EMIvEeGRR2I0BJEz4PV88DyCuPmMT1rDtznlsHY3cKSDcc9vj0Q411jUnX0iU2vVowUgWn/cpySKjpXdZ8m+5g==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@redis/client": "^5.8.3"
|
||||
}
|
||||
},
|
||||
"node_modules/redis/node_modules/@redis/time-series": {
|
||||
"version": "5.8.3",
|
||||
"resolved": "https://registry.npmjs.org/@redis/time-series/-/time-series-5.8.3.tgz",
|
||||
"integrity": "sha512-5Jwy3ilsUYQjzpE7WZ1lEeG1RkqQ5kHtwV1p8yxXHSEmyUbC/T/AVgyjMcm52Olj/Ov/mhDKjx6ndYUi14bXsw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@redis/client": "^5.8.3"
|
||||
}
|
||||
},
|
||||
"node_modules/reflect-metadata": {
|
||||
"version": "0.2.2",
|
||||
"resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz",
|
||||
@@ -13739,30 +13849,6 @@
|
||||
"url": "https://opencollective.com/webpack"
|
||||
}
|
||||
},
|
||||
"node_modules/schema-utils/node_modules/ajv": {
|
||||
"version": "6.12.6",
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
|
||||
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fast-deep-equal": "^3.1.1",
|
||||
"fast-json-stable-stringify": "^2.0.0",
|
||||
"json-schema-traverse": "^0.4.1",
|
||||
"uri-js": "^4.2.2"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/epoberezkin"
|
||||
}
|
||||
},
|
||||
"node_modules/schema-utils/node_modules/json-schema-traverse": {
|
||||
"version": "0.4.1",
|
||||
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
|
||||
"integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/semver": {
|
||||
"version": "7.7.2",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
|
||||
@@ -14593,6 +14679,23 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/terser-webpack-plugin/node_modules/ajv": {
|
||||
"version": "8.17.1",
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz",
|
||||
"integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
"fast-uri": "^3.0.1",
|
||||
"json-schema-traverse": "^1.0.0",
|
||||
"require-from-string": "^2.0.2"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/epoberezkin"
|
||||
}
|
||||
},
|
||||
"node_modules/terser-webpack-plugin/node_modules/ajv-keywords": {
|
||||
"version": "5.1.0",
|
||||
"resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz",
|
||||
@@ -14621,6 +14724,13 @@
|
||||
"node": ">= 10.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/terser-webpack-plugin/node_modules/json-schema-traverse": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
|
||||
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/terser-webpack-plugin/node_modules/schema-utils": {
|
||||
"version": "4.3.2",
|
||||
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.2.tgz",
|
||||
@@ -14939,7 +15049,7 @@
|
||||
"version": "10.9.2",
|
||||
"resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz",
|
||||
"integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@cspotcode/source-map-support": "^0.8.0",
|
||||
@@ -15255,7 +15365,7 @@
|
||||
"version": "5.9.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
@@ -15427,7 +15537,7 @@
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz",
|
||||
"integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/v8-to-istanbul": {
|
||||
@@ -15669,7 +15779,7 @@
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz",
|
||||
"integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/wrap-ansi": {
|
||||
@@ -15752,6 +15862,28 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/xss": {
|
||||
"version": "1.0.15",
|
||||
"resolved": "https://registry.npmjs.org/xss/-/xss-1.0.15.tgz",
|
||||
"integrity": "sha512-FVdlVVC67WOIPvfOwhoMETV72f6GbW7aOabBC3WxN/oUdoEMDyLz4OgRv5/gck2ZeNqEQu+Tb0kloovXOfpYVg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"commander": "^2.20.3",
|
||||
"cssfilter": "0.0.10"
|
||||
},
|
||||
"bin": {
|
||||
"xss": "bin/xss"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/xss/node_modules/commander": {
|
||||
"version": "2.20.3",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
|
||||
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/xtend": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
|
||||
@@ -15820,7 +15952,7 @@
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz",
|
||||
"integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
"@aws-sdk/s3-request-presigner": "^3.899.0",
|
||||
"@langchain/core": "^0.3.78",
|
||||
"@langchain/openai": "^0.6.14",
|
||||
"@nestjs/apollo": "^13.2.1",
|
||||
"@nestjs/common": "^11.1.6",
|
||||
"@nestjs/config": "^4.0.2",
|
||||
"@nestjs/core": "^11.1.6",
|
||||
@@ -51,6 +52,8 @@
|
||||
"cache-manager-redis-yet": "^5.1.5",
|
||||
"class-transformer": "^0.5.1",
|
||||
"class-validator": "^0.14.2",
|
||||
"dataloader": "^2.2.3",
|
||||
"date-fns": "^4.1.0",
|
||||
"dotenv": "^17.2.3",
|
||||
"form-data": "^4.0.4",
|
||||
"graphql": "^16.11.0",
|
||||
@@ -78,7 +81,7 @@
|
||||
"devDependencies": {
|
||||
"@nestjs/cli": "^10.0.0",
|
||||
"@nestjs/schematics": "^10.0.0",
|
||||
"@nestjs/testing": "^10.0.0",
|
||||
"@nestjs/testing": "^11.1.6",
|
||||
"@types/bcrypt": "^6.0.0",
|
||||
"@types/express": "^5.0.0",
|
||||
"@types/jest": "^29.5.2",
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { ConfigModule } from '@nestjs/config';
|
||||
import { ConfigModule, ConfigService } from '@nestjs/config';
|
||||
import { ScheduleModule } from '@nestjs/schedule';
|
||||
import { GraphQLModule } from '@nestjs/graphql';
|
||||
import { ApolloDriver, ApolloDriverConfig } from '@nestjs/apollo';
|
||||
import { APP_GUARD, APP_FILTER } from '@nestjs/core';
|
||||
import { join } from 'path';
|
||||
import { AppController } from './app.controller';
|
||||
import { AppService } from './app.service';
|
||||
import { DatabaseModule } from './database/database.module';
|
||||
@@ -17,6 +20,7 @@ import { AnalyticsModule } from './modules/analytics/analytics.module';
|
||||
import { FeedbackModule } from './modules/feedback/feedback.module';
|
||||
import { PhotosModule } from './modules/photos/photos.module';
|
||||
import { ComplianceModule } from './modules/compliance/compliance.module';
|
||||
import { GraphQLCustomModule } from './graphql/graphql.module';
|
||||
import { JwtAuthGuard } from './modules/auth/guards/jwt-auth.guard';
|
||||
import { ErrorTrackingService } from './common/services/error-tracking.service';
|
||||
import { GlobalExceptionFilter } from './common/filters/global-exception.filter';
|
||||
@@ -29,6 +33,28 @@ import { HealthController } from './common/controllers/health.controller';
|
||||
isGlobal: true,
|
||||
envFilePath: '.env',
|
||||
}),
|
||||
GraphQLModule.forRootAsync<ApolloDriverConfig>({
|
||||
driver: ApolloDriver,
|
||||
inject: [ConfigService],
|
||||
imports: [GraphQLCustomModule],
|
||||
useFactory: (configService: ConfigService) => ({
|
||||
autoSchemaFile: join(process.cwd(), 'src/schema.gql'),
|
||||
sortSchema: true,
|
||||
playground: configService.get('NODE_ENV') !== 'production',
|
||||
introspection: true,
|
||||
context: ({ req, res }, ...args) => {
|
||||
// DataLoaders will be provided via REQUEST scope
|
||||
return { req, res };
|
||||
},
|
||||
formatError: (error) => {
|
||||
return {
|
||||
message: error.message,
|
||||
code: error.extensions?.code || 'INTERNAL_SERVER_ERROR',
|
||||
path: error.path,
|
||||
};
|
||||
},
|
||||
}),
|
||||
}),
|
||||
ScheduleModule.forRoot(),
|
||||
DatabaseModule,
|
||||
CommonModule,
|
||||
@@ -43,6 +69,7 @@ import { HealthController } from './common/controllers/health.controller';
|
||||
FeedbackModule,
|
||||
PhotosModule,
|
||||
ComplianceModule,
|
||||
GraphQLCustomModule,
|
||||
],
|
||||
controllers: [AppController, HealthController],
|
||||
providers: [
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
import * as DataLoader from 'dataloader';
|
||||
import { Injectable, Scope } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository, In } from 'typeorm';
|
||||
import { Child } from '../../database/entities/child.entity';
|
||||
|
||||
@Injectable({ scope: Scope.REQUEST })
|
||||
export class ChildDataLoader {
|
||||
constructor(
|
||||
@InjectRepository(Child)
|
||||
private readonly childRepository: Repository<Child>,
|
||||
) {}
|
||||
|
||||
public readonly batchChildren = new DataLoader<string, Child>(
|
||||
async (childIds: readonly string[]) => {
|
||||
const children = await this.childRepository.find({
|
||||
where: { id: In([...childIds]) },
|
||||
});
|
||||
|
||||
const childMap = new Map(children.map((child) => [child.id, child]));
|
||||
return childIds.map((id) => childMap.get(id) || null);
|
||||
},
|
||||
);
|
||||
|
||||
public readonly batchChildrenByFamily = new DataLoader<
|
||||
string,
|
||||
Child[]
|
||||
>(async (familyIds: readonly string[]) => {
|
||||
const children = await this.childRepository.find({
|
||||
where: { familyId: In([...familyIds]) },
|
||||
});
|
||||
|
||||
const childrenByFamily = new Map<string, Child[]>();
|
||||
children.forEach((child) => {
|
||||
const existing = childrenByFamily.get(child.familyId) || [];
|
||||
existing.push(child);
|
||||
childrenByFamily.set(child.familyId, existing);
|
||||
});
|
||||
|
||||
return familyIds.map((familyId) => childrenByFamily.get(familyId) || []);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import * as DataLoader from 'dataloader';
|
||||
import { Injectable, Scope } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository, In } from 'typeorm';
|
||||
import { User } from '../../database/entities/user.entity';
|
||||
|
||||
@Injectable({ scope: Scope.REQUEST })
|
||||
export class UserDataLoader {
|
||||
constructor(
|
||||
@InjectRepository(User)
|
||||
private readonly userRepository: Repository<User>,
|
||||
) {}
|
||||
|
||||
public readonly batchUsers = new DataLoader<string, User>(
|
||||
async (userIds: readonly string[]) => {
|
||||
const users = await this.userRepository.find({
|
||||
where: { id: In([...userIds]) },
|
||||
});
|
||||
|
||||
const userMap = new Map(users.map((user) => [user.id, user]));
|
||||
return userIds.map((id) => userMap.get(id) || null);
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
# Example GraphQL Queries for Maternal App
|
||||
|
||||
# ========================================
|
||||
# Dashboard Query - Optimized Single Query
|
||||
# ========================================
|
||||
# This query replaces multiple REST API calls:
|
||||
# - GET /api/v1/children
|
||||
# - GET /api/v1/tracking/child/:id/recent
|
||||
# - GET /api/v1/tracking/child/:id/summary/today
|
||||
# - GET /api/v1/families/:id/members
|
||||
|
||||
query GetDashboard($childId: ID) {
|
||||
dashboard(childId: $childId) {
|
||||
# Children list
|
||||
children {
|
||||
id
|
||||
name
|
||||
birthDate
|
||||
gender
|
||||
photoUrl
|
||||
}
|
||||
|
||||
# Selected child (specified or first)
|
||||
selectedChild {
|
||||
id
|
||||
name
|
||||
birthDate
|
||||
gender
|
||||
photoUrl
|
||||
}
|
||||
|
||||
# Recent activities (last 10 for selected child)
|
||||
recentActivities {
|
||||
id
|
||||
type
|
||||
startedAt
|
||||
endedAt
|
||||
notes
|
||||
metadata
|
||||
logger {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
|
||||
# Today's summary for selected child
|
||||
todaySummary {
|
||||
date
|
||||
feedingCount
|
||||
totalFeedingAmount
|
||||
sleepCount
|
||||
totalSleepDuration
|
||||
diaperCount
|
||||
medicationCount
|
||||
}
|
||||
|
||||
# Family members
|
||||
familyMembers {
|
||||
userId
|
||||
role
|
||||
user {
|
||||
id
|
||||
name
|
||||
email
|
||||
}
|
||||
}
|
||||
|
||||
# Aggregations
|
||||
totalChildren
|
||||
totalActivitiesToday
|
||||
}
|
||||
}
|
||||
|
||||
# Example Variables:
|
||||
# {
|
||||
# "childId": "child_abc123"
|
||||
# }
|
||||
|
||||
# ========================================
|
||||
# Dashboard Query - All Children
|
||||
# ========================================
|
||||
# Get dashboard data without specifying a child
|
||||
# (will return first child's data)
|
||||
|
||||
query GetDashboardAllChildren {
|
||||
dashboard {
|
||||
children {
|
||||
id
|
||||
name
|
||||
birthDate
|
||||
}
|
||||
selectedChild {
|
||||
id
|
||||
name
|
||||
}
|
||||
totalChildren
|
||||
totalActivitiesToday
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { Child } from '../database/entities/child.entity';
|
||||
import { Activity } from '../database/entities/activity.entity';
|
||||
import { FamilyMember } from '../database/entities/family-member.entity';
|
||||
import { User } from '../database/entities/user.entity';
|
||||
import { DashboardResolver } from './resolvers/dashboard.resolver';
|
||||
import { ChildDataLoader } from './dataloaders/child.dataloader';
|
||||
import { UserDataLoader } from './dataloaders/user.dataloader';
|
||||
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([Child, Activity, FamilyMember, User])],
|
||||
providers: [DashboardResolver, ChildDataLoader, UserDataLoader],
|
||||
exports: [ChildDataLoader, UserDataLoader],
|
||||
})
|
||||
export class GraphQLCustomModule {}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { Injectable, ExecutionContext } from '@nestjs/common';
|
||||
import { GqlExecutionContext } from '@nestjs/graphql';
|
||||
import { AuthGuard } from '@nestjs/passport';
|
||||
|
||||
@Injectable()
|
||||
export class GqlAuthGuard extends AuthGuard('jwt') {
|
||||
getRequest(context: ExecutionContext) {
|
||||
const ctx = GqlExecutionContext.create(context);
|
||||
return ctx.getContext().req;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
import { Resolver, Query, Args, Context, ResolveField, Parent } from '@nestjs/graphql';
|
||||
import { UseGuards } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository, Between } from 'typeorm';
|
||||
import { format } from 'date-fns';
|
||||
import { DashboardType, DailySummaryType } from '../types/dashboard.type';
|
||||
import { ActivityGQLType } from '../types/activity.type';
|
||||
import { ChildType } from '../types/child.type';
|
||||
import { UserType } from '../types/user.type';
|
||||
import { Child } from '../../database/entities/child.entity';
|
||||
import { Activity, ActivityType } from '../../database/entities/activity.entity';
|
||||
import { FamilyMember } from '../../database/entities/family-member.entity';
|
||||
import { User } from '../../database/entities/user.entity';
|
||||
import { GqlAuthGuard } from '../guards/gql-auth.guard';
|
||||
import { ChildDataLoader } from '../dataloaders/child.dataloader';
|
||||
import { UserDataLoader } from '../dataloaders/user.dataloader';
|
||||
|
||||
@Resolver(() => DashboardType)
|
||||
@UseGuards(GqlAuthGuard)
|
||||
export class DashboardResolver {
|
||||
constructor(
|
||||
@InjectRepository(Child)
|
||||
private readonly childRepository: Repository<Child>,
|
||||
@InjectRepository(Activity)
|
||||
private readonly activityRepository: Repository<Activity>,
|
||||
@InjectRepository(FamilyMember)
|
||||
private readonly familyMemberRepository: Repository<FamilyMember>,
|
||||
@InjectRepository(User)
|
||||
private readonly userRepository: Repository<User>,
|
||||
) {}
|
||||
|
||||
@Query(() => DashboardType, { name: 'dashboard' })
|
||||
async getDashboard(
|
||||
@Args('childId', { nullable: true }) childId: string,
|
||||
@Context() context: any,
|
||||
): Promise<DashboardType> {
|
||||
const userId = context.req.user?.userId;
|
||||
|
||||
if (!userId) {
|
||||
throw new Error('User not authenticated');
|
||||
}
|
||||
|
||||
// Get user's family memberships
|
||||
const familyMemberships = await this.familyMemberRepository.find({
|
||||
where: { userId },
|
||||
relations: ['family'],
|
||||
});
|
||||
|
||||
const familyIds = familyMemberships.map((fm) => fm.familyId);
|
||||
|
||||
// Get all children in user's families
|
||||
const children = await this.childRepository.find({
|
||||
where: familyIds.length > 0 ? familyIds.map(id => ({ familyId: id })) : [],
|
||||
order: { createdAt: 'ASC' },
|
||||
});
|
||||
|
||||
// Select child (specified or first child)
|
||||
const selectedChild = childId
|
||||
? children.find((c) => c.id === childId)
|
||||
: children[0];
|
||||
|
||||
// Get today's date range
|
||||
const today = new Date();
|
||||
today.setHours(0, 0, 0, 0);
|
||||
const tomorrow = new Date(today);
|
||||
tomorrow.setDate(tomorrow.getDate() + 1);
|
||||
|
||||
// Get recent activities (last 10 for selected child)
|
||||
const recentActivities = selectedChild
|
||||
? await this.activityRepository.find({
|
||||
where: { childId: selectedChild.id },
|
||||
order: { startedAt: 'DESC' },
|
||||
take: 10,
|
||||
})
|
||||
: [];
|
||||
|
||||
// Get today's activities for selected child
|
||||
const todayActivities = selectedChild
|
||||
? await this.activityRepository.find({
|
||||
where: {
|
||||
childId: selectedChild.id,
|
||||
startedAt: Between(today, tomorrow),
|
||||
},
|
||||
})
|
||||
: [];
|
||||
|
||||
// Calculate today's summary
|
||||
const todaySummary = this.calculateDailySummary(
|
||||
todayActivities,
|
||||
format(today, 'yyyy-MM-dd'),
|
||||
);
|
||||
|
||||
// Get all family members
|
||||
const familyMembers = await this.familyMemberRepository.find({
|
||||
where: familyIds.length > 0 ? familyIds.map(id => ({ familyId: id })) : [],
|
||||
relations: ['user'],
|
||||
});
|
||||
|
||||
return {
|
||||
children: children as any[],
|
||||
selectedChild: selectedChild as any,
|
||||
recentActivities: recentActivities as any[],
|
||||
todaySummary,
|
||||
familyMembers: familyMembers as any[],
|
||||
totalChildren: children.length,
|
||||
totalActivitiesToday: todayActivities.length,
|
||||
};
|
||||
}
|
||||
|
||||
@ResolveField(() => ChildType, { nullable: true })
|
||||
async child(
|
||||
@Parent() activity: ActivityGQLType,
|
||||
@Context() context: any,
|
||||
): Promise<ChildType> {
|
||||
const childLoader: ChildDataLoader = context.childLoader;
|
||||
return childLoader.batchChildren.load(activity.childId) as any;
|
||||
}
|
||||
|
||||
@ResolveField(() => UserType, { nullable: true })
|
||||
async logger(
|
||||
@Parent() activity: ActivityGQLType,
|
||||
@Context() context: any,
|
||||
): Promise<UserType> {
|
||||
const userLoader: UserDataLoader = context.userLoader;
|
||||
return userLoader.batchUsers.load(activity.loggedBy);
|
||||
}
|
||||
|
||||
private calculateDailySummary(
|
||||
activities: Activity[],
|
||||
date: string,
|
||||
): DailySummaryType {
|
||||
const summary = {
|
||||
feedingCount: 0,
|
||||
totalFeedingAmount: 0,
|
||||
sleepCount: 0,
|
||||
totalSleepDuration: 0,
|
||||
diaperCount: 0,
|
||||
medicationCount: 0,
|
||||
date,
|
||||
};
|
||||
|
||||
activities.forEach((activity) => {
|
||||
switch (activity.type) {
|
||||
case ActivityType.FEEDING:
|
||||
summary.feedingCount++;
|
||||
if (activity.metadata?.amount) {
|
||||
summary.totalFeedingAmount += activity.metadata.amount;
|
||||
}
|
||||
break;
|
||||
case ActivityType.SLEEP:
|
||||
summary.sleepCount++;
|
||||
if (activity.endedAt && activity.startedAt) {
|
||||
const duration = Math.floor(
|
||||
(activity.endedAt.getTime() - activity.startedAt.getTime()) / 60000,
|
||||
);
|
||||
summary.totalSleepDuration += duration;
|
||||
}
|
||||
break;
|
||||
case ActivityType.DIAPER:
|
||||
summary.diaperCount++;
|
||||
break;
|
||||
case ActivityType.MEDICATION:
|
||||
case ActivityType.MEDICINE:
|
||||
summary.medicationCount++;
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
return summary;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
import { ObjectType, Field, ID, registerEnumType, Int, Float } from '@nestjs/graphql';
|
||||
import { ChildType } from './child.type';
|
||||
import { UserType } from './user.type';
|
||||
|
||||
export enum ActivityType {
|
||||
FEEDING = 'feeding',
|
||||
SLEEP = 'sleep',
|
||||
DIAPER = 'diaper',
|
||||
GROWTH = 'growth',
|
||||
MEDICATION = 'medication',
|
||||
MEDICINE = 'medicine',
|
||||
TEMPERATURE = 'temperature',
|
||||
MILESTONE = 'milestone',
|
||||
ACTIVITY = 'activity',
|
||||
}
|
||||
|
||||
export enum FeedingMethod {
|
||||
BREAST = 'breast',
|
||||
BOTTLE = 'bottle',
|
||||
SOLIDS = 'solids',
|
||||
}
|
||||
|
||||
export enum DiaperType {
|
||||
WET = 'wet',
|
||||
DIRTY = 'dirty',
|
||||
BOTH = 'both',
|
||||
}
|
||||
|
||||
registerEnumType(ActivityType, { name: 'ActivityType' });
|
||||
registerEnumType(FeedingMethod, { name: 'FeedingMethod' });
|
||||
registerEnumType(DiaperType, { name: 'DiaperType' });
|
||||
|
||||
@ObjectType('Activity')
|
||||
export class ActivityGQLType {
|
||||
@Field(() => ID)
|
||||
id: string;
|
||||
|
||||
@Field()
|
||||
childId: string;
|
||||
|
||||
@Field(() => ActivityType)
|
||||
type: ActivityType;
|
||||
|
||||
@Field()
|
||||
startedAt: Date;
|
||||
|
||||
@Field({ nullable: true })
|
||||
endedAt?: Date;
|
||||
|
||||
@Field()
|
||||
loggedBy: string;
|
||||
|
||||
@Field({ nullable: true })
|
||||
notes?: string;
|
||||
|
||||
// Metadata as JSON string or object
|
||||
@Field({ nullable: true })
|
||||
metadata?: string;
|
||||
|
||||
// Relations
|
||||
@Field(() => ChildType, { nullable: true })
|
||||
child?: ChildType;
|
||||
|
||||
@Field(() => UserType, { nullable: true })
|
||||
logger?: UserType;
|
||||
|
||||
@Field()
|
||||
createdAt: Date;
|
||||
|
||||
@Field()
|
||||
updatedAt: Date;
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import { ObjectType, Field, ID, registerEnumType } from '@nestjs/graphql';
|
||||
|
||||
export enum Gender {
|
||||
MALE = 'male',
|
||||
FEMALE = 'female',
|
||||
OTHER = 'other',
|
||||
}
|
||||
|
||||
registerEnumType(Gender, {
|
||||
name: 'Gender',
|
||||
});
|
||||
|
||||
@ObjectType('Child')
|
||||
export class ChildType {
|
||||
@Field(() => ID)
|
||||
id: string;
|
||||
|
||||
@Field()
|
||||
name: string;
|
||||
|
||||
@Field()
|
||||
birthDate: Date;
|
||||
|
||||
@Field({ nullable: true })
|
||||
gender?: string;
|
||||
|
||||
@Field({ nullable: true })
|
||||
photoUrl?: string;
|
||||
|
||||
@Field()
|
||||
familyId: string;
|
||||
|
||||
@Field()
|
||||
createdAt: Date;
|
||||
|
||||
@Field()
|
||||
updatedAt: Date;
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import { ObjectType, Field, Int } from '@nestjs/graphql';
|
||||
import { ChildType } from './child.type';
|
||||
import { ActivityGQLType } from './activity.type';
|
||||
import { FamilyMemberType } from './family.type';
|
||||
|
||||
@ObjectType('DailySummary')
|
||||
export class DailySummaryType {
|
||||
@Field(() => Int)
|
||||
feedingCount: number;
|
||||
|
||||
@Field(() => Int, { nullable: true })
|
||||
totalFeedingAmount?: number;
|
||||
|
||||
@Field(() => Int)
|
||||
sleepCount: number;
|
||||
|
||||
@Field(() => Int, { nullable: true })
|
||||
totalSleepDuration?: number;
|
||||
|
||||
@Field(() => Int)
|
||||
diaperCount: number;
|
||||
|
||||
@Field(() => Int)
|
||||
medicationCount: number;
|
||||
|
||||
@Field()
|
||||
date: string;
|
||||
}
|
||||
|
||||
@ObjectType('Dashboard')
|
||||
export class DashboardType {
|
||||
@Field(() => [ChildType])
|
||||
children: ChildType[];
|
||||
|
||||
@Field(() => ChildType, { nullable: true })
|
||||
selectedChild?: ChildType;
|
||||
|
||||
@Field(() => [ActivityGQLType])
|
||||
recentActivities: ActivityGQLType[];
|
||||
|
||||
@Field(() => DailySummaryType, { nullable: true })
|
||||
todaySummary?: DailySummaryType;
|
||||
|
||||
@Field(() => [FamilyMemberType])
|
||||
familyMembers: FamilyMemberType[];
|
||||
|
||||
@Field(() => Int)
|
||||
totalChildren: number;
|
||||
|
||||
@Field(() => Int)
|
||||
totalActivitiesToday: number;
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import { ObjectType, Field, ID, registerEnumType } from '@nestjs/graphql';
|
||||
import { UserType } from './user.type';
|
||||
|
||||
export enum FamilyRole {
|
||||
PARENT = 'parent',
|
||||
CAREGIVER = 'caregiver',
|
||||
}
|
||||
|
||||
registerEnumType(FamilyRole, {
|
||||
name: 'FamilyRole',
|
||||
});
|
||||
|
||||
@ObjectType('FamilyMember')
|
||||
export class FamilyMemberType {
|
||||
@Field()
|
||||
familyId: string;
|
||||
|
||||
@Field()
|
||||
userId: string;
|
||||
|
||||
@Field(() => FamilyRole)
|
||||
role: FamilyRole;
|
||||
|
||||
@Field(() => UserType, { nullable: true })
|
||||
user?: UserType;
|
||||
|
||||
@Field()
|
||||
createdAt: Date;
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import { ObjectType, Field, ID } from '@nestjs/graphql';
|
||||
import { FamilyMemberType } from './family.type';
|
||||
|
||||
@ObjectType('User')
|
||||
export class UserType {
|
||||
@Field(() => ID)
|
||||
id: string;
|
||||
|
||||
@Field()
|
||||
email: string;
|
||||
|
||||
@Field()
|
||||
name: string;
|
||||
|
||||
@Field({ nullable: true })
|
||||
phone?: string;
|
||||
|
||||
@Field(() => [FamilyMemberType], { nullable: true })
|
||||
families?: FamilyMemberType[];
|
||||
|
||||
@Field()
|
||||
createdAt: Date;
|
||||
|
||||
@Field()
|
||||
updatedAt: Date;
|
||||
}
|
||||
Reference in New Issue
Block a user