From a257817f8c591f1dd82ab6b153fcc30a6ff3fb8b Mon Sep 17 00:00:00 2001 From: Ravi Gulhane <> Date: Thu, 16 Apr 2026 10:09:31 +0530 Subject: [PATCH] on the way --- .env.example | 2 + package-lock.json | 947 +++++++++++++--------- package.json | 5 +- scripts/backfill-join-codes.js | 12 +- src/config/constants.js | 14 +- src/controllers/event.controller.js | 149 +++- src/controllers/question.controller.js | 26 +- src/controllers/rewardPoint.controller.js | 27 +- src/controllers/session.controller.js | 49 +- src/controllers/visitor.controller.js | 317 +++++++- src/controllers/visitorAuth.controller.js | 44 +- src/models/Event.js | 19 + src/models/Question.js | 4 + src/models/QuizPoll.js | 2 + src/models/RewardPoint.js | 15 +- src/models/Session.js | 10 + src/models/rewardRulesSchema.js | 19 + src/routes/event.routes.js | 29 +- src/routes/question.routes.js | 15 + src/routes/quizPoll.routes.js | 13 + src/routes/rewardPoint.routes.js | 12 +- src/routes/session.routes.js | 17 + src/routes/visitor.routes.js | 41 + src/services/rewardAward.js | 66 ++ src/utils/bedrockExtractEvent.js | 78 ++ src/utils/eventBuzzCode.js | 4 +- src/utils/quizLoyaltyScore.js | 65 ++ src/utils/rewardRulesResolve.js | 39 + src/utils/sessionQuestionFirebaseSync.js | 2 + 29 files changed, 1587 insertions(+), 455 deletions(-) create mode 100644 src/models/rewardRulesSchema.js create mode 100644 src/services/rewardAward.js create mode 100644 src/utils/bedrockExtractEvent.js create mode 100644 src/utils/quizLoyaltyScore.js create mode 100644 src/utils/rewardRulesResolve.js diff --git a/.env.example b/.env.example index dd25cd7..b1a6fe4 100644 --- a/.env.example +++ b/.env.example @@ -33,6 +33,8 @@ AWS_SECRET_ACCESS_KEY=ZpIxby1vbCLGB7CPWZsAltZBEqkhwzgBpFOKDGj8 AWS_DEFAULT_REGION=ap-south-1 AWS_BUCKET=rapportapp AWS_URL=https://did94hhhcgq1d.cloudfront.net +# Amazon Bedrock (AI event generation). Model must be enabled in your AWS account for this region. +# BEDROCK_MODEL_ID=anthropic.claude-3-haiku-20240307-v1:0 # Public API base URL injected into published microsite HTML for contact forms (e.g. https://api.example.com/api or http://localhost:3001/api) MICROSITE_API_BASE_URL=http://localhost:3001/api diff --git a/package-lock.json b/package-lock.json index 9e7ab4a..520de67 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,7 @@ "name": "eventbuzz-backend", "version": "1.0.0", "dependencies": { + "@aws-sdk/client-bedrock-runtime": "^3.1030.0", "@aws-sdk/client-s3": "^3.1012.0", "@aws-sdk/lib-storage": "^3.1012.0", "@hapi/hapi": "^21.3.0", @@ -232,6 +233,64 @@ "node": ">=14.0.0" } }, + "node_modules/@aws-sdk/client-bedrock-runtime": { + "version": "3.1030.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-bedrock-runtime/-/client-bedrock-runtime-3.1030.0.tgz", + "integrity": "sha512-5Lnyx6mQPsIdld5Xr9FJqu8Hi9RVY6SgE8Rysmn4r3lRY2vNohNEu+gCtdXRDkkv/PgK9OnbA0sUPFU9rBRMYA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "^3.973.27", + "@aws-sdk/credential-provider-node": "^3.972.30", + "@aws-sdk/eventstream-handler-node": "^3.972.13", + "@aws-sdk/middleware-eventstream": "^3.972.9", + "@aws-sdk/middleware-host-header": "^3.972.9", + "@aws-sdk/middleware-logger": "^3.972.9", + "@aws-sdk/middleware-recursion-detection": "^3.972.10", + "@aws-sdk/middleware-user-agent": "^3.972.29", + "@aws-sdk/middleware-websocket": "^3.972.15", + "@aws-sdk/region-config-resolver": "^3.972.11", + "@aws-sdk/token-providers": "3.1030.0", + "@aws-sdk/types": "^3.973.7", + "@aws-sdk/util-endpoints": "^3.996.6", + "@aws-sdk/util-user-agent-browser": "^3.972.9", + "@aws-sdk/util-user-agent-node": "^3.973.15", + "@smithy/config-resolver": "^4.4.14", + "@smithy/core": "^3.23.14", + "@smithy/eventstream-serde-browser": "^4.2.13", + "@smithy/eventstream-serde-config-resolver": "^4.3.13", + "@smithy/eventstream-serde-node": "^4.2.13", + "@smithy/fetch-http-handler": "^5.3.16", + "@smithy/hash-node": "^4.2.13", + "@smithy/invalid-dependency": "^4.2.13", + "@smithy/middleware-content-length": "^4.2.13", + "@smithy/middleware-endpoint": "^4.4.29", + "@smithy/middleware-retry": "^4.5.0", + "@smithy/middleware-serde": "^4.2.17", + "@smithy/middleware-stack": "^4.2.13", + "@smithy/node-config-provider": "^4.3.13", + "@smithy/node-http-handler": "^4.5.2", + "@smithy/protocol-http": "^5.3.13", + "@smithy/smithy-client": "^4.12.9", + "@smithy/types": "^4.14.0", + "@smithy/url-parser": "^4.2.13", + "@smithy/util-base64": "^4.3.2", + "@smithy/util-body-length-browser": "^4.2.2", + "@smithy/util-body-length-node": "^4.2.3", + "@smithy/util-defaults-mode-browser": "^4.3.45", + "@smithy/util-defaults-mode-node": "^4.2.49", + "@smithy/util-endpoints": "^3.3.4", + "@smithy/util-middleware": "^4.2.13", + "@smithy/util-retry": "^4.3.0", + "@smithy/util-stream": "^4.5.22", + "@smithy/util-utf8": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, "node_modules/@aws-sdk/client-s3": { "version": "3.1012.0", "resolved": "https://registry.npmjs.org/@aws-sdk/client-s3/-/client-s3-3.1012.0.tgz", @@ -299,22 +358,22 @@ } }, "node_modules/@aws-sdk/core": { - "version": "3.973.21", - "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.973.21.tgz", - "integrity": "sha512-OTUcDX9Yfz/FLKbHjiMaP9D4Hs44lYJzN7zBcrK2nDmBt0Wr8D6nYt12QoBkZsW0nVMFsTIGaZCrsU9zCcIMXQ==", + "version": "3.973.27", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.973.27.tgz", + "integrity": "sha512-CUZ5m8hwMCH6OYI4Li/WgMfIEx10Q2PLI9Y3XOUTPGZJ53aZ0007jCv+X/ywsaERyKPdw5MRZWk877roQksQ4A==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "^3.973.6", - "@aws-sdk/xml-builder": "^3.972.12", - "@smithy/core": "^3.23.12", - "@smithy/node-config-provider": "^4.3.12", - "@smithy/property-provider": "^4.2.12", - "@smithy/protocol-http": "^5.3.12", - "@smithy/signature-v4": "^5.3.12", - "@smithy/smithy-client": "^4.12.6", - "@smithy/types": "^4.13.1", + "@aws-sdk/types": "^3.973.7", + "@aws-sdk/xml-builder": "^3.972.17", + "@smithy/core": "^3.23.14", + "@smithy/node-config-provider": "^4.3.13", + "@smithy/property-provider": "^4.2.13", + "@smithy/protocol-http": "^5.3.13", + "@smithy/signature-v4": "^5.3.13", + "@smithy/smithy-client": "^4.12.9", + "@smithy/types": "^4.14.0", "@smithy/util-base64": "^4.3.2", - "@smithy/util-middleware": "^4.2.12", + "@smithy/util-middleware": "^4.2.13", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" }, @@ -336,15 +395,15 @@ } }, "node_modules/@aws-sdk/credential-provider-env": { - "version": "3.972.19", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.19.tgz", - "integrity": "sha512-33NpkQtmnsjLr9QdZvL3w8bjy+WoBJ+jY8JwuzxIq38rDNi1kwpBWW7Yjh+8bMlksd+ZAWW0fH4S/6OeoAdU5A==", + "version": "3.972.25", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.25.tgz", + "integrity": "sha512-6QfI0wv4jpG5CrdO/AO0JfZ2ux+tKwJPrUwmvxXF50vI5KIypKVGNF6b4vlkYEnKumDTI1NX2zUBi8JoU5QU3A==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.973.21", - "@aws-sdk/types": "^3.973.6", - "@smithy/property-provider": "^4.2.12", - "@smithy/types": "^4.13.1", + "@aws-sdk/core": "^3.973.27", + "@aws-sdk/types": "^3.973.7", + "@smithy/property-provider": "^4.2.13", + "@smithy/types": "^4.14.0", "tslib": "^2.6.2" }, "engines": { @@ -352,20 +411,20 @@ } }, "node_modules/@aws-sdk/credential-provider-http": { - "version": "3.972.21", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.21.tgz", - "integrity": "sha512-xFke7yjbON4unNOG0TApQwz+o1LH5VhVLgWlUuiLRWNDyBfeHIFje2ck8qHybvJ8Fkm5m3SsN+pvHtVo6PGWlQ==", + "version": "3.972.27", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.27.tgz", + "integrity": "sha512-3V3Usj9Gs93h865DqN4M2NWJhC5kXU9BvZskfN3+69omuYlE3TZxOEcVQtBGLOloJB7BVfJKXVLqeNhOzHqSlQ==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.973.21", - "@aws-sdk/types": "^3.973.6", - "@smithy/fetch-http-handler": "^5.3.15", - "@smithy/node-http-handler": "^4.5.0", - "@smithy/property-provider": "^4.2.12", - "@smithy/protocol-http": "^5.3.12", - "@smithy/smithy-client": "^4.12.6", - "@smithy/types": "^4.13.1", - "@smithy/util-stream": "^4.5.20", + "@aws-sdk/core": "^3.973.27", + "@aws-sdk/types": "^3.973.7", + "@smithy/fetch-http-handler": "^5.3.16", + "@smithy/node-http-handler": "^4.5.2", + "@smithy/property-provider": "^4.2.13", + "@smithy/protocol-http": "^5.3.13", + "@smithy/smithy-client": "^4.12.9", + "@smithy/types": "^4.14.0", + "@smithy/util-stream": "^4.5.22", "tslib": "^2.6.2" }, "engines": { @@ -373,24 +432,24 @@ } }, "node_modules/@aws-sdk/credential-provider-ini": { - "version": "3.972.21", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.972.21.tgz", - "integrity": "sha512-fmJN7KhB7CoG65w9fC2LVOd2wZbR2d1yJIpZNe2J5CeDPu7nUHSmavuJAeGEoE3OL5UIBVPNhmK/fV/NQrs3Hw==", + "version": "3.972.29", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.972.29.tgz", + "integrity": "sha512-SiBuAnXecCbT/OpAf3vqyI/AVE3mTaYr9ShXLybxZiPLBiPCCOIWSGAtYYGQWMRvobBTiqOewaB+wcgMMZI2Aw==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.973.21", - "@aws-sdk/credential-provider-env": "^3.972.19", - "@aws-sdk/credential-provider-http": "^3.972.21", - "@aws-sdk/credential-provider-login": "^3.972.21", - "@aws-sdk/credential-provider-process": "^3.972.19", - "@aws-sdk/credential-provider-sso": "^3.972.21", - "@aws-sdk/credential-provider-web-identity": "^3.972.21", - "@aws-sdk/nested-clients": "^3.996.11", - "@aws-sdk/types": "^3.973.6", - "@smithy/credential-provider-imds": "^4.2.12", - "@smithy/property-provider": "^4.2.12", - "@smithy/shared-ini-file-loader": "^4.4.7", - "@smithy/types": "^4.13.1", + "@aws-sdk/core": "^3.973.27", + "@aws-sdk/credential-provider-env": "^3.972.25", + "@aws-sdk/credential-provider-http": "^3.972.27", + "@aws-sdk/credential-provider-login": "^3.972.29", + "@aws-sdk/credential-provider-process": "^3.972.25", + "@aws-sdk/credential-provider-sso": "^3.972.29", + "@aws-sdk/credential-provider-web-identity": "^3.972.29", + "@aws-sdk/nested-clients": "^3.996.19", + "@aws-sdk/types": "^3.973.7", + "@smithy/credential-provider-imds": "^4.2.13", + "@smithy/property-provider": "^4.2.13", + "@smithy/shared-ini-file-loader": "^4.4.8", + "@smithy/types": "^4.14.0", "tslib": "^2.6.2" }, "engines": { @@ -398,18 +457,18 @@ } }, "node_modules/@aws-sdk/credential-provider-login": { - "version": "3.972.21", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.21.tgz", - "integrity": "sha512-ENU+YCiuQocQjfIf9bPxZ+ZY0wIBkl3SMH22optBQwy8UFpSfonHynXzGT27xQxer4cYTNOpwDqbfo57BusbpQ==", + "version": "3.972.29", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.29.tgz", + "integrity": "sha512-OGOslTbOlxXexKMqhxCEbBQbUIfuhGxU5UXw3Fm56ypXHvrXH4aTt/xb5Y884LOoteP1QST1lVZzHfcTnWhiPQ==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.973.21", - "@aws-sdk/nested-clients": "^3.996.11", - "@aws-sdk/types": "^3.973.6", - "@smithy/property-provider": "^4.2.12", - "@smithy/protocol-http": "^5.3.12", - "@smithy/shared-ini-file-loader": "^4.4.7", - "@smithy/types": "^4.13.1", + "@aws-sdk/core": "^3.973.27", + "@aws-sdk/nested-clients": "^3.996.19", + "@aws-sdk/types": "^3.973.7", + "@smithy/property-provider": "^4.2.13", + "@smithy/protocol-http": "^5.3.13", + "@smithy/shared-ini-file-loader": "^4.4.8", + "@smithy/types": "^4.14.0", "tslib": "^2.6.2" }, "engines": { @@ -417,22 +476,22 @@ } }, "node_modules/@aws-sdk/credential-provider-node": { - "version": "3.972.22", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.22.tgz", - "integrity": "sha512-VE6i8nkmrRyhKut7nnfCWRbdDf+CfyRr8ixSwdaPDguYlgvkAO2pHu9oK11XzbSuatB0io1ozI/vpYhelXn8Pg==", + "version": "3.972.30", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.30.tgz", + "integrity": "sha512-FMnAnWxc8PG+ZrZ2OBKzY4luCUJhe9CG0B9YwYr4pzrYGLXBS2rl+UoUvjGbAwiptxRL6hyA3lFn03Bv1TLqTw==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/credential-provider-env": "^3.972.19", - "@aws-sdk/credential-provider-http": "^3.972.21", - "@aws-sdk/credential-provider-ini": "^3.972.21", - "@aws-sdk/credential-provider-process": "^3.972.19", - "@aws-sdk/credential-provider-sso": "^3.972.21", - "@aws-sdk/credential-provider-web-identity": "^3.972.21", - "@aws-sdk/types": "^3.973.6", - "@smithy/credential-provider-imds": "^4.2.12", - "@smithy/property-provider": "^4.2.12", - "@smithy/shared-ini-file-loader": "^4.4.7", - "@smithy/types": "^4.13.1", + "@aws-sdk/credential-provider-env": "^3.972.25", + "@aws-sdk/credential-provider-http": "^3.972.27", + "@aws-sdk/credential-provider-ini": "^3.972.29", + "@aws-sdk/credential-provider-process": "^3.972.25", + "@aws-sdk/credential-provider-sso": "^3.972.29", + "@aws-sdk/credential-provider-web-identity": "^3.972.29", + "@aws-sdk/types": "^3.973.7", + "@smithy/credential-provider-imds": "^4.2.13", + "@smithy/property-provider": "^4.2.13", + "@smithy/shared-ini-file-loader": "^4.4.8", + "@smithy/types": "^4.14.0", "tslib": "^2.6.2" }, "engines": { @@ -440,16 +499,16 @@ } }, "node_modules/@aws-sdk/credential-provider-process": { - "version": "3.972.19", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.19.tgz", - "integrity": "sha512-hjj5bFo4kf5/WzAMjDEFByVOMbq5gZiagIpJexf7Kp9nIDaGzhCphMsx03NCA8s9zUJzHlD1lXazd7MS+e03Lg==", + "version": "3.972.25", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.25.tgz", + "integrity": "sha512-HR7ynNRdNhNsdVCOCegy1HsfsRzozCOPtD3RzzT1JouuaHobWyRfJzCBue/3jP7gECHt+kQyZUvwg/cYLWurNQ==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.973.21", - "@aws-sdk/types": "^3.973.6", - "@smithy/property-provider": "^4.2.12", - "@smithy/shared-ini-file-loader": "^4.4.7", - "@smithy/types": "^4.13.1", + "@aws-sdk/core": "^3.973.27", + "@aws-sdk/types": "^3.973.7", + "@smithy/property-provider": "^4.2.13", + "@smithy/shared-ini-file-loader": "^4.4.8", + "@smithy/types": "^4.14.0", "tslib": "^2.6.2" }, "engines": { @@ -457,18 +516,36 @@ } }, "node_modules/@aws-sdk/credential-provider-sso": { - "version": "3.972.21", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.972.21.tgz", - "integrity": "sha512-9jWRCuMZpZKlqCZ46bvievqdfswsyB2yPAr9rOiN+FxaGgf8jrR5iYDqJgscvk1jrbAxiK4cIjHv3XjIAWAhzQ==", + "version": "3.972.29", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.972.29.tgz", + "integrity": "sha512-HWv4SEq3jZDYPlwryZVef97+U8CxxRos5mK8sgGO1dQaFZpV5giZLzqGE5hkDmh2csYcBO2uf5XHjPTpZcJlig==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.973.21", - "@aws-sdk/nested-clients": "^3.996.11", - "@aws-sdk/token-providers": "3.1012.0", - "@aws-sdk/types": "^3.973.6", - "@smithy/property-provider": "^4.2.12", - "@smithy/shared-ini-file-loader": "^4.4.7", - "@smithy/types": "^4.13.1", + "@aws-sdk/core": "^3.973.27", + "@aws-sdk/nested-clients": "^3.996.19", + "@aws-sdk/token-providers": "3.1026.0", + "@aws-sdk/types": "^3.973.7", + "@smithy/property-provider": "^4.2.13", + "@smithy/shared-ini-file-loader": "^4.4.8", + "@smithy/types": "^4.14.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-sso/node_modules/@aws-sdk/token-providers": { + "version": "3.1026.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.1026.0.tgz", + "integrity": "sha512-Ieq/HiRrbEtrYP387Nes0XlR7H1pJiJOZKv+QyQzMYpvTiDs0VKy2ZB3E2Zf+aFovWmeE7lRE4lXyF7dYM6GgA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.973.27", + "@aws-sdk/nested-clients": "^3.996.19", + "@aws-sdk/types": "^3.973.7", + "@smithy/property-provider": "^4.2.13", + "@smithy/shared-ini-file-loader": "^4.4.8", + "@smithy/types": "^4.14.0", "tslib": "^2.6.2" }, "engines": { @@ -476,17 +553,32 @@ } }, "node_modules/@aws-sdk/credential-provider-web-identity": { - "version": "3.972.21", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.21.tgz", - "integrity": "sha512-ShWQO/cQVZ+j3zUDK7Kj+m7grPzQCVA2iaZdJ+hJTGvVH5lR32Ip/rgZZ+zBdH6D6wczP9Upa4NMXoqJdGpK1g==", + "version": "3.972.29", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.29.tgz", + "integrity": "sha512-PdMBza1WEKEUPFEmMGCfnU2RYCz9MskU2e8JxjyUOsMKku7j9YaDKvbDi2dzC0ihFoM6ods2SbhfAAro+Gwlew==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.973.21", - "@aws-sdk/nested-clients": "^3.996.11", - "@aws-sdk/types": "^3.973.6", - "@smithy/property-provider": "^4.2.12", - "@smithy/shared-ini-file-loader": "^4.4.7", - "@smithy/types": "^4.13.1", + "@aws-sdk/core": "^3.973.27", + "@aws-sdk/nested-clients": "^3.996.19", + "@aws-sdk/types": "^3.973.7", + "@smithy/property-provider": "^4.2.13", + "@smithy/shared-ini-file-loader": "^4.4.8", + "@smithy/types": "^4.14.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/eventstream-handler-node": { + "version": "3.972.13", + "resolved": "https://registry.npmjs.org/@aws-sdk/eventstream-handler-node/-/eventstream-handler-node-3.972.13.tgz", + "integrity": "sha512-2Pi1kD0MDkMAxDHqvpi/hKMs9hXUYbj2GLEjCwy+0jzfLChAsF50SUYnOeTI+RztA+Ic4pnLAdB03f1e8nggxQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.7", + "@smithy/eventstream-codec": "^4.2.13", + "@smithy/types": "^4.14.0", "tslib": "^2.6.2" }, "engines": { @@ -532,6 +624,21 @@ "node": ">=20.0.0" } }, + "node_modules/@aws-sdk/middleware-eventstream": { + "version": "3.972.9", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-eventstream/-/middleware-eventstream-3.972.9.tgz", + "integrity": "sha512-ypgOvpWxQTCnQyDHGxnTviqqANE7FIIzII7VczJnTPCJcJlu17hMQXnvE47aKSKsawVJAaaRsyOEbHQuLJF9ng==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.7", + "@smithy/protocol-http": "^5.3.13", + "@smithy/types": "^4.14.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, "node_modules/@aws-sdk/middleware-expect-continue": { "version": "3.972.8", "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-expect-continue/-/middleware-expect-continue-3.972.8.tgz", @@ -573,14 +680,14 @@ } }, "node_modules/@aws-sdk/middleware-host-header": { - "version": "3.972.8", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.972.8.tgz", - "integrity": "sha512-wAr2REfKsqoKQ+OkNqvOShnBoh+nkPurDKW7uAeVSu6kUECnWlSJiPvnoqxGlfousEY/v9LfS9sNc46hjSYDIQ==", + "version": "3.972.9", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.972.9.tgz", + "integrity": "sha512-je5vRdNw4SkuTnmRbFZLdye4sQ0faLt8kwka5wnnSU30q1mHO4X+idGEJOOE+Tn1ME7Oryn05xxkDvIb3UaLaQ==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "^3.973.6", - "@smithy/protocol-http": "^5.3.12", - "@smithy/types": "^4.13.1", + "@aws-sdk/types": "^3.973.7", + "@smithy/protocol-http": "^5.3.13", + "@smithy/types": "^4.14.0", "tslib": "^2.6.2" }, "engines": { @@ -602,13 +709,13 @@ } }, "node_modules/@aws-sdk/middleware-logger": { - "version": "3.972.8", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.972.8.tgz", - "integrity": "sha512-CWl5UCM57WUFaFi5kB7IBY1UmOeLvNZAZ2/OZ5l20ldiJ3TiIz1pC65gYj8X0BCPWkeR1E32mpsCk1L1I4n+lA==", + "version": "3.972.9", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.972.9.tgz", + "integrity": "sha512-HsVgDrruhqI28RkaXALm8grJ7Agc1wF6Et0xh6pom8NdO2VdO/SD9U/tPwUjewwK/pVoka+EShBxyCvgsPCtog==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "^3.973.6", - "@smithy/types": "^4.13.1", + "@aws-sdk/types": "^3.973.7", + "@smithy/types": "^4.14.0", "tslib": "^2.6.2" }, "engines": { @@ -616,15 +723,15 @@ } }, "node_modules/@aws-sdk/middleware-recursion-detection": { - "version": "3.972.8", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.972.8.tgz", - "integrity": "sha512-BnnvYs2ZEpdlmZ2PNlV2ZyQ8j8AEkMTjN79y/YA475ER1ByFYrkVR85qmhni8oeTaJcDqbx364wDpitDAA/wCA==", + "version": "3.972.10", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.972.10.tgz", + "integrity": "sha512-RVQQbq5orQ/GHUnXvqEOj2HHPBJm+mM+ySwZKS5UaLBwra5ugRtiH09PLUoOZRl7a1YzaOzXSuGbn9iD5j60WQ==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "^3.973.6", + "@aws-sdk/types": "^3.973.7", "@aws/lambda-invoke-store": "^0.2.2", - "@smithy/protocol-http": "^5.3.12", - "@smithy/types": "^4.13.1", + "@smithy/protocol-http": "^5.3.13", + "@smithy/types": "^4.14.0", "tslib": "^2.6.2" }, "engines": { @@ -671,66 +778,89 @@ } }, "node_modules/@aws-sdk/middleware-user-agent": { - "version": "3.972.22", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.972.22.tgz", - "integrity": "sha512-pZPNGWZVQvgUIO/P9PXZNz7ciq9mLYb/wQEurg3phKTa3DiBIunIRcgA0eBNwmog6S3oy0KR1bv4EJ4ld9A5sQ==", + "version": "3.972.29", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.972.29.tgz", + "integrity": "sha512-f/sIRzuTfEjg6NsbMYvye2VsmnQoNgntntleQyx5uGacUYzszbfIlO3GcI6G6daWUmTm0IDZc11qMHWwF0o0mQ==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.973.21", - "@aws-sdk/types": "^3.973.6", - "@aws-sdk/util-endpoints": "^3.996.5", - "@smithy/core": "^3.23.12", - "@smithy/protocol-http": "^5.3.12", - "@smithy/types": "^4.13.1", - "@smithy/util-retry": "^4.2.12", + "@aws-sdk/core": "^3.973.27", + "@aws-sdk/types": "^3.973.7", + "@aws-sdk/util-endpoints": "^3.996.6", + "@smithy/core": "^3.23.14", + "@smithy/protocol-http": "^5.3.13", + "@smithy/types": "^4.14.0", + "@smithy/util-retry": "^4.3.0", "tslib": "^2.6.2" }, "engines": { "node": ">=20.0.0" } }, + "node_modules/@aws-sdk/middleware-websocket": { + "version": "3.972.15", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-websocket/-/middleware-websocket-3.972.15.tgz", + "integrity": "sha512-hsZ35FORQsN5hwNdMD6zWmHCphbXkDxO6j+xwCUiuMb0O6gzS/PWgttQNl1OAn7h/uqZAMUG4yOS0wY/yhAieg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.7", + "@aws-sdk/util-format-url": "^3.972.9", + "@smithy/eventstream-codec": "^4.2.13", + "@smithy/eventstream-serde-browser": "^4.2.13", + "@smithy/fetch-http-handler": "^5.3.16", + "@smithy/protocol-http": "^5.3.13", + "@smithy/signature-v4": "^5.3.13", + "@smithy/types": "^4.14.0", + "@smithy/util-base64": "^4.3.2", + "@smithy/util-hex-encoding": "^4.2.2", + "@smithy/util-utf8": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">= 14.0.0" + } + }, "node_modules/@aws-sdk/nested-clients": { - "version": "3.996.11", - "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.996.11.tgz", - "integrity": "sha512-i7SwoSR4JB/79JoGDUACnFUQOZwXGLWNX35lIb1Pq72nUGlVV+RFZp+BLa8S+mog2pbXU9+6Kc5YwGiMi5bKhQ==", + "version": "3.996.19", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.996.19.tgz", + "integrity": "sha512-uFkmCDXvmQYLanlYdOFS0+MQWkrj9wPMt/ZCc/0J0fjPim6F5jBVBmEomvGY/j77ILW6GTPwN22Jc174Mhkw6Q==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "^3.973.21", - "@aws-sdk/middleware-host-header": "^3.972.8", - "@aws-sdk/middleware-logger": "^3.972.8", - "@aws-sdk/middleware-recursion-detection": "^3.972.8", - "@aws-sdk/middleware-user-agent": "^3.972.22", - "@aws-sdk/region-config-resolver": "^3.972.8", - "@aws-sdk/types": "^3.973.6", - "@aws-sdk/util-endpoints": "^3.996.5", - "@aws-sdk/util-user-agent-browser": "^3.972.8", - "@aws-sdk/util-user-agent-node": "^3.973.8", - "@smithy/config-resolver": "^4.4.11", - "@smithy/core": "^3.23.12", - "@smithy/fetch-http-handler": "^5.3.15", - "@smithy/hash-node": "^4.2.12", - "@smithy/invalid-dependency": "^4.2.12", - "@smithy/middleware-content-length": "^4.2.12", - "@smithy/middleware-endpoint": "^4.4.26", - "@smithy/middleware-retry": "^4.4.43", - "@smithy/middleware-serde": "^4.2.15", - "@smithy/middleware-stack": "^4.2.12", - "@smithy/node-config-provider": "^4.3.12", - "@smithy/node-http-handler": "^4.5.0", - "@smithy/protocol-http": "^5.3.12", - "@smithy/smithy-client": "^4.12.6", - "@smithy/types": "^4.13.1", - "@smithy/url-parser": "^4.2.12", + "@aws-sdk/core": "^3.973.27", + "@aws-sdk/middleware-host-header": "^3.972.9", + "@aws-sdk/middleware-logger": "^3.972.9", + "@aws-sdk/middleware-recursion-detection": "^3.972.10", + "@aws-sdk/middleware-user-agent": "^3.972.29", + "@aws-sdk/region-config-resolver": "^3.972.11", + "@aws-sdk/types": "^3.973.7", + "@aws-sdk/util-endpoints": "^3.996.6", + "@aws-sdk/util-user-agent-browser": "^3.972.9", + "@aws-sdk/util-user-agent-node": "^3.973.15", + "@smithy/config-resolver": "^4.4.14", + "@smithy/core": "^3.23.14", + "@smithy/fetch-http-handler": "^5.3.16", + "@smithy/hash-node": "^4.2.13", + "@smithy/invalid-dependency": "^4.2.13", + "@smithy/middleware-content-length": "^4.2.13", + "@smithy/middleware-endpoint": "^4.4.29", + "@smithy/middleware-retry": "^4.5.0", + "@smithy/middleware-serde": "^4.2.17", + "@smithy/middleware-stack": "^4.2.13", + "@smithy/node-config-provider": "^4.3.13", + "@smithy/node-http-handler": "^4.5.2", + "@smithy/protocol-http": "^5.3.13", + "@smithy/smithy-client": "^4.12.9", + "@smithy/types": "^4.14.0", + "@smithy/url-parser": "^4.2.13", "@smithy/util-base64": "^4.3.2", "@smithy/util-body-length-browser": "^4.2.2", "@smithy/util-body-length-node": "^4.2.3", - "@smithy/util-defaults-mode-browser": "^4.3.42", - "@smithy/util-defaults-mode-node": "^4.2.45", - "@smithy/util-endpoints": "^3.3.3", - "@smithy/util-middleware": "^4.2.12", - "@smithy/util-retry": "^4.2.12", + "@smithy/util-defaults-mode-browser": "^4.3.45", + "@smithy/util-defaults-mode-node": "^4.2.49", + "@smithy/util-endpoints": "^3.3.4", + "@smithy/util-middleware": "^4.2.13", + "@smithy/util-retry": "^4.3.0", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" }, @@ -739,15 +869,15 @@ } }, "node_modules/@aws-sdk/region-config-resolver": { - "version": "3.972.8", - "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.972.8.tgz", - "integrity": "sha512-1eD4uhTDeambO/PNIDVG19A6+v4NdD7xzwLHDutHsUqz0B+i661MwQB2eYO4/crcCvCiQG4SRm1k81k54FEIvw==", + "version": "3.972.11", + "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.972.11.tgz", + "integrity": "sha512-6Q8B1dcx6BBqUTY1Mc/eROKA0FImEEY5VPSd6AGPEUf0ErjExz4snVqa9kNJSoVDV1rKaNf3qrWojgcKW+SdDg==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "^3.973.6", - "@smithy/config-resolver": "^4.4.11", - "@smithy/node-config-provider": "^4.3.12", - "@smithy/types": "^4.13.1", + "@aws-sdk/types": "^3.973.7", + "@smithy/config-resolver": "^4.4.14", + "@smithy/node-config-provider": "^4.3.13", + "@smithy/types": "^4.14.0", "tslib": "^2.6.2" }, "engines": { @@ -772,17 +902,17 @@ } }, "node_modules/@aws-sdk/token-providers": { - "version": "3.1012.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.1012.0.tgz", - "integrity": "sha512-vzKwy020zjuiF4WTJzejx5nYcXJnRhHpb6i3lyZHIwfFwXG1yX4bzBVNMWYWF+bz1i2Pp2VhJbPyzpqj4VuJXQ==", + "version": "3.1030.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.1030.0.tgz", + "integrity": "sha512-gUuCLTnEiUgpxHEnJSidxZZlQ+rQwc/mrijz6DxeMijTwS3/e3UfJvL8C1YDvcbt8MkkXj92h0MpYtfhR+EGeg==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.973.21", - "@aws-sdk/nested-clients": "^3.996.11", - "@aws-sdk/types": "^3.973.6", - "@smithy/property-provider": "^4.2.12", - "@smithy/shared-ini-file-loader": "^4.4.7", - "@smithy/types": "^4.13.1", + "@aws-sdk/core": "^3.973.27", + "@aws-sdk/nested-clients": "^3.996.19", + "@aws-sdk/types": "^3.973.7", + "@smithy/property-provider": "^4.2.13", + "@smithy/shared-ini-file-loader": "^4.4.8", + "@smithy/types": "^4.14.0", "tslib": "^2.6.2" }, "engines": { @@ -790,12 +920,12 @@ } }, "node_modules/@aws-sdk/types": { - "version": "3.973.6", - "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.973.6.tgz", - "integrity": "sha512-Atfcy4E++beKtwJHiDln2Nby8W/mam64opFPTiHEqgsthqeydFS1pY+OUlN1ouNOmf8ArPU/6cDS65anOP3KQw==", + "version": "3.973.7", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.973.7.tgz", + "integrity": "sha512-reXRwoJ6CfChoqAsBszUYajAF8Z2LRE+CRcKocvFSMpIiLOtYU3aJ9trmn6VVPAzbbY5LXF+FfmUslbXk1SYFg==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.13.1", + "@smithy/types": "^4.14.0", "tslib": "^2.6.2" }, "engines": { @@ -815,15 +945,30 @@ } }, "node_modules/@aws-sdk/util-endpoints": { - "version": "3.996.5", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.996.5.tgz", - "integrity": "sha512-Uh93L5sXFNbyR5sEPMzUU8tJ++Ku97EY4udmC01nB8Zu+xfBPwpIwJ6F7snqQeq8h2pf+8SGN5/NoytfKgYPIw==", + "version": "3.996.6", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.996.6.tgz", + "integrity": "sha512-2nUQ+2ih7CShuKHpGSIYvvAIOHy52dOZguYG36zptBukhw6iFwcvGfG0tes0oZFWQqEWvgZe9HLWaNlvXGdOrg==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "^3.973.6", - "@smithy/types": "^4.13.1", - "@smithy/url-parser": "^4.2.12", - "@smithy/util-endpoints": "^3.3.3", + "@aws-sdk/types": "^3.973.7", + "@smithy/types": "^4.14.0", + "@smithy/url-parser": "^4.2.13", + "@smithy/util-endpoints": "^3.3.4", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/util-format-url": { + "version": "3.972.9", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-format-url/-/util-format-url-3.972.9.tgz", + "integrity": "sha512-fNJXHrs0ZT7Wx0KGIqKv7zLxlDXt2vqjx9z6oKUQFmpE5o4xxnSryvVHfHpIifYHWKz94hFccIldJ0YSZjlCBw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.7", + "@smithy/querystring-builder": "^4.2.13", + "@smithy/types": "^4.14.0", "tslib": "^2.6.2" }, "engines": { @@ -843,27 +988,27 @@ } }, "node_modules/@aws-sdk/util-user-agent-browser": { - "version": "3.972.8", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.972.8.tgz", - "integrity": "sha512-B3KGXJviV2u6Cdw2SDY2aDhoJkVfY/Q/Trwk2CMSkikE1Oi6gRzxhvhIfiRpHfmIsAhV4EA54TVEX8K6CbHbkA==", + "version": "3.972.9", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.972.9.tgz", + "integrity": "sha512-sn/LMzTbGjYqCCF24390WxPd6hkpoSptiUn5DzVp4cD71yqw+yGEGm1YCxyEoPXyc8qciM8UzLJcZBFslxo5Uw==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "^3.973.6", - "@smithy/types": "^4.13.1", + "@aws-sdk/types": "^3.973.7", + "@smithy/types": "^4.14.0", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "node_modules/@aws-sdk/util-user-agent-node": { - "version": "3.973.8", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.973.8.tgz", - "integrity": "sha512-Kvb96TafGPLYo4Z2GRCzQTne77epXgiZEo0DDXwavzkWmgDV/1XD1tMA766gzRcHHFUraWsE+4T8DKtPTZUxgQ==", + "version": "3.973.15", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.973.15.tgz", + "integrity": "sha512-fYn3s9PtKdgQkczGZCFMgkNEe8aq1JCVbnRqjqN9RSVW43xn2RV9xdcZ3z01a48Jpkuh/xCmBKJxdLOo4Ozg7w==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/middleware-user-agent": "^3.972.22", - "@aws-sdk/types": "^3.973.6", - "@smithy/node-config-provider": "^4.3.12", - "@smithy/types": "^4.13.1", + "@aws-sdk/middleware-user-agent": "^3.972.29", + "@aws-sdk/types": "^3.973.7", + "@smithy/node-config-provider": "^4.3.13", + "@smithy/types": "^4.14.0", "@smithy/util-config-provider": "^4.2.2", "tslib": "^2.6.2" }, @@ -880,13 +1025,13 @@ } }, "node_modules/@aws-sdk/xml-builder": { - "version": "3.972.13", - "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.13.tgz", - "integrity": "sha512-I/+BMxM4WE/6xL0tyV7tAUDOAXmyw/va1oGr/eSly43HmLUcD1G+v96vEKAA8VoLcZ03ZQo/PWzjmN9zQErqPQ==", + "version": "3.972.17", + "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.17.tgz", + "integrity": "sha512-Ra7hjqAZf1OXRRMueB13qex7mFJRDK/pgCvdSFemXBT8KCGnQDPoKzHY1SjN+TjJVmnpSF14W5tJ1vDamFu+Gg==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.13.1", - "fast-xml-parser": "5.5.6", + "@smithy/types": "^4.14.0", + "fast-xml-parser": "5.5.8", "tslib": "^2.6.2" }, "engines": { @@ -2673,16 +2818,16 @@ } }, "node_modules/@smithy/config-resolver": { - "version": "4.4.12", - "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-4.4.12.tgz", - "integrity": "sha512-XXzYyCQmyv2bISMFxu9WbB0XvUQGU0XytjXR8KBthEAWMXtVo4wxRLwZDwMtdSPq9oDWx+LpiHQbtKoLvl/szA==", + "version": "4.4.15", + "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-4.4.15.tgz", + "integrity": "sha512-BJdMBY5YO9iHh+lPLYdHv6LbX+J8IcPCYMl1IJdBt2KDWNHwONHrPVHk3ttYBqJd9wxv84wlbN0f7GlQzcQtNQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/node-config-provider": "^4.3.12", - "@smithy/types": "^4.13.1", + "@smithy/node-config-provider": "^4.3.13", + "@smithy/types": "^4.14.0", "@smithy/util-config-provider": "^4.2.2", - "@smithy/util-endpoints": "^3.3.3", - "@smithy/util-middleware": "^4.2.12", + "@smithy/util-endpoints": "^3.4.0", + "@smithy/util-middleware": "^4.2.13", "tslib": "^2.6.2" }, "engines": { @@ -2690,18 +2835,18 @@ } }, "node_modules/@smithy/core": { - "version": "3.23.12", - "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.23.12.tgz", - "integrity": "sha512-o9VycsYNtgC+Dy3I0yrwCqv9CWicDnke0L7EVOrZtJpjb2t0EjaEofmMrYc0T1Kn3yk32zm6cspxF9u9Bj7e5w==", + "version": "3.23.14", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.23.14.tgz", + "integrity": "sha512-vJ0IhpZxZAkFYOegMKSrxw7ujhhT2pass/1UEcZ4kfl5srTAqtPU5I7MdYQoreVas3204ykCiNhY1o7Xlz6Yyg==", "license": "Apache-2.0", "dependencies": { - "@smithy/protocol-http": "^5.3.12", - "@smithy/types": "^4.13.1", - "@smithy/url-parser": "^4.2.12", + "@smithy/protocol-http": "^5.3.13", + "@smithy/types": "^4.14.0", + "@smithy/url-parser": "^4.2.13", "@smithy/util-base64": "^4.3.2", "@smithy/util-body-length-browser": "^4.2.2", - "@smithy/util-middleware": "^4.2.12", - "@smithy/util-stream": "^4.5.20", + "@smithy/util-middleware": "^4.2.13", + "@smithy/util-stream": "^4.5.22", "@smithy/util-utf8": "^4.2.2", "@smithy/uuid": "^1.1.2", "tslib": "^2.6.2" @@ -2711,15 +2856,15 @@ } }, "node_modules/@smithy/credential-provider-imds": { - "version": "4.2.12", - "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.2.12.tgz", - "integrity": "sha512-cr2lR792vNZcYMriSIj+Um3x9KWrjcu98kn234xA6reOAFMmbRpQMOv8KPgEmLLtx3eldU6c5wALKFqNOhugmg==", + "version": "4.2.13", + "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.2.13.tgz", + "integrity": "sha512-wboCPijzf6RJKLOvnjDAiBxGSmSnGXj35o5ZAWKDaHa/cvQ5U3ZJ13D4tMCE8JG4dxVAZFy/P0x/V9CwwdfULQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/node-config-provider": "^4.3.12", - "@smithy/property-provider": "^4.2.12", - "@smithy/types": "^4.13.1", - "@smithy/url-parser": "^4.2.12", + "@smithy/node-config-provider": "^4.3.13", + "@smithy/property-provider": "^4.2.13", + "@smithy/types": "^4.14.0", + "@smithy/url-parser": "^4.2.13", "tslib": "^2.6.2" }, "engines": { @@ -2727,13 +2872,13 @@ } }, "node_modules/@smithy/eventstream-codec": { - "version": "4.2.12", - "resolved": "https://registry.npmjs.org/@smithy/eventstream-codec/-/eventstream-codec-4.2.12.tgz", - "integrity": "sha512-FE3bZdEl62ojmy8x4FHqxq2+BuOHlcxiH5vaZ6aqHJr3AIZzwF5jfx8dEiU/X0a8RboyNDjmXjlbr8AdEyLgiA==", + "version": "4.2.13", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-codec/-/eventstream-codec-4.2.13.tgz", + "integrity": "sha512-vYahwBAtRaAcFbOmE9aLr12z7RiHYDSLcnogSdxfm7kKfsNa3wH+NU5r7vTeB5rKvLsWyPjVX8iH94brP7umiQ==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/crc32": "5.2.0", - "@smithy/types": "^4.13.1", + "@smithy/types": "^4.14.0", "@smithy/util-hex-encoding": "^4.2.2", "tslib": "^2.6.2" }, @@ -2742,13 +2887,13 @@ } }, "node_modules/@smithy/eventstream-serde-browser": { - "version": "4.2.12", - "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-browser/-/eventstream-serde-browser-4.2.12.tgz", - "integrity": "sha512-XUSuMxlTxV5pp4VpqZf6Sa3vT/Q75FVkLSpSSE3KkWBvAQWeuWt1msTv8fJfgA4/jcJhrbrbMzN1AC/hvPmm5A==", + "version": "4.2.13", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-browser/-/eventstream-serde-browser-4.2.13.tgz", + "integrity": "sha512-wwybfcOX0tLqCcBP378TIU9IqrDuZq/tDV48LlZNydMpCnqnYr+hWBAYbRE+rFFf/p7IkDJySM3bgiMKP2ihPg==", "license": "Apache-2.0", "dependencies": { - "@smithy/eventstream-serde-universal": "^4.2.12", - "@smithy/types": "^4.13.1", + "@smithy/eventstream-serde-universal": "^4.2.13", + "@smithy/types": "^4.14.0", "tslib": "^2.6.2" }, "engines": { @@ -2756,12 +2901,12 @@ } }, "node_modules/@smithy/eventstream-serde-config-resolver": { - "version": "4.3.12", - "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-config-resolver/-/eventstream-serde-config-resolver-4.3.12.tgz", - "integrity": "sha512-7epsAZ3QvfHkngz6RXQYseyZYHlmWXSTPOfPmXkiS+zA6TBNo1awUaMFL9vxyXlGdoELmCZyZe1nQE+imbmV+Q==", + "version": "4.3.13", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-config-resolver/-/eventstream-serde-config-resolver-4.3.13.tgz", + "integrity": "sha512-ied1lO559PtAsMJzg2TKRlctLnEi1PfkNeMMpdwXDImk1zV9uvS/Oxoy/vcy9uv1GKZAjDAB5xT6ziE9fzm5wA==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.13.1", + "@smithy/types": "^4.14.0", "tslib": "^2.6.2" }, "engines": { @@ -2769,13 +2914,13 @@ } }, "node_modules/@smithy/eventstream-serde-node": { - "version": "4.2.12", - "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-node/-/eventstream-serde-node-4.2.12.tgz", - "integrity": "sha512-D1pFuExo31854eAvg89KMn9Oab/wEeJR6Buy32B49A9Ogdtx5fwZPqBHUlDzaCDpycTFk2+fSQgX689Qsk7UGA==", + "version": "4.2.13", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-node/-/eventstream-serde-node-4.2.13.tgz", + "integrity": "sha512-hFyK+ORJrxAN3RYoaD6+gsGDQjeix8HOEkosoajvXYZ4VeqonM3G4jd9IIRm/sWGXUKmudkY9KdYjzosUqdM8A==", "license": "Apache-2.0", "dependencies": { - "@smithy/eventstream-serde-universal": "^4.2.12", - "@smithy/types": "^4.13.1", + "@smithy/eventstream-serde-universal": "^4.2.13", + "@smithy/types": "^4.14.0", "tslib": "^2.6.2" }, "engines": { @@ -2783,13 +2928,13 @@ } }, "node_modules/@smithy/eventstream-serde-universal": { - "version": "4.2.12", - "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-universal/-/eventstream-serde-universal-4.2.12.tgz", - "integrity": "sha512-+yNuTiyBACxOJUTvbsNsSOfH9G9oKbaJE1lNL3YHpGcuucl6rPZMi3nrpehpVOVR2E07YqFFmtwpImtpzlouHQ==", + "version": "4.2.13", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-universal/-/eventstream-serde-universal-4.2.13.tgz", + "integrity": "sha512-kRrq4EKLGeOxhC2CBEhRNcu1KSzNJzYY7RK3S7CxMPgB5dRrv55WqQOtRwQxQLC04xqORFLUgnDlc6xrNUULaA==", "license": "Apache-2.0", "dependencies": { - "@smithy/eventstream-codec": "^4.2.12", - "@smithy/types": "^4.13.1", + "@smithy/eventstream-codec": "^4.2.13", + "@smithy/types": "^4.14.0", "tslib": "^2.6.2" }, "engines": { @@ -2797,14 +2942,14 @@ } }, "node_modules/@smithy/fetch-http-handler": { - "version": "5.3.15", - "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.3.15.tgz", - "integrity": "sha512-T4jFU5N/yiIfrtrsb9uOQn7RdELdM/7HbyLNr6uO/mpkj1ctiVs7CihVr51w4LyQlXWDpXFn4BElf1WmQvZu/A==", + "version": "5.3.16", + "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.3.16.tgz", + "integrity": "sha512-nYDRUIvNd4mFmuXraRWt6w5UsZTNqtj4hXJA/iiOD4tuseIdLP9Lq38teH/SZTcIFCa2f+27o7hYpIsWktJKEQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/protocol-http": "^5.3.12", - "@smithy/querystring-builder": "^4.2.12", - "@smithy/types": "^4.13.1", + "@smithy/protocol-http": "^5.3.13", + "@smithy/querystring-builder": "^4.2.13", + "@smithy/types": "^4.14.0", "@smithy/util-base64": "^4.3.2", "tslib": "^2.6.2" }, @@ -2828,12 +2973,12 @@ } }, "node_modules/@smithy/hash-node": { - "version": "4.2.12", - "resolved": "https://registry.npmjs.org/@smithy/hash-node/-/hash-node-4.2.12.tgz", - "integrity": "sha512-QhBYbGrbxTkZ43QoTPrK72DoYviDeg6YKDrHTMJbbC+A0sml3kSjzFtXP7BtbyJnXojLfTQldGdUR0RGD8dA3w==", + "version": "4.2.13", + "resolved": "https://registry.npmjs.org/@smithy/hash-node/-/hash-node-4.2.13.tgz", + "integrity": "sha512-4/oy9h0jjmY80a2gOIo75iLl8TOPhmtx4E2Hz+PfMjvx/vLtGY4TMU/35WRyH2JHPfT5CVB38u4JRow7gnmzJA==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.13.1", + "@smithy/types": "^4.14.0", "@smithy/util-buffer-from": "^4.2.2", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" @@ -2857,12 +3002,12 @@ } }, "node_modules/@smithy/invalid-dependency": { - "version": "4.2.12", - "resolved": "https://registry.npmjs.org/@smithy/invalid-dependency/-/invalid-dependency-4.2.12.tgz", - "integrity": "sha512-/4F1zb7Z8LOu1PalTdESFHR0RbPwHd3FcaG1sI3UEIriQTWakysgJr65lc1jj6QY5ye7aFsisajotH6UhWfm/g==", + "version": "4.2.13", + "resolved": "https://registry.npmjs.org/@smithy/invalid-dependency/-/invalid-dependency-4.2.13.tgz", + "integrity": "sha512-jvC0RB/8BLj2SMIkY0Npl425IdnxZJxInpZJbu563zIRnVjpDMXevU3VMCRSabaLB0kf/eFIOusdGstrLJ8IDg==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.13.1", + "@smithy/types": "^4.14.0", "tslib": "^2.6.2" }, "engines": { @@ -2896,13 +3041,13 @@ } }, "node_modules/@smithy/middleware-content-length": { - "version": "4.2.12", - "resolved": "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-4.2.12.tgz", - "integrity": "sha512-YE58Yz+cvFInWI/wOTrB+DbvUVz/pLn5mC5MvOV4fdRUc6qGwygyngcucRQjAhiCEbmfLOXX0gntSIcgMvAjmA==", + "version": "4.2.13", + "resolved": "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-4.2.13.tgz", + "integrity": "sha512-IPMLm/LE4AZwu6qiE8Rr8vJsWhs9AtOdySRXrOM7xnvclp77Tyh7hMs/FRrMf26kgIe67vFJXXOSmVxS7oKeig==", "license": "Apache-2.0", "dependencies": { - "@smithy/protocol-http": "^5.3.12", - "@smithy/types": "^4.13.1", + "@smithy/protocol-http": "^5.3.13", + "@smithy/types": "^4.14.0", "tslib": "^2.6.2" }, "engines": { @@ -2910,18 +3055,18 @@ } }, "node_modules/@smithy/middleware-endpoint": { - "version": "4.4.26", - "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-4.4.26.tgz", - "integrity": "sha512-8Qfikvd2GVKSm8S6IbjfwFlRY9VlMrj0Dp4vTwAuhqbX7NhJKE5DQc2bnfJIcY0B+2YKMDBWfvexbSZeejDgeg==", + "version": "4.4.29", + "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-4.4.29.tgz", + "integrity": "sha512-R9Q/58U+qBiSARGWbAbFLczECg/RmysRksX6Q8BaQEpt75I7LI6WGDZnjuC9GXSGKljEbA7N118LhGaMbfrTXw==", "license": "Apache-2.0", "dependencies": { - "@smithy/core": "^3.23.12", - "@smithy/middleware-serde": "^4.2.15", - "@smithy/node-config-provider": "^4.3.12", - "@smithy/shared-ini-file-loader": "^4.4.7", - "@smithy/types": "^4.13.1", - "@smithy/url-parser": "^4.2.12", - "@smithy/util-middleware": "^4.2.12", + "@smithy/core": "^3.23.14", + "@smithy/middleware-serde": "^4.2.17", + "@smithy/node-config-provider": "^4.3.13", + "@smithy/shared-ini-file-loader": "^4.4.8", + "@smithy/types": "^4.14.0", + "@smithy/url-parser": "^4.2.13", + "@smithy/util-middleware": "^4.2.13", "tslib": "^2.6.2" }, "engines": { @@ -2929,18 +3074,19 @@ } }, "node_modules/@smithy/middleware-retry": { - "version": "4.4.43", - "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.4.43.tgz", - "integrity": "sha512-ZwsifBdyuNHrFGmbc7bAfP2b54+kt9J2rhFd18ilQGAB+GDiP4SrawqyExbB7v455QVR7Psyhb2kjULvBPIhvA==", + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.5.1.tgz", + "integrity": "sha512-/zY+Gp7Qj2D2hVm3irkCyONER7E9MiX3cUUm/k2ZmhkzZkrPgwVS4aJ5NriZUEN/M0D1hhjrgjUmX04HhRwdWA==", "license": "Apache-2.0", "dependencies": { - "@smithy/node-config-provider": "^4.3.12", - "@smithy/protocol-http": "^5.3.12", - "@smithy/service-error-classification": "^4.2.12", - "@smithy/smithy-client": "^4.12.6", - "@smithy/types": "^4.13.1", - "@smithy/util-middleware": "^4.2.12", - "@smithy/util-retry": "^4.2.12", + "@smithy/core": "^3.23.14", + "@smithy/node-config-provider": "^4.3.13", + "@smithy/protocol-http": "^5.3.13", + "@smithy/service-error-classification": "^4.2.13", + "@smithy/smithy-client": "^4.12.9", + "@smithy/types": "^4.14.0", + "@smithy/util-middleware": "^4.2.13", + "@smithy/util-retry": "^4.3.1", "@smithy/uuid": "^1.1.2", "tslib": "^2.6.2" }, @@ -2949,14 +3095,14 @@ } }, "node_modules/@smithy/middleware-serde": { - "version": "4.2.15", - "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-4.2.15.tgz", - "integrity": "sha512-ExYhcltZSli0pgAKOpQQe1DLFBLryeZ22605y/YS+mQpdNWekum9Ujb/jMKfJKgjtz1AZldtwA/wCYuKJgjjlg==", + "version": "4.2.17", + "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-4.2.17.tgz", + "integrity": "sha512-0T2mcaM6v9W1xku86Dk0bEW7aEseG6KenFkPK98XNw0ZhOqOiD1MrMsdnQw9QsL3/Oa85T53iSMlm0SZdSuIEQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/core": "^3.23.12", - "@smithy/protocol-http": "^5.3.12", - "@smithy/types": "^4.13.1", + "@smithy/core": "^3.23.14", + "@smithy/protocol-http": "^5.3.13", + "@smithy/types": "^4.14.0", "tslib": "^2.6.2" }, "engines": { @@ -2964,12 +3110,12 @@ } }, "node_modules/@smithy/middleware-stack": { - "version": "4.2.12", - "resolved": "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-4.2.12.tgz", - "integrity": "sha512-kruC5gRHwsCOuyCd4ouQxYjgRAym2uDlCvQ5acuMtRrcdfg7mFBg6blaxcJ09STpt3ziEkis6bhg1uwrWU7txw==", + "version": "4.2.13", + "resolved": "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-4.2.13.tgz", + "integrity": "sha512-g72jN/sGDLyTanrCLH9fhg3oysO3f7tQa6eWWsMyn2BiYNCgjF24n4/I9wff/5XidFvjj9ilipAoQrurTUrLvw==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.13.1", + "@smithy/types": "^4.14.0", "tslib": "^2.6.2" }, "engines": { @@ -2977,14 +3123,14 @@ } }, "node_modules/@smithy/node-config-provider": { - "version": "4.3.12", - "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-4.3.12.tgz", - "integrity": "sha512-tr2oKX2xMcO+rBOjobSwVAkV05SIfUKz8iI53rzxEmgW3GOOPOv0UioSDk+J8OpRQnpnhsO3Af6IEBabQBVmiw==", + "version": "4.3.13", + "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-4.3.13.tgz", + "integrity": "sha512-iGxQ04DsKXLckbgnX4ipElrOTk+IHgTyu0q0WssZfYhDm9CQWHmu6cOeI5wmWRxpXbBDhIIfXMWz5tPEtcVqbw==", "license": "Apache-2.0", "dependencies": { - "@smithy/property-provider": "^4.2.12", - "@smithy/shared-ini-file-loader": "^4.4.7", - "@smithy/types": "^4.13.1", + "@smithy/property-provider": "^4.2.13", + "@smithy/shared-ini-file-loader": "^4.4.8", + "@smithy/types": "^4.14.0", "tslib": "^2.6.2" }, "engines": { @@ -2992,15 +3138,14 @@ } }, "node_modules/@smithy/node-http-handler": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.5.0.tgz", - "integrity": "sha512-Rnq9vQWiR1+/I6NZZMNzJHV6pZYyEHt2ZnuV3MG8z2NNenC4i/8Kzttz7CjZiHSmsN5frhXhg17z3Zqjjhmz1A==", + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.5.2.tgz", + "integrity": "sha512-/oD7u8M0oj2ZTFw7GkuuHWpIxtWdLlnyNkbrWcyVYhd5RJNDuczdkb0wfnQICyNFrVPlr8YHOhamjNy3zidhmA==", "license": "Apache-2.0", "dependencies": { - "@smithy/abort-controller": "^4.2.12", - "@smithy/protocol-http": "^5.3.12", - "@smithy/querystring-builder": "^4.2.12", - "@smithy/types": "^4.13.1", + "@smithy/protocol-http": "^5.3.13", + "@smithy/querystring-builder": "^4.2.13", + "@smithy/types": "^4.14.0", "tslib": "^2.6.2" }, "engines": { @@ -3008,12 +3153,12 @@ } }, "node_modules/@smithy/property-provider": { - "version": "4.2.12", - "resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-4.2.12.tgz", - "integrity": "sha512-jqve46eYU1v7pZ5BM+fmkbq3DerkSluPr5EhvOcHxygxzD05ByDRppRwRPPpFrsFo5yDtCYLKu+kreHKVrvc7A==", + "version": "4.2.13", + "resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-4.2.13.tgz", + "integrity": "sha512-bGzUCthxRmezuxkbu9wD33wWg9KX3hJpCXpQ93vVkPrHn9ZW6KNNdY5xAUWNuRCwQ+VyboFuWirG1lZhhkcyRQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.13.1", + "@smithy/types": "^4.14.0", "tslib": "^2.6.2" }, "engines": { @@ -3021,12 +3166,12 @@ } }, "node_modules/@smithy/protocol-http": { - "version": "5.3.12", - "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-5.3.12.tgz", - "integrity": "sha512-fit0GZK9I1xoRlR4jXmbLhoN0OdEpa96ul8M65XdmXnxXkuMxM0Y8HDT0Fh0Xb4I85MBvBClOzgSrV1X2s1Hxw==", + "version": "5.3.13", + "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-5.3.13.tgz", + "integrity": "sha512-+HsmuJUF4u8POo6s8/a2Yb/AQ5t/YgLovCuHF9oxbocqv+SZ6gd8lC2duBFiCA/vFHoHQhoq7QjqJqZC6xOxxg==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.13.1", + "@smithy/types": "^4.14.0", "tslib": "^2.6.2" }, "engines": { @@ -3034,12 +3179,12 @@ } }, "node_modules/@smithy/querystring-builder": { - "version": "4.2.12", - "resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-4.2.12.tgz", - "integrity": "sha512-6wTZjGABQufekycfDGMEB84BgtdOE/rCVTov+EDXQ8NHKTUNIp/j27IliwP7tjIU9LR+sSzyGBOXjeEtVgzCHg==", + "version": "4.2.13", + "resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-4.2.13.tgz", + "integrity": "sha512-tG4aOYFCZdPMjbgfhnIQ322H//ojujldp1SrHPHpBSb3NqgUp3dwiUGRJzie87hS1DYwWGqDuPaowoDF+rYCbQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.13.1", + "@smithy/types": "^4.14.0", "@smithy/util-uri-escape": "^4.2.2", "tslib": "^2.6.2" }, @@ -3048,12 +3193,12 @@ } }, "node_modules/@smithy/querystring-parser": { - "version": "4.2.12", - "resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-4.2.12.tgz", - "integrity": "sha512-P2OdvrgiAKpkPNKlKUtWbNZKB1XjPxM086NeVhK+W+wI46pIKdWBe5QyXvhUm3MEcyS/rkLvY8rZzyUdmyDZBw==", + "version": "4.2.13", + "resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-4.2.13.tgz", + "integrity": "sha512-hqW3Q4P+CDzUyQ87GrboGMeD7XYNMOF+CuTwu936UQRB/zeYn3jys8C3w+wMkDfY7CyyyVwZQ5cNFoG0x1pYmA==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.13.1", + "@smithy/types": "^4.14.0", "tslib": "^2.6.2" }, "engines": { @@ -3061,24 +3206,24 @@ } }, "node_modules/@smithy/service-error-classification": { - "version": "4.2.12", - "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-4.2.12.tgz", - "integrity": "sha512-LlP29oSQN0Tw0b6D0Xo6BIikBswuIiGYbRACy5ujw/JgWSzTdYj46U83ssf6Ux0GyNJVivs2uReU8pt7Eu9okQ==", + "version": "4.2.13", + "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-4.2.13.tgz", + "integrity": "sha512-a0s8XZMfOC/qpqq7RCPvJlk93rWFrElH6O++8WJKz0FqnA4Y7fkNi/0mnGgSH1C4x6MFsuBA8VKu4zxFrMe5Vw==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.13.1" + "@smithy/types": "^4.14.0" }, "engines": { "node": ">=18.0.0" } }, "node_modules/@smithy/shared-ini-file-loader": { - "version": "4.4.7", - "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-4.4.7.tgz", - "integrity": "sha512-HrOKWsUb+otTeo1HxVWeEb99t5ER1XrBi/xka2Wv6NVmTbuCUC1dvlrksdvxFtODLBjsC+PHK+fuy2x/7Ynyiw==", + "version": "4.4.8", + "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-4.4.8.tgz", + "integrity": "sha512-VZCZx2bZasxdqxVgEAhREvDSlkatTPnkdWy1+Kiy8w7kYPBosW0V5IeDwzDUMvWBt56zpK658rx1cOBFOYaPaw==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.13.1", + "@smithy/types": "^4.14.0", "tslib": "^2.6.2" }, "engines": { @@ -3086,16 +3231,16 @@ } }, "node_modules/@smithy/signature-v4": { - "version": "5.3.12", - "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.3.12.tgz", - "integrity": "sha512-B/FBwO3MVOL00DaRSXfXfa/TRXRheagt/q5A2NM13u7q+sHS59EOVGQNfG7DkmVtdQm5m3vOosoKAXSqn/OEgw==", + "version": "5.3.13", + "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.3.13.tgz", + "integrity": "sha512-YpYSyM0vMDwKbHD/JA7bVOF6kToVRpa+FM5ateEVRpsTNu564g1muBlkTubXhSKKYXInhpADF46FPyrZcTLpXg==", "license": "Apache-2.0", "dependencies": { "@smithy/is-array-buffer": "^4.2.2", - "@smithy/protocol-http": "^5.3.12", - "@smithy/types": "^4.13.1", + "@smithy/protocol-http": "^5.3.13", + "@smithy/types": "^4.14.0", "@smithy/util-hex-encoding": "^4.2.2", - "@smithy/util-middleware": "^4.2.12", + "@smithy/util-middleware": "^4.2.13", "@smithy/util-uri-escape": "^4.2.2", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" @@ -3105,17 +3250,17 @@ } }, "node_modules/@smithy/smithy-client": { - "version": "4.12.6", - "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-4.12.6.tgz", - "integrity": "sha512-aib3f0jiMsJ6+cvDnXipBsGDL7ztknYSVqJs1FdN9P+u9tr/VzOR7iygSh6EUOdaBeMCMSh3N0VdyYsG4o91DQ==", + "version": "4.12.9", + "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-4.12.9.tgz", + "integrity": "sha512-ovaLEcTU5olSeHcRXcxV6viaKtpkHZumn6Ps0yn7dRf2rRSfy794vpjOtrWDO0d1auDSvAqxO+lyhERSXQ03EQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/core": "^3.23.12", - "@smithy/middleware-endpoint": "^4.4.26", - "@smithy/middleware-stack": "^4.2.12", - "@smithy/protocol-http": "^5.3.12", - "@smithy/types": "^4.13.1", - "@smithy/util-stream": "^4.5.20", + "@smithy/core": "^3.23.14", + "@smithy/middleware-endpoint": "^4.4.29", + "@smithy/middleware-stack": "^4.2.13", + "@smithy/protocol-http": "^5.3.13", + "@smithy/types": "^4.14.0", + "@smithy/util-stream": "^4.5.22", "tslib": "^2.6.2" }, "engines": { @@ -3123,9 +3268,9 @@ } }, "node_modules/@smithy/types": { - "version": "4.13.1", - "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.13.1.tgz", - "integrity": "sha512-787F3yzE2UiJIQ+wYW1CVg2odHjmaWLGksnKQHUrK/lYZSEcy1msuLVvxaR/sI2/aDe9U+TBuLsXnr3vod1g0g==", + "version": "4.14.0", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.14.0.tgz", + "integrity": "sha512-OWgntFLW88kx2qvf/c/67Vno1yuXm/f9M7QFAtVkkO29IJXGBIg0ycEaBTH0kvCtwmvZxRujrgP5a86RvsXJAQ==", "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" @@ -3135,13 +3280,13 @@ } }, "node_modules/@smithy/url-parser": { - "version": "4.2.12", - "resolved": "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-4.2.12.tgz", - "integrity": "sha512-wOPKPEpso+doCZGIlr+e1lVI6+9VAKfL4kZWFgzVgGWY2hZxshNKod4l2LXS3PRC9otH/JRSjtEHqQ/7eLciRA==", + "version": "4.2.13", + "resolved": "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-4.2.13.tgz", + "integrity": "sha512-2G03yoboIRZlZze2+PT4GZEjgwQsJjUgn6iTsvxA02bVceHR6vp4Cuk7TUnPFWKF+ffNUk3kj4COwkENS2K3vw==", "license": "Apache-2.0", "dependencies": { - "@smithy/querystring-parser": "^4.2.12", - "@smithy/types": "^4.13.1", + "@smithy/querystring-parser": "^4.2.13", + "@smithy/types": "^4.14.0", "tslib": "^2.6.2" }, "engines": { @@ -3212,14 +3357,14 @@ } }, "node_modules/@smithy/util-defaults-mode-browser": { - "version": "4.3.42", - "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.3.42.tgz", - "integrity": "sha512-0vjwmcvkWAUtikXnWIUOyV6IFHTEeQUYh3JUZcDgcszF+hD/StAsQ3rCZNZEPHgI9kVNcbnyc8P2CBHnwgmcwg==", + "version": "4.3.45", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.3.45.tgz", + "integrity": "sha512-ag9sWc6/nWZAuK3Wm9KlFJUnRkXLrXn33RFjIAmCTFThqLHY+7wCst10BGq56FxslsDrjhSie46c8OULS+BiIw==", "license": "Apache-2.0", "dependencies": { - "@smithy/property-provider": "^4.2.12", - "@smithy/smithy-client": "^4.12.6", - "@smithy/types": "^4.13.1", + "@smithy/property-provider": "^4.2.13", + "@smithy/smithy-client": "^4.12.9", + "@smithy/types": "^4.14.0", "tslib": "^2.6.2" }, "engines": { @@ -3227,17 +3372,17 @@ } }, "node_modules/@smithy/util-defaults-mode-node": { - "version": "4.2.46", - "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.2.46.tgz", - "integrity": "sha512-0uDAEJ7r8ny1e4YjK4+UB3VEG53t64ovPaQ9Zu6rXwRuXj1KXaKX6FtrPr7SmTxISQrYykShtBxEiQQViuqk/g==", + "version": "4.2.50", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.2.50.tgz", + "integrity": "sha512-xpjncL5XozFA3No7WypTsPU1du0fFS8flIyO+Wh2nhCy7bpEapvU7BR55Bg+wrfw+1cRA+8G8UsTjaxgzrMzXg==", "license": "Apache-2.0", "dependencies": { - "@smithy/config-resolver": "^4.4.12", - "@smithy/credential-provider-imds": "^4.2.12", - "@smithy/node-config-provider": "^4.3.12", - "@smithy/property-provider": "^4.2.12", - "@smithy/smithy-client": "^4.12.6", - "@smithy/types": "^4.13.1", + "@smithy/config-resolver": "^4.4.15", + "@smithy/credential-provider-imds": "^4.2.13", + "@smithy/node-config-provider": "^4.3.13", + "@smithy/property-provider": "^4.2.13", + "@smithy/smithy-client": "^4.12.9", + "@smithy/types": "^4.14.0", "tslib": "^2.6.2" }, "engines": { @@ -3245,13 +3390,13 @@ } }, "node_modules/@smithy/util-endpoints": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/@smithy/util-endpoints/-/util-endpoints-3.3.3.tgz", - "integrity": "sha512-VACQVe50j0HZPjpwWcjyT51KUQ4AnsvEaQ2lKHOSL4mNLD0G9BjEniQ+yCt1qqfKfiAHRAts26ud7hBjamrwig==", + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/@smithy/util-endpoints/-/util-endpoints-3.4.0.tgz", + "integrity": "sha512-QQHGPKkw6NPcU6TJ1rNEEa201srPtZiX4k61xL163vvs9sTqW/XKz+UEuJ00uvPqoN+5Rs4Ka1UJ7+Mp03IXJw==", "license": "Apache-2.0", "dependencies": { - "@smithy/node-config-provider": "^4.3.12", - "@smithy/types": "^4.13.1", + "@smithy/node-config-provider": "^4.3.13", + "@smithy/types": "^4.14.0", "tslib": "^2.6.2" }, "engines": { @@ -3271,12 +3416,12 @@ } }, "node_modules/@smithy/util-middleware": { - "version": "4.2.12", - "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-4.2.12.tgz", - "integrity": "sha512-Er805uFUOvgc0l8nv0e0su0VFISoxhJ/AwOn3gL2NWNY2LUEldP5WtVcRYSQBcjg0y9NfG8JYrCJaYDpupBHJQ==", + "version": "4.2.13", + "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-4.2.13.tgz", + "integrity": "sha512-GTooyrlmRTqvUen4eK7/K1p6kryF7bnDfq6XsAbIsf2mo51B/utaH+XThY6dKgNCWzMAaH/+OLmqaBuLhLWRow==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.13.1", + "@smithy/types": "^4.14.0", "tslib": "^2.6.2" }, "engines": { @@ -3284,13 +3429,13 @@ } }, "node_modules/@smithy/util-retry": { - "version": "4.2.12", - "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-4.2.12.tgz", - "integrity": "sha512-1zopLDUEOwumjcHdJ1mwBHddubYF8GMQvstVCLC54Y46rqoHwlIU+8ZzUeaBcD+WCJHyDGSeZ2ml9YSe9aqcoQ==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-4.3.1.tgz", + "integrity": "sha512-FwmicpgWOkP5kZUjN3y+3JIom8NLGqSAJBeoIgK0rIToI817TEBHCrd0A2qGeKQlgDeP+Jzn4i0H/NLAXGy9uQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/service-error-classification": "^4.2.12", - "@smithy/types": "^4.13.1", + "@smithy/service-error-classification": "^4.2.13", + "@smithy/types": "^4.14.0", "tslib": "^2.6.2" }, "engines": { @@ -3298,14 +3443,14 @@ } }, "node_modules/@smithy/util-stream": { - "version": "4.5.20", - "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-4.5.20.tgz", - "integrity": "sha512-4yXLm5n/B5SRBR2p8cZ90Sbv4zL4NKsgxdzCzp/83cXw2KxLEumt5p+GAVyRNZgQOSrzXn9ARpO0lUe8XSlSDw==", + "version": "4.5.22", + "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-4.5.22.tgz", + "integrity": "sha512-3H8iq/0BfQjUs2/4fbHZ9aG9yNzcuZs24LPkcX1Q7Z+qpqaGM8+qbGmE8zo9m2nCRgamyvS98cHdcWvR6YUsew==", "license": "Apache-2.0", "dependencies": { - "@smithy/fetch-http-handler": "^5.3.15", - "@smithy/node-http-handler": "^4.5.0", - "@smithy/types": "^4.13.1", + "@smithy/fetch-http-handler": "^5.3.16", + "@smithy/node-http-handler": "^4.5.2", + "@smithy/types": "^4.14.0", "@smithy/util-base64": "^4.3.2", "@smithy/util-buffer-from": "^4.2.2", "@smithy/util-hex-encoding": "^4.2.2", @@ -4838,9 +4983,9 @@ } }, "node_modules/fast-xml-parser": { - "version": "5.5.6", - "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.5.6.tgz", - "integrity": "sha512-3+fdZyBRVg29n4rXP0joHthhcHdPUHaIC16cuyyd1iLsuaO6Vea36MPrxgAzbZna8lhvZeRL8Bc9GP56/J9xEw==", + "version": "5.5.8", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.5.8.tgz", + "integrity": "sha512-Z7Fh2nVQSb2d+poDViM063ix2ZGt9jmY1nWhPfHBOK2Hgnb/OW3P4Et3P/81SEej0J7QbWtJqxO05h8QYfK7LQ==", "funding": [ { "type": "github", @@ -4850,8 +4995,8 @@ "license": "MIT", "dependencies": { "fast-xml-builder": "^1.1.4", - "path-expression-matcher": "^1.1.3", - "strnum": "^2.1.2" + "path-expression-matcher": "^1.2.0", + "strnum": "^2.2.0" }, "bin": { "fxparser": "src/cli/cli.js" @@ -7524,9 +7669,9 @@ } }, "node_modules/path-expression-matcher": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/path-expression-matcher/-/path-expression-matcher-1.1.3.tgz", - "integrity": "sha512-qdVgY8KXmVdJZRSS1JdEPOKPdTiEK/pi0RkcT2sw1RhXxohdujUlJFPuS1TSkevZ9vzd3ZlL7ULl1MHGTApKzQ==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/path-expression-matcher/-/path-expression-matcher-1.5.0.tgz", + "integrity": "sha512-cbrerZV+6rvdQrrD+iGMcZFEiiSrbv9Tfdkvnusy6y0x0GKBXREFg/Y65GhIfm0tnLntThhzCnfKwp1WRjeCyQ==", "funding": [ { "type": "github", diff --git a/package.json b/package.json index ee2c75d..4853e54 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "backfill:tenant-organization": "node scripts/backfill-tenant-organization.js" }, "dependencies": { + "@aws-sdk/client-bedrock-runtime": "^3.1030.0", "@aws-sdk/client-s3": "^3.1012.0", "@aws-sdk/lib-storage": "^3.1012.0", "@hapi/hapi": "^21.3.0", @@ -24,9 +25,9 @@ "joi": "^17.13.0", "jsonwebtoken": "^9.0.2", "mongoose": "^8.0.0", - "razorpay": "^2.9.6", - "otplib": "^12.0.1", "nodemailer": "^6.9.7", + "otplib": "^12.0.1", + "razorpay": "^2.9.6", "winston": "^3.11.0" }, "devDependencies": { diff --git a/scripts/backfill-join-codes.js b/scripts/backfill-join-codes.js index b9b645e..08d05cb 100644 --- a/scripts/backfill-join-codes.js +++ b/scripts/backfill-join-codes.js @@ -1,5 +1,5 @@ /** - * One-off: set joinCode on all sessions and tracks (same algorithm as eventBuzzCode util). + * One-off: set joinCode on all sessions, tracks, and events (same algorithm as eventBuzzCode util). * Run: cd backend && npm run backfill:join-codes */ const path = require('path'); @@ -8,6 +8,7 @@ require('dotenv').config({ path: path.join(__dirname, '..', '.env') }); const mongoose = require('mongoose'); const Session = require(path.join(__dirname, '..', 'src', 'models', 'Session')); const Track = require(path.join(__dirname, '..', 'src', 'models', 'Track')); +const Event = require(path.join(__dirname, '..', 'src', 'models', 'Event')); const { eventBuzzCodeFor } = require(path.join(__dirname, '..', 'src', 'utils', 'eventBuzzCode')); async function run() { @@ -36,6 +37,15 @@ async function run() { } console.log(`Backfilled joinCode for ${nt} track(s).`); + const events = await Event.find({}).select('_id').lean(); + let ne = 0; + for (const ev of events) { + const jc = eventBuzzCodeFor(ev._id.toString(), 'event'); + await Event.updateOne({ _id: ev._id }, { $set: { joinCode: jc } }); + ne += 1; + } + console.log(`Backfilled joinCode for ${ne} event(s).`); + await mongoose.disconnect(); } diff --git a/src/config/constants.js b/src/config/constants.js index d098f9e..e54ba08 100644 --- a/src/config/constants.js +++ b/src/config/constants.js @@ -42,7 +42,19 @@ const WEBSITE_SECTION_TYPES = [ 'tickets', ]; const DESIGN_TEMPLATE_TYPES = ['brochure', 'microsite', 'badge', 'emailer', 'certificate']; -const REWARD_SOURCES = ['registration', 'attendance', 'feedback', 'quiz', 'referral', 'manual']; +const REWARD_SOURCES = [ + 'registration', + 'attendance', + 'feedback', + 'quiz', + 'referral', + 'manual', + 'question_ask', + 'question_vote', + 'quiz_poll', + 'poll', + 'rating', +]; module.exports = { ROLES, diff --git a/src/controllers/event.controller.js b/src/controllers/event.controller.js index 0cf46f2..2e0355b 100644 --- a/src/controllers/event.controller.js +++ b/src/controllers/event.controller.js @@ -5,6 +5,12 @@ const { success, error } = require('../utils/response'); const { resolveScopedOrganizationId } = require('../utils/tenantListScope'); const { uploadBufferToS3AndGetCloudFrontUrl } = require('../utils/s3Media'); const { registrationUrlForForm } = require('../utils/micrositeBuild'); +const { + extractEventFieldsFromDescription, + EVENT_TYPES: AI_EVENT_TYPES, + EVENT_STATUS: AI_EVENT_STATUS, + LOCATION_TYPES: AI_LOCATION_TYPES, +} = require('../utils/bedrockExtractEvent'); function parseDataUrl(dataUrl) { if (typeof dataUrl !== 'string') return null; @@ -92,34 +98,59 @@ async function assignCreatorToEventStaff(creatorUserId, orgId, eventId) { await map.save(); } -const list = async (request, h) => { - const { organization: organizationQuery, status, type, page = 1, limit = 20 } = request.query; - const user = request.auth.credentials.user; - const scoped = resolveScopedOrganizationId(user, organizationQuery); - if (scoped.error) { - return h.response(error(scoped.error.code, scoped.error.message)).code(400); - } - const filter = { organization: scoped.organizationId }; - if (status) filter.status = status; - if (type) filter.type = type; +function defaultAiDateRange() { + const start = new Date(); + start.setUTCDate(start.getUTCDate() + 7); + start.setUTCHours(10, 0, 0, 0); + const end = new Date(start.getTime()); + end.setUTCHours(18, 0, 0, 0); + return { start, end }; +} - const skip = (parseInt(page, 10) - 1) * parseInt(limit, 10); - const [events, total] = await Promise.all([ - Event.find(filter).populate('organization').sort({ startDate: -1 }).skip(skip).limit(parseInt(limit, 10)).lean(), - Event.countDocuments(filter), - ]); - return h.response(success({ events, total, page: parseInt(page, 10), limit: parseInt(limit, 10) })) - .header('X-Total-Count', total); -}; +function mergeAiExtractedPayload(ai) { + const { start: ds, end: de } = defaultAiDateRange(); + const norm = (v) => (typeof v === 'string' ? v.toLowerCase().trim() : ''); + const type = AI_EVENT_TYPES.includes(norm(ai.type)) ? norm(ai.type) : 'conference'; + const status = AI_EVENT_STATUS.includes(norm(ai.status)) ? norm(ai.status) : 'draft'; + const locType = AI_LOCATION_TYPES.includes(norm(ai.locationType)) ? norm(ai.locationType) : 'physical'; -const create = async (request, h) => { - const user = request.auth.credentials.user; - const orgId = request.payload.organization || (user.organization?._id || user.organization); - if (!orgId) return h.response(error('VALIDATION_ERROR', 'Organization required')).code(400); - if (user.role !== 'super_admin' && (user.organization?._id?.toString() !== orgId?.toString())) { - return h.response(error('FORBIDDEN', 'Cannot create event for this organization')).code(403); + let name = typeof ai.name === 'string' ? ai.name.trim() : ''; + if (name.length < 3) { + const fromDesc = typeof ai.description === 'string' ? ai.description.trim().slice(0, 80) : ''; + name = (fromDesc || 'New event').slice(0, 100); + if (name.length < 3) name = 'New event'; } - const { metadata: metaIn, ...rest } = request.payload; + + let startDate = ai.startDate ? new Date(ai.startDate) : ds; + let endDate = ai.endDate ? new Date(ai.endDate) : de; + if (Number.isNaN(startDate.getTime())) startDate = ds; + if (Number.isNaN(endDate.getTime())) endDate = de; + if (endDate <= startDate) { + endDate = new Date(startDate.getTime() + 8 * 60 * 60 * 1000); + } + + const subtitle = typeof ai.subtitle === 'string' ? ai.subtitle : ''; + const description = typeof ai.description === 'string' ? ai.description : ''; + const location = typeof ai.location === 'string' ? ai.location : ''; + const virtualLink = typeof ai.virtualLink === 'string' ? ai.virtualLink : ''; + const tags = Array.isArray(ai.tags) ? ai.tags.filter((t) => typeof t === 'string') : []; + + return { + name, + subtitle, + description, + type, + status, + startDate, + endDate, + location, + locationType: locType, + virtualLink, + tags, + }; +} + +async function persistEventWithSideEffects(user, orgId, rest, metaIn) { const event = await Event.create({ ...rest, organization: orgId, @@ -131,7 +162,6 @@ const create = async (request, h) => { }, }); - // Convert base64 images to CloudFront URLs. const eventId = event._id?.toString?.() || event._id; let didUpdate = false; if (rest.image != null) { @@ -169,6 +199,61 @@ const create = async (request, h) => { console.error('assignCreatorToEventStaff', err); } + return event; +} + +const list = async (request, h) => { + const { organization: organizationQuery, status, type, page = 1, limit = 20 } = request.query; + const user = request.auth.credentials.user; + const scoped = resolveScopedOrganizationId(user, organizationQuery); + if (scoped.error) { + return h.response(error(scoped.error.code, scoped.error.message)).code(400); + } + const filter = { organization: scoped.organizationId }; + if (status) filter.status = status; + if (type) filter.type = type; + + const skip = (parseInt(page, 10) - 1) * parseInt(limit, 10); + const [events, total] = await Promise.all([ + Event.find(filter).populate('organization').sort({ startDate: -1 }).skip(skip).limit(parseInt(limit, 10)).lean(), + Event.countDocuments(filter), + ]); + return h.response(success({ events, total, page: parseInt(page, 10), limit: parseInt(limit, 10) })) + .header('X-Total-Count', total); +}; + +const create = async (request, h) => { + const user = request.auth.credentials.user; + const orgId = request.payload.organization || (user.organization?._id || user.organization); + if (!orgId) return h.response(error('VALIDATION_ERROR', 'Organization required')).code(400); + if (user.role !== 'super_admin' && (user.organization?._id?.toString() !== orgId?.toString())) { + return h.response(error('FORBIDDEN', 'Cannot create event for this organization')).code(403); + } + const { metadata: metaIn, ...rest } = request.payload; + const event = await persistEventWithSideEffects(user, orgId, rest, metaIn); + return h.response(success(event, 'Event created', 201)).code(201); +}; + +const generateFromAi = async (request, h) => { + const user = request.auth.credentials.user; + const orgId = request.payload.organization || (user.organization?._id || user.organization); + if (!orgId) return h.response(error('VALIDATION_ERROR', 'Organization required')).code(400); + if (user.role !== 'super_admin' && (user.organization?._id?.toString() !== orgId?.toString())) { + return h.response(error('FORBIDDEN', 'Cannot create event for this organization')).code(403); + } + + const { description } = request.payload; + let extracted; + try { + extracted = await extractEventFieldsFromDescription(description); + } catch (err) { + console.error('extractEventFieldsFromDescription', err); + const msg = err.message || 'AI extraction failed'; + return h.response(error('BEDROCK_ERROR', msg)).code(502); + } + + const merged = mergeAiExtractedPayload(extracted); + const event = await persistEventWithSideEffects(user, orgId, merged, undefined); return h.response(success(event, 'Event created', 201)).code(201); }; @@ -355,4 +440,14 @@ const registrationLink = async (request, h) => { return h.response(success({ url })); }; -module.exports = { list, create, get, update, remove, analytics, duplicate, registrationLink }; +module.exports = { + list, + create, + generateFromAi, + get, + update, + remove, + analytics, + duplicate, + registrationLink, +}; diff --git a/src/controllers/question.controller.js b/src/controllers/question.controller.js index 40e05ea..cb062b6 100644 --- a/src/controllers/question.controller.js +++ b/src/controllers/question.controller.js @@ -66,6 +66,30 @@ const update = async (request, h) => { return h.response(success(question)); }; +const PATCH_KEYS = ['question', 'isAnonymous', 'shortlisted', 'audienceListed', 'status']; + +const patch = async (request, h) => { + const question = await Question.findById(request.params.id); + if (!question) return h.response(error('NOT_FOUND', 'Question not found')).code(404); + const event = await Event.findById(question.event); + const user = request.auth.credentials.user; + if (user.role !== 'super_admin' && event.organization?.toString() !== (user.organization?._id || user.organization)?.toString()) { + return h.response(error('FORBIDDEN', 'Access denied')).code(403); + } + const payload = request.payload || {}; + for (const key of PATCH_KEYS) { + if (Object.prototype.hasOwnProperty.call(payload, key)) { + question[key] = payload[key]; + } + } + await question.save(); + if (question.session) { + const lean = await Question.findById(question._id).populate('participant').lean(); + await syncSessionQuestionToFirebase(lean); + } + return h.response(success(question)); +}; + const remove = async (request, h) => { const question = await Question.findById(request.params.id); if (!question) return h.response(error('NOT_FOUND', 'Question not found')).code(404); @@ -127,4 +151,4 @@ const sessionQuestions = async (request, h) => { return h.response(success({ questions })); }; -module.exports = { list, create, get, update, remove, upvote, answer, sessionQuestions }; +module.exports = { list, create, get, update, patch, remove, upvote, answer, sessionQuestions }; diff --git a/src/controllers/rewardPoint.controller.js b/src/controllers/rewardPoint.controller.js index 6627e1d..7cbded1 100644 --- a/src/controllers/rewardPoint.controller.js +++ b/src/controllers/rewardPoint.controller.js @@ -83,14 +83,33 @@ const leaderboard = async (request, h) => { if (user.role !== 'super_admin' && event.organization?.toString() !== (user.organization?._id || user.organization)?.toString()) { return h.response(error('FORBIDDEN', 'Access denied')).code(403); } + const lim = Math.min(500, Math.max(1, parseInt(request.query.limit, 10) || 100)); const aggregated = await RewardPoint.aggregate([ - { $match: { event: event._id, type: 'earned' } }, - { $group: { _id: '$participant', total: { $sum: '$points' } } }, + { $match: { event: event._id, type: { $in: ['earned', 'redeemed'] } } }, + { + $group: { + _id: '$participant', + earned: { $sum: { $cond: [{ $eq: ['$type', 'earned'] }, '$points', 0] } }, + redeemed: { $sum: { $cond: [{ $eq: ['$type', 'redeemed'] }, '$points', 0] } }, + }, + }, + { $project: { participantId: '$_id', total: { $subtract: ['$earned', '$redeemed'] }, earned: 1, redeemed: 1 } }, + { $match: { total: { $gt: 0 } } }, { $sort: { total: -1 } }, - { $limit: 100 }, + { $limit: lim }, { $lookup: { from: 'participants', localField: '_id', foreignField: '_id', as: 'p' } }, { $unwind: '$p' }, - { $project: { participantId: '$_id', total: 1, firstName: '$p.firstName', lastName: '$p.lastName', email: '$p.email' } }, + { + $project: { + participantId: '$_id', + total: 1, + earned: 1, + redeemed: 1, + firstName: '$p.firstName', + lastName: '$p.lastName', + email: '$p.email', + }, + }, ]); return h.response(success({ leaderboard: aggregated })); }; diff --git a/src/controllers/session.controller.js b/src/controllers/session.controller.js index c2c4fae..08973e4 100644 --- a/src/controllers/session.controller.js +++ b/src/controllers/session.controller.js @@ -1,6 +1,8 @@ +const mongoose = require('mongoose'); const Session = require('../models/Session'); const Event = require('../models/Event'); const Track = require('../models/Track'); +const Form = require('../models/Form'); const { success, error } = require('../utils/response'); function normalizeSessionPayload(payload, eventId) { @@ -8,9 +10,32 @@ function normalizeSessionPayload(payload, eventId) { if (!body.track || String(body.track).trim() === '') { delete body.track; } + for (const key of ['audienceFeedbackForm', 'audienceRatingsForm']) { + if (key in body && (body[key] === '' || body[key] === undefined)) body[key] = null; + } return body; } +async function assertFeedbackFormForEvent(formId, eventId) { + if (formId == null || formId === '') return null; + if (!mongoose.Types.ObjectId.isValid(String(formId))) { + throw Object.assign(new Error('Invalid form id'), { code: 400 }); + } + const [event, form] = await Promise.all([ + Event.findById(eventId).select('organization').lean(), + Form.findById(String(formId)).select('tenantOrganization type').lean(), + ]); + if (!event) throw Object.assign(new Error('Event not found'), { code: 404 }); + if (!form) throw Object.assign(new Error('Form not found'), { code: 400 }); + if (form.type !== 'feedback') { + throw Object.assign(new Error('Only feedback-type forms can be linked here'), { code: 400 }); + } + if (String(form.tenantOrganization) !== String(event.organization)) { + throw Object.assign(new Error('Form does not belong to this event organization'), { code: 400 }); + } + return String(formId); +} + const list = async (request, h) => { const { eventId } = request.params; const event = await Event.findById(eventId); @@ -36,6 +61,17 @@ const create = async (request, h) => { const tr = await Track.findOne({ _id: body.track, event: eventId }); if (!tr) return h.response(error('VALIDATION_ERROR', 'Track does not belong to this event')).code(400); } + try { + if (body.audienceFeedbackForm !== undefined && body.audienceFeedbackForm !== null) { + body.audienceFeedbackForm = await assertFeedbackFormForEvent(body.audienceFeedbackForm, eventId); + } + if (body.audienceRatingsForm !== undefined && body.audienceRatingsForm !== null) { + body.audienceRatingsForm = await assertFeedbackFormForEvent(body.audienceRatingsForm, eventId); + } + } catch (err) { + const code = err.code === 404 ? 404 : 400; + return h.response(error('VALIDATION_ERROR', err.message || 'Invalid form')).code(code); + } const session = await Session.create({ ...body, event: eventId }); const populated = await Session.findById(session._id).populate('speakers moderator track').lean(); return h.response(success(populated, 'Session created', 201)).code(201); @@ -60,12 +96,23 @@ const update = async (request, h) => { if (user.role !== 'super_admin' && event.organization?.toString() !== (user.organization?._id || user.organization)?.toString()) { return h.response(error('FORBIDDEN', 'Access denied')).code(403); } - const raw = { ...request.payload }; + const raw = normalizeSessionPayload({ ...request.payload }, String(session.event)); const trackVal = raw.track && String(raw.track).trim() ? raw.track : null; if (trackVal) { const tr = await Track.findOne({ _id: trackVal, event: session.event }); if (!tr) return h.response(error('VALIDATION_ERROR', 'Track does not belong to this event')).code(400); } + try { + if (raw.audienceFeedbackForm !== undefined) { + raw.audienceFeedbackForm = await assertFeedbackFormForEvent(raw.audienceFeedbackForm, String(session.event)); + } + if (raw.audienceRatingsForm !== undefined) { + raw.audienceRatingsForm = await assertFeedbackFormForEvent(raw.audienceRatingsForm, String(session.event)); + } + } catch (err) { + const code = err.code === 404 ? 404 : 400; + return h.response(error('VALIDATION_ERROR', err.message || 'Invalid form')).code(code); + } delete raw.track; Object.assign(session, raw); session.track = trackVal; diff --git a/src/controllers/visitor.controller.js b/src/controllers/visitor.controller.js index 46f2706..84c6bbd 100644 --- a/src/controllers/visitor.controller.js +++ b/src/controllers/visitor.controller.js @@ -2,6 +2,7 @@ const mongoose = require('mongoose'); const Session = require('../models/Session'); const Track = require('../models/Track'); const Event = require('../models/Event'); +const Form = require('../models/Form'); const EventMicrosite = require('../models/EventMicrosite'); const Question = require('../models/Question'); const RewardPoint = require('../models/RewardPoint'); @@ -12,6 +13,9 @@ const { success, error } = require('../utils/response'); const { sendMicrositeContactEmail } = require('../utils/mail'); const { syncSessionQuestionToFirebase } = require('../utils/sessionQuestionFirebaseSync'); const { participantInEvent } = require('../utils/participantMembership'); +const { mergeRules } = require('../utils/rewardRulesResolve'); +const { isObjectiveQuestionCorrect } = require('../utils/quizLoyaltyScore'); +const { awardIfNew } = require('../services/rewardAward'); /** All event fields for JSON except website, timestamps; adds logoUrl from website.logo || image. Never expose inviteCode. */ function publicEventLoginPayload(doc) { @@ -94,6 +98,16 @@ async function resolveCode(request, h) { }), ); } + + const eventByJoin = await Event.findOne({ joinCode: sessionCode }).lean(); + if (eventByJoin) { + return h.response( + success({ + type: 'event', + eventId: eventByJoin._id.toString(), + }), + ); + } } const folder = normalizeFolderSlug(code); @@ -281,6 +295,12 @@ function publicSessionPreview(session) { joinCode: session.joinCode, speakers, moderator, + audienceFeedbackEnabled: session.audienceFeedbackEnabled !== false, + audienceRatingsEnabled: session.audienceRatingsEnabled !== false, + audienceFeedbackForm: + session.audienceFeedbackForm != null ? String(session.audienceFeedbackForm) : null, + audienceRatingsForm: + session.audienceRatingsForm != null ? String(session.audienceRatingsForm) : null, }; } @@ -367,7 +387,12 @@ async function listSessionQuestions(request, h) { if (!session || String(session.event) !== String(eventId)) { return h.response(error('NOT_FOUND', 'Session not found')).code(404); } - const rows = await Question.find({ session: sessionId, event: eventId }) + const rows = await Question.find({ + session: sessionId, + event: eventId, + audienceListed: { $ne: false }, + status: { $ne: 'archived' }, + }) .populate('participant') .sort({ createdAt: -1 }) .lean(); @@ -407,9 +432,33 @@ async function createSessionQuestion(request, h) { const populated = await Question.findById(doc._id).populate('participant').lean(); await syncSessionQuestionToFirebase(populated); - return h.response(success({ question: publicVisitorQuestion(populated) }, 'Question submitted', 201)).code(201); + let pointsEarned = 0; + if (participantId) { + const [eventLean, sessionLean] = await Promise.all([ + Event.findById(eventId).select('rewardRules').lean(), + Session.findById(sessionId).select('rewardRules').lean(), + ]); + const rules = mergeRules(eventLean?.rewardRules || {}, sessionLean?.rewardRules || {}, {}); + const ar = await awardIfNew({ + participantId, + eventId, + points: rules.questionAsk, + source: 'question_ask', + sourceId: `question_ask:${doc._id}`, + description: 'Asked a question', + metadata: { ruleSnapshot: rules, sessionId: String(sessionId) }, + }); + pointsEarned = ar.pointsAwarded || 0; + } + + const body = { + question: publicVisitorQuestion(populated), + ...(pointsEarned > 0 ? { reward: { pointsEarned } } : {}), + }; + return h.response(success(body, 'Question submitted', 201)).code(201); } +/** @returns {boolean} whether the vote state changed */ function applyAudienceVoteDoc(question, voterKey, direction) { const raw = question.audienceVotes; const votes = Array.isArray(raw) ? raw.map((v) => ({ key: String(v.key), dir: v.dir === 'down' ? 'down' : 'up' })) : []; @@ -418,7 +467,7 @@ function applyAudienceVoteDoc(question, voterKey, direction) { if (direction === 'up') { if (prev === 'up') { - return; + return false; } if (prev === 'down') { votes[idx].dir = 'up'; @@ -430,7 +479,7 @@ function applyAudienceVoteDoc(question, voterKey, direction) { } } else if (direction === 'down') { if (prev === 'down') { - return; + return false; } if (prev === 'up') { votes[idx].dir = 'down'; @@ -442,6 +491,7 @@ function applyAudienceVoteDoc(question, voterKey, direction) { } } question.audienceVotes = votes; + return true; } function audienceYourVote(question, voterKey) { @@ -490,13 +540,33 @@ async function voteSessionQuestion(request, h) { ? String(credParticipant._id) : String(voterKey).trim(); - applyAudienceVoteDoc(question, audienceKey, direction); + const voteChanged = applyAudienceVoteDoc(question, audienceKey, direction); await question.save(); const populated = await Question.findById(question._id).populate('participant').lean(); await syncSessionQuestionToFirebase(populated); + let pointsEarned = 0; + if (credParticipant && voteChanged) { + const [eventLean, sessionLean] = await Promise.all([ + Event.findById(eventId).select('rewardRules').lean(), + Session.findById(sessionId).select('rewardRules').lean(), + ]); + const rules = mergeRules(eventLean?.rewardRules || {}, sessionLean?.rewardRules || {}, {}); + const pts = direction === 'up' ? rules.questionUpvote : rules.questionDownvote; + const ar = await awardIfNew({ + participantId: credParticipant._id, + eventId, + points: pts, + source: 'question_vote', + sourceId: `question_vote:${question._id}:${credParticipant._id}:${direction}`, + description: direction === 'up' ? 'Upvoted a question' : 'Downvoted a question', + metadata: { direction, ruleSnapshot: rules, sessionId: String(sessionId) }, + }); + pointsEarned = ar.pointsAwarded || 0; + } + const yv = audienceYourVote(question, audienceKey); return h.response( success({ @@ -504,6 +574,7 @@ async function voteSessionQuestion(request, h) { up: question.upvotes ?? 0, down: question.downvotes ?? 0, yourVote: yv, + ...(pointsEarned > 0 ? { reward: { pointsEarned } } : {}), }), ); } @@ -544,7 +615,66 @@ async function submitVisitorQuizPollResponse(request, h) { totalScore: request.payload.totalScore, timeSpent: request.payload.timeSpent, }); - return h.response(success(doc, 'Response submitted', 201)).code(201); + + const eventLean = await Event.findById(eventId).select('rewardRules').lean(); + let sessionRules = {}; + if (quizPoll.session) { + const s = await Session.findById(quizPoll.session).select('rewardRules').lean(); + if (s) sessionRules = s.rewardRules || {}; + } + const quizRules = (quizPoll.settings && quizPoll.settings.rewardRules) || {}; + const rules = mergeRules(eventLean?.rewardRules || {}, sessionRules, quizRules); + const responseId = String(doc._id); + const typeLower = String(quizPoll.type || '').toLowerCase(); + + let pointsEarned = 0; + if (typeLower === 'quiz') { + const arSubmit = await awardIfNew({ + participantId: participant._id, + eventId, + points: rules.quizPollSubmitted, + source: 'quiz_poll', + sourceId: `quiz_poll_submit:${quizPollId}:${responseId}`, + description: 'Quiz submission', + metadata: { ruleSnapshot: rules, quizPollId: String(quizPollId) }, + }); + pointsEarned += arSubmit.pointsAwarded || 0; + const perCorrect = rules.quizCorrectPerQuestion; + if (perCorrect > 0) { + const questions = quizPoll.questions || []; + for (let i = 0; i < questions.length; i++) { + const item = (request.payload.responses || []).find((r) => Number(r.questionIndex) === i); + if (!isObjectiveQuestionCorrect(questions[i], item)) continue; + const arCor = await awardIfNew({ + participantId: participant._id, + eventId, + points: perCorrect, + source: 'quiz_poll', + sourceId: `quiz_correct:${quizPollId}:${responseId}:q:${i}`, + description: 'Correct quiz answer', + metadata: { quizPollId: String(quizPollId), questionIndex: i }, + }); + pointsEarned += arCor.pointsAwarded || 0; + } + } + } else if (typeLower === 'poll' || typeLower === 'survey') { + const arPoll = await awardIfNew({ + participantId: participant._id, + eventId, + points: rules.pollParticipation, + source: 'poll', + sourceId: `poll_submit:${quizPollId}:${responseId}`, + description: typeLower === 'survey' ? 'Survey submission' : 'Poll participation', + metadata: { ruleSnapshot: rules, quizPollId: String(quizPollId) }, + }); + pointsEarned += arPoll.pointsAwarded || 0; + } + + const responsePayload = { + ...(doc.toObject ? doc.toObject() : doc), + ...(pointsEarned > 0 ? { reward: { pointsEarned } } : {}), + }; + return h.response(success(responsePayload, 'Response submitted', 201)).code(201); } function normAnswerVal(ans) { @@ -736,6 +866,99 @@ async function putVisitorSessionBookmark(request, h) { return h.response(success({ sessionIds, bookmarked, sessionId: String(sessionId) })); } +async function participantLeaderboardSelf(eventId, participantId) { + const eid = new mongoose.Types.ObjectId(eventId); + const pid = new mongoose.Types.ObjectId(participantId); + const mineAgg = await RewardPoint.aggregate([ + { $match: { event: eid, participant: pid, type: { $in: ['earned', 'redeemed'] } } }, + { + $group: { + _id: null, + earned: { $sum: { $cond: [{ $eq: ['$type', 'earned'] }, '$points', 0] } }, + redeemed: { $sum: { $cond: [{ $eq: ['$type', 'redeemed'] }, '$points', 0] } }, + }, + }, + { $project: { total: { $subtract: ['$earned', '$redeemed'] } } }, + ]); + const total = mineAgg[0]?.total ?? 0; + if (total <= 0) return { total: 0, rank: null }; + const higherAgg = await RewardPoint.aggregate([ + { $match: { event: eid, type: { $in: ['earned', 'redeemed'] } } }, + { + $group: { + _id: '$participant', + earned: { $sum: { $cond: [{ $eq: ['$type', 'earned'] }, '$points', 0] } }, + redeemed: { $sum: { $cond: [{ $eq: ['$type', 'redeemed'] }, '$points', 0] } }, + }, + }, + { $project: { net: { $subtract: ['$earned', '$redeemed'] } } }, + { $match: { net: { $gt: total } } }, + { $count: 'n' }, + ]); + const rank = (higherAgg[0]?.n ?? 0) + 1; + return { total, rank }; +} + +/** GET …/visitor/events/{eventId}/leaderboard — public; optional JWT adds `your` rank + total */ +async function visitorEventLeaderboard(request, h) { + const { eventId } = request.params; + if (!mongoose.Types.ObjectId.isValid(eventId)) { + return h.response(error('VALIDATION_ERROR', 'Invalid event id')).code(400); + } + const event = await Event.findById(eventId).select('_id').lean(); + if (!event) return h.response(error('NOT_FOUND', 'Event not found')).code(404); + const limit = Math.min(100, Math.max(5, parseInt(request.query.limit, 10) || 50)); + const eid = event._id; + + const aggregated = await RewardPoint.aggregate([ + { $match: { event: eid, type: { $in: ['earned', 'redeemed'] } } }, + { + $group: { + _id: '$participant', + earned: { $sum: { $cond: [{ $eq: ['$type', 'earned'] }, '$points', 0] } }, + redeemed: { $sum: { $cond: [{ $eq: ['$type', 'redeemed'] }, '$points', 0] } }, + }, + }, + { $project: { participantId: '$_id', total: { $subtract: ['$earned', '$redeemed'] } } }, + { $match: { total: { $gt: 0 } } }, + { $sort: { total: -1 } }, + { $limit: limit }, + { $lookup: { from: 'participants', localField: '_id', foreignField: '_id', as: 'p' } }, + { $unwind: '$p' }, + { + $project: { + participantId: '$_id', + total: 1, + firstName: '$p.firstName', + lastName: '$p.lastName', + displayName: '$p.displayName', + }, + }, + ]); + + const leaderboard = aggregated.map((row, idx) => { + const dn = [row.firstName, row.lastName].filter(Boolean).join(' ').trim(); + const label = dn || (row.displayName && String(row.displayName).trim()) || 'Participant'; + return { + rank: idx + 1, + participantId: String(row.participantId), + total: row.total, + displayName: label, + }; + }); + + let your = null; + const auth = request.auth; + if (auth && auth.isAuthenticated && auth.credentials && auth.credentials.participant) { + const p = auth.credentials.participant; + if (participantInEvent(p, eventId)) { + your = await participantLeaderboardSelf(eventId, p._id); + } + } + + return h.response(success({ leaderboard, your })); +} + /** GET …/visitor/events/{eventId}/reward-balance — attendee JWT; total points for this event only */ async function visitorEventRewardBalance(request, h) { const { eventId } = request.params; @@ -753,6 +976,85 @@ async function visitorEventRewardBalance(request, h) { return h.response(success({ total })); } +/** GET …/visitor/events/{eventId}/reward-history — attendee JWT; paginated ledger for this event */ +async function visitorEventRewardHistory(request, h) { + const { eventId } = request.params; + const participant = request.auth.credentials.participant; + if (!mongoose.Types.ObjectId.isValid(eventId)) { + return h.response(error('VALIDATION_ERROR', 'Invalid event id')).code(400); + } + if (!participantInEvent(participant, eventId)) { + return h.response(error('FORBIDDEN', 'Event mismatch')).code(403); + } + const page = Math.max(1, parseInt(request.query.page, 10) || 1); + const limit = Math.min(100, Math.max(1, parseInt(request.query.limit, 10) || 20)); + const skip = (page - 1) * limit; + const [items, total] = await Promise.all([ + RewardPoint.find({ participant: participant._id, event: eventId }) + .sort({ createdAt: -1 }) + .skip(skip) + .limit(limit) + .lean(), + RewardPoint.countDocuments({ participant: participant._id, event: eventId }), + ]); + const rows = items.map((p) => { + let metaObj; + const m = p.metadata; + if (m instanceof Map) metaObj = Object.fromEntries(m); + else if (m && typeof m === 'object') metaObj = { ...m }; + return { + _id: String(p._id), + points: p.points, + type: p.type, + source: p.source, + description: p.description, + createdAt: p.createdAt, + metadata: metaObj, + }; + }); + return h.response(success({ items: rows, total, page, limit })); +} + +/** GET …/visitor/events/{eventId}/feedback-forms/{formId} — public schema for rendering (org must match event). */ +async function visitorFeedbackFormPreview(request, h) { + const { eventId, formId } = request.params; + if (!mongoose.Types.ObjectId.isValid(eventId) || !mongoose.Types.ObjectId.isValid(formId)) { + return h.response(error('VALIDATION_ERROR', 'Invalid id')).code(400); + } + const [event, form] = await Promise.all([ + Event.findById(eventId).select('organization').lean(), + Form.findById(formId).select('tenantOrganization type name description fields settings').lean(), + ]); + if (!event) return h.response(error('NOT_FOUND', 'Event not found')).code(404); + if (!form) return h.response(error('NOT_FOUND', 'Form not found')).code(404); + if (form.type !== 'feedback') { + return h.response(error('VALIDATION_ERROR', 'Form is not a feedback form')).code(400); + } + if (String(form.tenantOrganization) !== String(event.organization)) { + return h.response(error('FORBIDDEN', 'Form is not available for this event')).code(403); + } + if (!form.settings?.isActive) { + return h.response(error('VALIDATION_ERROR', 'Form is not accepting responses')).code(400); + } + const fields = (form.fields || []).map((f) => ({ + label: f.label, + type: f.type, + required: !!f.required, + options: Array.isArray(f.options) ? f.options.map((o) => String(o)) : [], + placeholder: f.placeholder ? String(f.placeholder) : '', + })); + return h.response( + success({ + _id: String(form._id), + name: form.name, + description: form.description || '', + fields, + thankYouMessage: form.settings?.thankYouMessage || '', + showThankYou: form.settings?.showThankYou !== false, + }), + ); +} + async function submitMicrositeContact(request, h) { const p = request.payload; if (p._gotcha) return h.response(error('VALIDATION_ERROR', 'Invalid request')).code(400); @@ -789,5 +1091,8 @@ module.exports = { listVisitorSessionBookmarks, putVisitorSessionBookmark, visitorEventRewardBalance, + visitorEventRewardHistory, + visitorEventLeaderboard, submitMicrositeContact, + visitorFeedbackFormPreview, }; \ No newline at end of file diff --git a/src/controllers/visitorAuth.controller.js b/src/controllers/visitorAuth.controller.js index ce5f8ae..a3095cb 100644 --- a/src/controllers/visitorAuth.controller.js +++ b/src/controllers/visitorAuth.controller.js @@ -7,6 +7,25 @@ const VisitorAuthToken = require('../models/VisitorAuthToken'); const { success, error } = require('../utils/response'); const { sendVisitorOtpEmail, sendVisitorMagicLinkEmail } = require('../utils/mail'); const { getMembership, membershipGuestQuery } = require('../utils/participantMembership'); +const { mergeRules } = require('../utils/rewardRulesResolve'); +const { awardIfNew } = require('../services/rewardAward'); + +/** @returns {number} loyalty points actually awarded (0 if duplicate / disabled) */ +async function awardRegistrationIfNew(participant, eventId, eventLean) { + const rules = mergeRules(eventLean.rewardRules || {}, {}, {}); + const pts = rules.registrationComplete; + if (!participant?._id) return 0; + const r = await awardIfNew({ + participantId: participant._id, + eventId, + points: pts, + source: 'registration', + sourceId: `registration:${participant._id}:${eventId}`, + description: 'Event registration', + metadata: { ruleSnapshot: rules }, + }); + return r.pointsAwarded || 0; +} const ATTENDEE_SECRET = process.env.ATTENDEE_JWT_SECRET || process.env.JWT_SECRET || 'default-secret-change-in-production'; @@ -309,6 +328,7 @@ async function verifyOtp(request, h) { const orgId = event.organization; let participant = await Participant.findOne({ tenantOrganization: orgId, email: em }); const inEvent = participant && getMembership(participant, eventId); + let registrationRewardPoints = 0; if (!inEvent) { if (!allowsNewRegistration(event)) { return h.response(error('REGISTRATION_CLOSED', 'Registration is closed for this event')).code(403); @@ -335,6 +355,7 @@ async function verifyOtp(request, h) { }, ], }); + registrationRewardPoints = await awardRegistrationIfNew(participant, eventId, event); } else { participant.eventMemberships.push({ event: eventId, @@ -347,6 +368,7 @@ async function verifyOtp(request, h) { if (phone) participant.phone = String(phone).trim(); if (displayName) participant.displayName = String(displayName).trim(); await participant.save(); + registrationRewardPoints = await awardRegistrationIfNew(participant, eventId, event); } } else { participant.emailVerified = true; @@ -360,7 +382,10 @@ async function verifyOtp(request, h) { const joinType = doc.joinType || 'event'; const sid = doc.sessionId; const tid = doc.trackId; - return h.response(success(authPayload(participant, event, eventId, joinType, sid, tid))); + const base = authPayload(participant, event, eventId, joinType, sid, tid); + const payload = + registrationRewardPoints > 0 ? { ...base, reward: { pointsEarned: registrationRewardPoints } } : base; + return h.response(success(payload)); } async function startLink(request, h) { @@ -450,6 +475,7 @@ async function exchangeLink(request, h) { const orgId = event.organization; let participant = await Participant.findOne({ tenantOrganization: orgId, email: em }); const inEvent = participant && getMembership(participant, eventId); + let registrationRewardPoints = 0; if (!inEvent) { if (!allowsNewRegistration(event)) { return h.response(error('REGISTRATION_CLOSED', 'Registration is closed for this event')).code(403); @@ -470,6 +496,7 @@ async function exchangeLink(request, h) { }, ], }); + registrationRewardPoints = await awardRegistrationIfNew(participant, eventId, event); } else { participant.eventMemberships.push({ event: eventId, @@ -478,6 +505,7 @@ async function exchangeLink(request, h) { }); participant.emailVerified = true; await participant.save(); + registrationRewardPoints = await awardRegistrationIfNew(participant, eventId, event); } } else { participant.emailVerified = true; @@ -485,9 +513,10 @@ async function exchangeLink(request, h) { } const joinType = doc.joinType || 'event'; - return h.response( - success(authPayload(participant, event, eventId, joinType, doc.sessionId, doc.trackId)) - ); + const base = authPayload(participant, event, eventId, joinType, doc.sessionId, doc.trackId); + const payload = + registrationRewardPoints > 0 ? { ...base, reward: { pointsEarned: registrationRewardPoints } } : base; + return h.response(success(payload)); } async function guestStart(request, h) { @@ -542,6 +571,7 @@ async function guestStart(request, h) { }, ], }); + const registrationRewardPoints = await awardRegistrationIfNew(participant, eventId, event); const out = authPayload( participant, @@ -551,7 +581,11 @@ async function guestStart(request, h) { oidStr(sessionId), oidStr(trackId) ); - return h.response(success({ ...out, guestToken: newGuestToken })); + const payload = + registrationRewardPoints > 0 + ? { ...out, guestToken: newGuestToken, reward: { pointsEarned: registrationRewardPoints } } + : { ...out, guestToken: newGuestToken }; + return h.response(success(payload)); } async function registrationStatus(request, h) { diff --git a/src/models/Event.js b/src/models/Event.js index 1ea97a2..29e6421 100644 --- a/src/models/Event.js +++ b/src/models/Event.js @@ -1,4 +1,6 @@ const mongoose = require('mongoose'); +const { eventBuzzCodeFor } = require('../utils/eventBuzzCode'); +const { rewardRulesSubSchema } = require('./rewardRulesSchema'); const eventSchema = new mongoose.Schema( { @@ -32,6 +34,8 @@ const eventSchema = new mongoose.Schema( virtualLink: String, tags: [String], image: String, + /** Public join code for `/code/{code}` — same alphabet as session/track; resolves after session/track lookup. */ + joinCode: { type: String, trim: true, uppercase: true, unique: true, sparse: true }, settings: { registrationOpen: { type: Boolean, default: true }, maxParticipants: Number, @@ -48,6 +52,8 @@ const eventSchema = new mongoose.Schema( /** Optional shared code for Invite Only registration (visitor must pass inviteToken) */ inviteCode: String, }, + /** Loyalty point rules (defaults); session/quiz may override via mergeRules. */ + rewardRules: rewardRulesSubSchema, bannerImage: String, website: { enabled: { type: Boolean, default: false }, @@ -101,4 +107,17 @@ eventSchema.index({ organization: 1 }); eventSchema.index({ status: 1 }); eventSchema.index({ startDate: 1 }); eventSchema.index({ 'website.folder': 1 }); + +eventSchema.pre('save', function assignEventJoinCode(next) { + try { + const missing = !this.joinCode || !String(this.joinCode).trim(); + if (missing && this._id) { + this.joinCode = eventBuzzCodeFor(this._id.toString(), 'event'); + } + next(); + } catch (err) { + next(err); + } +}); + module.exports = mongoose.model('Event', eventSchema); diff --git a/src/models/Question.js b/src/models/Question.js index 91680c1..38de172 100644 --- a/src/models/Question.js +++ b/src/models/Question.js @@ -24,6 +24,10 @@ const questionSchema = new mongoose.Schema( enum: ['pending', 'answered', 'archived'], default: 'pending', }, + /** Moderator: highlighted for Q&A / speaker prep */ + shortlisted: { type: Boolean, default: false }, + /** When false, hidden from attendee question feeds (RTDB + visitor API). */ + audienceListed: { type: Boolean, default: true }, upvotes: { type: Number, default: 0 }, downvotes: { type: Number, default: 0 }, /** diff --git a/src/models/QuizPoll.js b/src/models/QuizPoll.js index bfe3dfa..3292e71 100644 --- a/src/models/QuizPoll.js +++ b/src/models/QuizPoll.js @@ -1,4 +1,5 @@ const mongoose = require('mongoose'); +const { rewardRulesSubSchema } = require('./rewardRulesSchema'); const questionOptionSchema = new mongoose.Schema({ text: String, @@ -48,6 +49,7 @@ const quizPollSchema = new mongoose.Schema( randomizeQuestions: { type: Boolean, default: false }, timeRestricted: { type: Boolean, default: false }, timeLimit: { type: Number, default: 0, min: 0 }, + rewardRules: rewardRulesSubSchema, }, deletedAt: { type: Date, default: null }, }, diff --git a/src/models/RewardPoint.js b/src/models/RewardPoint.js index 1d6689b..fb054c4 100644 --- a/src/models/RewardPoint.js +++ b/src/models/RewardPoint.js @@ -1,4 +1,5 @@ const mongoose = require('mongoose'); +const { REWARD_SOURCES } = require('../config/constants'); const rewardPointSchema = new mongoose.Schema( { @@ -20,9 +21,10 @@ const rewardPointSchema = new mongoose.Schema( }, source: { type: String, - enum: ['registration', 'attendance', 'feedback', 'quiz', 'referral', 'manual'], + enum: REWARD_SOURCES, }, - sourceId: mongoose.Schema.Types.Mixed, + /** Stable id for idempotent system awards (string). */ + sourceId: { type: String, trim: true }, description: String, expiresAt: Date, metadata: { type: Map, of: mongoose.Schema.Types.Mixed }, @@ -33,4 +35,13 @@ const rewardPointSchema = new mongoose.Schema( rewardPointSchema.index({ participant: 1, event: 1 }); rewardPointSchema.index({ event: 1 }); +rewardPointSchema.index( + { participant: 1, event: 1, source: 1, sourceId: 1 }, + { + unique: true, + partialFilterExpression: { + sourceId: { $type: 'string', $exists: true }, + }, + } +); module.exports = mongoose.model('RewardPoint', rewardPointSchema); diff --git a/src/models/Session.js b/src/models/Session.js index ec2521b..7d3bf0e 100644 --- a/src/models/Session.js +++ b/src/models/Session.js @@ -1,5 +1,6 @@ const mongoose = require('mongoose'); const { eventBuzzCodeFor } = require('../utils/eventBuzzCode'); +const { rewardRulesSubSchema } = require('./rewardRulesSchema'); const sessionSchema = new mongoose.Schema( { @@ -36,6 +37,15 @@ const sessionSchema = new mongoose.Schema( }, ], joinCode: { type: String, trim: true, uppercase: true, unique: true, sparse: true }, + rewardRules: rewardRulesSubSchema, + /** When true (default), session page can surface feedback links if the event allows feedback */ + audienceFeedbackEnabled: { type: Boolean, default: true }, + /** When true (default), session page can surface ratings if the event has ratings enabled */ + audienceRatingsEnabled: { type: Boolean, default: true }, + /** Optional Form (type feedback) for custom feedback; null = default EventBuzz feedback UI */ + audienceFeedbackForm: { type: mongoose.Schema.Types.ObjectId, ref: 'Form' }, + /** Optional Form (type feedback) for custom ratings flow; null = default EventBuzz ratings UI */ + audienceRatingsForm: { type: mongoose.Schema.Types.ObjectId, ref: 'Form' }, }, { timestamps: true } ); diff --git a/src/models/rewardRulesSchema.js b/src/models/rewardRulesSchema.js new file mode 100644 index 0000000..65926b3 --- /dev/null +++ b/src/models/rewardRulesSchema.js @@ -0,0 +1,19 @@ +const mongoose = require('mongoose'); + +/** Shared shape: event → session → quiz/poll overrides via mergeRules (quiz wins). */ +const rewardRulesSubSchema = new mongoose.Schema( + { + registrationComplete: Number, + questionAsk: Number, + questionUpvote: Number, + questionDownvote: Number, + quizPollSubmitted: Number, + quizCorrectPerQuestion: Number, + pollParticipation: Number, + feedbackFormSubmitted: Number, + ratingSubmitted: Number, + }, + { _id: false } +); + +module.exports = { rewardRulesSubSchema }; diff --git a/src/routes/event.routes.js b/src/routes/event.routes.js index f1f6b33..624f5cb 100644 --- a/src/routes/event.routes.js +++ b/src/routes/event.routes.js @@ -23,6 +23,18 @@ const registrationFieldSchema = Joi.object({ options: Joi.array().items(fieldOptionSchema), }); +const rewardRulesPayload = Joi.object({ + registrationComplete: Joi.number().min(0).allow(null), + questionAsk: Joi.number().min(0).allow(null), + questionUpvote: Joi.number().min(0).allow(null), + questionDownvote: Joi.number().min(0).allow(null), + quizPollSubmitted: Joi.number().min(0).allow(null), + quizCorrectPerQuestion: Joi.number().min(0).allow(null), + pollParticipation: Joi.number().min(0).allow(null), + feedbackFormSubmitted: Joi.number().min(0).allow(null), + ratingSubmitted: Joi.number().min(0).allow(null), +}).allow(null); + const eventPayload = Joi.object({ name: Joi.string().min(3).max(100).required(), subtitle: Joi.string().allow('', null), @@ -52,13 +64,28 @@ const eventPayload = Joi.object({ registrationBaseFields: Joi.string().valid(...REGISTRATION_BASE_FIELDS).allow('', null), inviteCode: Joi.string().allow('', null), }), + /** Align with Event.metadata in the model; allow extra keys from older clients. */ metadata: Joi.object({ + totalRevenue: Joi.number().allow(null), + rating: Joi.number().allow(null), + ratingCount: Joi.number().allow(null), registrationCustomFields: Joi.array().items(registrationFieldSchema), - }), + }).unknown(true), + rewardRules: rewardRulesPayload, +}); + +const generateFromAiPayload = Joi.object({ + description: Joi.string().min(10).max(8000).required(), + organization: Joi.string().allow('', null), }); module.exports = [ { method: 'GET', path: '/api/events', options: { handler: controller.list } }, + { + method: 'POST', + path: '/api/events/generate-from-ai', + options: { validate: { payload: generateFromAiPayload }, handler: controller.generateFromAi }, + }, { method: 'POST', path: '/api/events', options: { validate: { payload: eventPayload }, handler: controller.create } }, { method: 'GET', path: '/api/events/{id}', options: { handler: controller.get } }, { method: 'PUT', path: '/api/events/{id}', options: { validate: { payload: eventPayload }, handler: controller.update } }, diff --git a/src/routes/question.routes.js b/src/routes/question.routes.js index fe14d51..a987c1c 100644 --- a/src/routes/question.routes.js +++ b/src/routes/question.routes.js @@ -13,11 +13,26 @@ const answerPayload = Joi.object({ isOfficial: Joi.boolean(), }); +const questionPatchPayload = Joi.object({ + question: Joi.string().trim().min(1), + isAnonymous: Joi.boolean(), + shortlisted: Joi.boolean(), + audienceListed: Joi.boolean(), + status: Joi.string().valid('pending', 'answered', 'archived'), +}) + .min(1) + .messages({ 'object.min': 'At least one field is required' }); + module.exports = [ { method: 'GET', path: '/api/events/{eventId}/questions', options: { handler: controller.list } }, { method: 'POST', path: '/api/events/{eventId}/questions', options: { validate: { payload: questionPayload }, handler: controller.create } }, { method: 'GET', path: '/api/questions/{id}', options: { handler: controller.get } }, { method: 'PUT', path: '/api/questions/{id}', options: { validate: { payload: questionPayload }, handler: controller.update } }, + { + method: 'PATCH', + path: '/api/questions/{id}', + options: { validate: { payload: questionPatchPayload }, handler: controller.patch }, + }, { method: 'DELETE', path: '/api/questions/{id}', options: { handler: controller.remove } }, { method: 'POST', path: '/api/questions/{id}/upvote', options: { handler: controller.upvote } }, { method: 'POST', path: '/api/questions/{id}/answer', options: { validate: { payload: answerPayload }, handler: controller.answer } }, diff --git a/src/routes/quizPoll.routes.js b/src/routes/quizPoll.routes.js index 52662a0..ab67654 100644 --- a/src/routes/quizPoll.routes.js +++ b/src/routes/quizPoll.routes.js @@ -15,6 +15,18 @@ const quizQuestionSchema = Joi.object({ points: Joi.number(), required: Joi.boolean(), }); +const rewardRulesPayload = Joi.object({ + registrationComplete: Joi.number().min(0).allow(null), + questionAsk: Joi.number().min(0).allow(null), + questionUpvote: Joi.number().min(0).allow(null), + questionDownvote: Joi.number().min(0).allow(null), + quizPollSubmitted: Joi.number().min(0).allow(null), + quizCorrectPerQuestion: Joi.number().min(0).allow(null), + pollParticipation: Joi.number().min(0).allow(null), + feedbackFormSubmitted: Joi.number().min(0).allow(null), + ratingSubmitted: Joi.number().min(0).allow(null), +}).allow(null); + const quizPollPayload = Joi.object({ session: Joi.string(), title: Joi.string().required(), @@ -32,6 +44,7 @@ const quizPollPayload = Joi.object({ randomizeQuestions: Joi.boolean(), timeRestricted: Joi.boolean(), timeLimit: Joi.number().integer().min(0), + rewardRules: rewardRulesPayload, }), }); diff --git a/src/routes/rewardPoint.routes.js b/src/routes/rewardPoint.routes.js index 451060b..1c8fbaf 100644 --- a/src/routes/rewardPoint.routes.js +++ b/src/routes/rewardPoint.routes.js @@ -23,6 +23,16 @@ module.exports = [ { method: 'PUT', path: '/api/reward-points/{id}', options: { validate: { payload: rewardPayload }, handler: controller.update } }, { method: 'DELETE', path: '/api/reward-points/{id}', options: { handler: controller.remove } }, { method: 'GET', path: '/api/participants/{participantId}/reward-points', options: { handler: controller.byParticipant } }, - { method: 'GET', path: '/api/events/{eventId}/leaderboard', options: { handler: controller.leaderboard } }, + { + method: 'GET', + path: '/api/events/{eventId}/leaderboard', + options: { + validate: { + params: Joi.object({ eventId: Joi.string().required() }), + query: Joi.object({ limit: Joi.number().integer().min(1).max(500) }), + }, + handler: controller.leaderboard, + }, + }, { method: 'POST', path: '/api/events/{eventId}/reward-points/bulk', options: { validate: { payload: bulkPayload }, handler: controller.bulk } }, ]; diff --git a/src/routes/session.routes.js b/src/routes/session.routes.js index 5bfbeb4..da1f845 100644 --- a/src/routes/session.routes.js +++ b/src/routes/session.routes.js @@ -2,6 +2,18 @@ const Joi = require('joi'); const controller = require('../controllers/session.controller'); const { SESSION_TYPES, SESSION_STATUS } = require('../config/constants'); +const rewardRulesPayload = Joi.object({ + registrationComplete: Joi.number().min(0).allow(null), + questionAsk: Joi.number().min(0).allow(null), + questionUpvote: Joi.number().min(0).allow(null), + questionDownvote: Joi.number().min(0).allow(null), + quizPollSubmitted: Joi.number().min(0).allow(null), + quizCorrectPerQuestion: Joi.number().min(0).allow(null), + pollParticipation: Joi.number().min(0).allow(null), + feedbackFormSubmitted: Joi.number().min(0).allow(null), + ratingSubmitted: Joi.number().min(0).allow(null), +}).allow(null); + const sessionPayload = Joi.object({ title: Joi.string().required(), description: Joi.string().allow('', null), @@ -15,6 +27,11 @@ const sessionPayload = Joi.object({ capacity: Joi.number(), status: Joi.string().valid(...SESSION_STATUS), resources: Joi.array().items(Joi.object({ name: Joi.string(), type: Joi.string(), url: Joi.string() })), + rewardRules: rewardRulesPayload, + audienceFeedbackEnabled: Joi.boolean(), + audienceRatingsEnabled: Joi.boolean(), + audienceFeedbackForm: Joi.string().hex().length(24).allow(null, ''), + audienceRatingsForm: Joi.string().hex().length(24).allow(null, ''), }); module.exports = [ diff --git a/src/routes/visitor.routes.js b/src/routes/visitor.routes.js index 00975b8..79f477a 100644 --- a/src/routes/visitor.routes.js +++ b/src/routes/visitor.routes.js @@ -158,6 +158,20 @@ module.exports = [ handler: controller.trackPreview, }, }, + { + method: 'GET', + path: '/api/visitor/events/{eventId}/feedback-forms/{formId}', + options: { + auth: false, + validate: { + params: Joi.object({ + eventId: Joi.string().required(), + formId: Joi.string().required(), + }), + }, + handler: controller.visitorFeedbackFormPreview, + }, + }, { method: 'GET', path: '/api/visitor/sessions/{sessionId}/questions', @@ -309,6 +323,18 @@ module.exports = [ handler: controller.putVisitorSessionBookmark, }, }, + { + method: 'GET', + path: '/api/visitor/events/{eventId}/leaderboard', + options: { + auth: { mode: 'optional', strategy: 'jwt-attendee' }, + validate: { + params: Joi.object({ eventId: Joi.string().required() }), + query: Joi.object({ limit: Joi.number().integer().min(5).max(100) }), + }, + handler: controller.visitorEventLeaderboard, + }, + }, { method: 'GET', path: '/api/visitor/events/{eventId}/reward-balance', @@ -320,6 +346,21 @@ module.exports = [ handler: controller.visitorEventRewardBalance, }, }, + { + method: 'GET', + path: '/api/visitor/events/{eventId}/reward-history', + options: { + auth: 'jwt-attendee', + validate: { + params: Joi.object({ eventId: Joi.string().required() }), + query: Joi.object({ + page: Joi.number().integer().min(1), + limit: Joi.number().integer().min(1).max(100), + }), + }, + handler: controller.visitorEventRewardHistory, + }, + }, { method: 'POST', path: '/api/visitor/events/{eventId}/quiz-polls/{quizPollId}/responses', diff --git a/src/services/rewardAward.js b/src/services/rewardAward.js new file mode 100644 index 0000000..3008d0d --- /dev/null +++ b/src/services/rewardAward.js @@ -0,0 +1,66 @@ +const mongoose = require('mongoose'); +const RewardPoint = require('../models/RewardPoint'); +const { REWARD_SOURCES } = require('../config/constants'); + +function asMetadataMap(meta) { + if (!meta || typeof meta !== 'object') return undefined; + const entries = Object.entries(meta).filter(([, v]) => v !== undefined); + if (!entries.length) return undefined; + return new Map(entries); +} + +/** + * Create an earned row when sourceId is new; duplicate key → no-op. + * @param {object} opts + * @param {string|mongoose.Types.ObjectId} opts.participantId + * @param {string|mongoose.Types.ObjectId} opts.eventId + * @param {number} opts.points + * @param {string} opts.source — must be in REWARD_SOURCES + * @param {string} opts.sourceId + * @param {string} [opts.description] + * @param {Record} [opts.metadata] + */ +async function awardIfNew(opts) { + const { + participantId, + eventId, + points, + source, + sourceId, + description, + metadata: metaPlain, + } = opts; + const pts = Number(points); + if (!Number.isFinite(pts) || pts <= 0) { + return { awarded: false, reason: 'no_points', pointsAwarded: 0 }; + } + if (!source || !REWARD_SOURCES.includes(source)) { + return { awarded: false, reason: 'bad_source', pointsAwarded: 0 }; + } + const sid = sourceId != null ? String(sourceId).trim() : ''; + if (!sid) { + return { awarded: false, reason: 'missing_sourceId', pointsAwarded: 0 }; + } + + const rounded = Math.round(pts); + try { + const doc = await RewardPoint.create({ + participant: participantId, + event: eventId, + points: rounded, + type: 'earned', + source, + sourceId: sid, + description, + metadata: asMetadataMap(metaPlain), + }); + return { awarded: true, doc, pointsAwarded: rounded }; + } catch (err) { + if (err && err.code === 11000) { + return { awarded: false, duplicate: true, pointsAwarded: 0 }; + } + throw err; + } +} + +module.exports = { awardIfNew }; diff --git a/src/utils/bedrockExtractEvent.js b/src/utils/bedrockExtractEvent.js new file mode 100644 index 0000000..14a98b4 --- /dev/null +++ b/src/utils/bedrockExtractEvent.js @@ -0,0 +1,78 @@ +const { BedrockRuntimeClient, InvokeModelCommand } = require('@aws-sdk/client-bedrock-runtime'); + +const EVENT_TYPES = ['conference', 'workshop', 'summit', 'cme', 'webinar']; +const EVENT_STATUS = ['draft', 'upcoming', 'live', 'completed']; +const LOCATION_TYPES = ['physical', 'virtual', 'hybrid']; + +function extractJsonObject(text) { + if (!text || typeof text !== 'string') return null; + const trimmed = text.trim(); + const fence = /^```(?:json)?\s*([\s\S]*?)```$/im.exec(trimmed); + const body = fence ? fence[1].trim() : trimmed; + const start = body.indexOf('{'); + const end = body.lastIndexOf('}'); + if (start === -1 || end <= start) return null; + try { + return JSON.parse(body.slice(start, end + 1)); + } catch { + return null; + } +} + +/** + * @param {string} userDescription + * @returns {Promise>} + */ +async function extractEventFieldsFromDescription(userDescription) { + const region = process.env.AWS_DEFAULT_REGION || process.env.AWS_REGION || 'ap-south-1'; + const modelId = + process.env.BEDROCK_MODEL_ID || 'anthropic.claude-3-haiku-20240307-v1:0'; + + const systemPrompt = `You extract structured event data from free-form text. Reply with ONLY a single JSON object, no markdown, no explanation. +Keys (use null for unknown optional fields): +- name: string, required, at least 3 characters, event title +- subtitle: string or null, short tagline +- description: string or null, longer details +- type: one of ${JSON.stringify(EVENT_TYPES)} +- status: one of ${JSON.stringify(EVENT_STATUS)} or omit (server defaults to draft) +- startDate: ISO8601 date-time string (UTC) for event start +- endDate: ISO 8601 date-time string (UTC) for event end, must be after startDate +- location: string or null, venue or city +- locationType: one of ${JSON.stringify(LOCATION_TYPES)} +- virtualLink: string or null, URL if virtual/hybrid +- tags: string array or omit. + +Infer reasonable dates only if the user mentions dates; otherwise use null for startDate and endDate so defaults apply.`; + + const client = new BedrockRuntimeClient({ region }); + const body = JSON.stringify({ + anthropic_version: 'bedrock-2023-05-31', + max_tokens: 2048, + messages: [ + { + role: 'user', + content: [{ type: 'text', text: `${systemPrompt}\n\nUser description:\n${userDescription}` }], + }, + ], + }); + + const out = await client.send( + new InvokeModelCommand({ + modelId, + contentType: 'application/json', + accept: 'application/json', + body: Buffer.from(body, 'utf8'), + }), + ); + + const raw = new TextDecoder().decode(out.body); + const decoded = JSON.parse(raw); + const text = decoded.content?.[0]?.text; + const parsed = extractJsonObject(text || ''); + if (!parsed || typeof parsed !== 'object') { + throw new Error('Model did not return valid JSON'); + } + return parsed; +} + +module.exports = { extractEventFieldsFromDescription, EVENT_TYPES, EVENT_STATUS, LOCATION_TYPES }; diff --git a/src/utils/eventBuzzCode.js b/src/utils/eventBuzzCode.js index 3b9386c..b552d00 100644 --- a/src/utils/eventBuzzCode.js +++ b/src/utils/eventBuzzCode.js @@ -1,9 +1,9 @@ -/** Deterministic 10-char A–Z / 0–9 code for a track or session id (stable across opens). Mirrors frontend eventBuzzCode.ts. */ +/** Deterministic 10-char A–Z / 0–9 code for a track, session, or event id (stable across opens). */ const EVENTBUZZ_CODE_CHARS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; /** * @param {string} id Mongo document id string - * @param {'track' | 'session'} kind + * @param {'track' | 'session' | 'event'} kind */ function eventBuzzCodeFor(id, kind) { const input = `${kind}:${id}`; diff --git a/src/utils/quizLoyaltyScore.js b/src/utils/quizLoyaltyScore.js new file mode 100644 index 0000000..3af2a16 --- /dev/null +++ b/src/utils/quizLoyaltyScore.js @@ -0,0 +1,65 @@ +function normAnswerVal(ans) { + if (ans == null) return ''; + if (typeof ans === 'object' && ans !== null && typeof ans.toString === 'function') { + return String(ans.toString()); + } + return String(ans); +} + +function optionIdStr(o) { + if (!o) return ''; + return o._id != null ? String(o._id) : ''; +} + +function correctIdSet(q) { + const ids = new Set(); + for (const o of q.options || []) { + if (o && o.isCorrect && optionIdStr(o)) ids.add(optionIdStr(o)); + } + return ids; +} + +/** + * @param {object} q question subdoc from QuizPoll + * @param {{ answer?: unknown }} answerItem from visitor payload + * @returns {boolean} + */ +function isObjectiveQuestionCorrect(q, answerItem) { + if (!answerItem) return false; + const qType = String(q.type || 'single').toLowerCase(); + if (qType === 'text' || qType === 'rating') return false; + const correct = correctIdSet(q); + if (correct.size === 0) return false; + + if (qType === 'multiple') { + const raw = answerItem.answer; + if (!Array.isArray(raw)) return false; + const ans = new Set(raw.map((x) => normAnswerVal(x))); + if (ans.size !== correct.size) return false; + for (const id of correct) { + if (!ans.has(id)) return false; + } + return true; + } + + const a = normAnswerVal(answerItem.answer); + return correct.has(a); +} + +/** + * @param {{ questions?: object[] }} quizPoll lean + * @param {Array<{ questionIndex?: number, answer?: unknown }>} responses + */ +function countQuizCorrectAnswers(quizPoll, responses) { + const questions = quizPoll.questions || []; + const list = Array.isArray(responses) ? responses : []; + let n = 0; + for (const item of list) { + const idx = Number(item.questionIndex); + if (!Number.isInteger(idx) || idx < 0 || idx >= questions.length) continue; + if (isObjectiveQuestionCorrect(questions[idx], item)) n += 1; + } + return n; +} + +module.exports = { countQuizCorrectAnswers, isObjectiveQuestionCorrect, normAnswerVal }; diff --git a/src/utils/rewardRulesResolve.js b/src/utils/rewardRulesResolve.js new file mode 100644 index 0000000..99952e7 --- /dev/null +++ b/src/utils/rewardRulesResolve.js @@ -0,0 +1,39 @@ +/** + * Merge order: quiz/poll settings → session → event (first defined wins, including 0). + * @param {object} eventRules + * @param {object} sessionRules + * @param {object} quizPollRules + */ +const REWARD_RULE_KEYS = [ + 'registrationComplete', + 'questionAsk', + 'questionUpvote', + 'questionDownvote', + 'quizPollSubmitted', + 'quizCorrectPerQuestion', + 'pollParticipation', + 'feedbackFormSubmitted', + 'ratingSubmitted', +]; + +function firstDefined(...vals) { + for (const v of vals) { + if (v !== undefined && v !== null) return Number(v); + } + return undefined; +} + +function mergeRules(eventRules = {}, sessionRules = {}, quizPollRules = {}) { + const out = {}; + const ev = eventRules && typeof eventRules === 'object' ? eventRules : {}; + const sess = sessionRules && typeof sessionRules === 'object' ? sessionRules : {}; + const quiz = quizPollRules && typeof quizPollRules === 'object' ? quizPollRules : {}; + for (const key of REWARD_RULE_KEYS) { + const raw = firstDefined(quiz[key], sess[key], ev[key]); + const n = raw === undefined || Number.isNaN(raw) ? 0 : Math.max(0, raw); + out[key] = n; + } + return out; +} + +module.exports = { REWARD_RULE_KEYS, mergeRules }; diff --git a/src/utils/sessionQuestionFirebaseSync.js b/src/utils/sessionQuestionFirebaseSync.js index 475f2b8..f055c83 100644 --- a/src/utils/sessionQuestionFirebaseSync.js +++ b/src/utils/sessionQuestionFirebaseSync.js @@ -37,6 +37,8 @@ function questionToRtdbPayload(doc) { upvotes: typeof o.upvotes === 'number' ? o.upvotes : 0, downvotes: typeof o.downvotes === 'number' ? o.downvotes : 0, status: o.status != null ? o.status : 'pending', + shortlisted: Boolean(o.shortlisted), + audienceListed: o.audienceListed !== false, createdAt: o.createdAt instanceof Date ? o.createdAt.toISOString()