diff --git a/docs/implementation-gaps.md b/docs/implementation-gaps.md index 5b4810a..2b9120b 100644 --- a/docs/implementation-gaps.md +++ b/docs/implementation-gaps.md @@ -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 diff --git a/maternal-app/maternal-app-backend/package-lock.json b/maternal-app/maternal-app-backend/package-lock.json index 50f12c4..80df7b3 100644 --- a/maternal-app/maternal-app-backend/package-lock.json +++ b/maternal-app/maternal-app-backend/package-lock.json @@ -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" diff --git a/maternal-app/maternal-app-backend/package.json b/maternal-app/maternal-app-backend/package.json index 5a6d950..3fb32c5 100644 --- a/maternal-app/maternal-app-backend/package.json +++ b/maternal-app/maternal-app-backend/package.json @@ -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", diff --git a/maternal-app/maternal-app-backend/src/app.module.ts b/maternal-app/maternal-app-backend/src/app.module.ts index 89406c2..f2590f9 100644 --- a/maternal-app/maternal-app-backend/src/app.module.ts +++ b/maternal-app/maternal-app-backend/src/app.module.ts @@ -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({ + 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: [ diff --git a/maternal-app/maternal-app-backend/src/graphql/dataloaders/child.dataloader.ts b/maternal-app/maternal-app-backend/src/graphql/dataloaders/child.dataloader.ts new file mode 100644 index 0000000..c0eb73a --- /dev/null +++ b/maternal-app/maternal-app-backend/src/graphql/dataloaders/child.dataloader.ts @@ -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, + ) {} + + public readonly batchChildren = new DataLoader( + 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(); + children.forEach((child) => { + const existing = childrenByFamily.get(child.familyId) || []; + existing.push(child); + childrenByFamily.set(child.familyId, existing); + }); + + return familyIds.map((familyId) => childrenByFamily.get(familyId) || []); + }); +} diff --git a/maternal-app/maternal-app-backend/src/graphql/dataloaders/user.dataloader.ts b/maternal-app/maternal-app-backend/src/graphql/dataloaders/user.dataloader.ts new file mode 100644 index 0000000..a5e4e47 --- /dev/null +++ b/maternal-app/maternal-app-backend/src/graphql/dataloaders/user.dataloader.ts @@ -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, + ) {} + + public readonly batchUsers = new DataLoader( + 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); + }, + ); +} diff --git a/maternal-app/maternal-app-backend/src/graphql/example-queries.gql b/maternal-app/maternal-app-backend/src/graphql/example-queries.gql new file mode 100644 index 0000000..31dd65d --- /dev/null +++ b/maternal-app/maternal-app-backend/src/graphql/example-queries.gql @@ -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 + } +} diff --git a/maternal-app/maternal-app-backend/src/graphql/graphql.module.ts b/maternal-app/maternal-app-backend/src/graphql/graphql.module.ts new file mode 100644 index 0000000..e3b02aa --- /dev/null +++ b/maternal-app/maternal-app-backend/src/graphql/graphql.module.ts @@ -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 {} diff --git a/maternal-app/maternal-app-backend/src/graphql/guards/gql-auth.guard.ts b/maternal-app/maternal-app-backend/src/graphql/guards/gql-auth.guard.ts new file mode 100644 index 0000000..3ae7ade --- /dev/null +++ b/maternal-app/maternal-app-backend/src/graphql/guards/gql-auth.guard.ts @@ -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; + } +} diff --git a/maternal-app/maternal-app-backend/src/graphql/resolvers/dashboard.resolver.ts b/maternal-app/maternal-app-backend/src/graphql/resolvers/dashboard.resolver.ts new file mode 100644 index 0000000..280ebad --- /dev/null +++ b/maternal-app/maternal-app-backend/src/graphql/resolvers/dashboard.resolver.ts @@ -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, + @InjectRepository(Activity) + private readonly activityRepository: Repository, + @InjectRepository(FamilyMember) + private readonly familyMemberRepository: Repository, + @InjectRepository(User) + private readonly userRepository: Repository, + ) {} + + @Query(() => DashboardType, { name: 'dashboard' }) + async getDashboard( + @Args('childId', { nullable: true }) childId: string, + @Context() context: any, + ): Promise { + 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 { + 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 { + 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; + } +} diff --git a/maternal-app/maternal-app-backend/src/graphql/types/activity.type.ts b/maternal-app/maternal-app-backend/src/graphql/types/activity.type.ts new file mode 100644 index 0000000..e71c054 --- /dev/null +++ b/maternal-app/maternal-app-backend/src/graphql/types/activity.type.ts @@ -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; +} diff --git a/maternal-app/maternal-app-backend/src/graphql/types/child.type.ts b/maternal-app/maternal-app-backend/src/graphql/types/child.type.ts new file mode 100644 index 0000000..fb3615d --- /dev/null +++ b/maternal-app/maternal-app-backend/src/graphql/types/child.type.ts @@ -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; +} diff --git a/maternal-app/maternal-app-backend/src/graphql/types/dashboard.type.ts b/maternal-app/maternal-app-backend/src/graphql/types/dashboard.type.ts new file mode 100644 index 0000000..36fc08e --- /dev/null +++ b/maternal-app/maternal-app-backend/src/graphql/types/dashboard.type.ts @@ -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; +} diff --git a/maternal-app/maternal-app-backend/src/graphql/types/family.type.ts b/maternal-app/maternal-app-backend/src/graphql/types/family.type.ts new file mode 100644 index 0000000..2f49a0f --- /dev/null +++ b/maternal-app/maternal-app-backend/src/graphql/types/family.type.ts @@ -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; +} diff --git a/maternal-app/maternal-app-backend/src/graphql/types/user.type.ts b/maternal-app/maternal-app-backend/src/graphql/types/user.type.ts new file mode 100644 index 0000000..ba4c71d --- /dev/null +++ b/maternal-app/maternal-app-backend/src/graphql/types/user.type.ts @@ -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; +}