@equilateral_ai/mindmeld 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. package/README.md +300 -0
  2. package/hooks/README.md +494 -0
  3. package/hooks/pre-compact.js +392 -0
  4. package/hooks/session-start.js +264 -0
  5. package/package.json +90 -0
  6. package/scripts/harvest.js +561 -0
  7. package/scripts/init-project.js +437 -0
  8. package/scripts/inject.js +388 -0
  9. package/src/collaboration/CollaborationPrompt.js +460 -0
  10. package/src/core/AlertEngine.js +813 -0
  11. package/src/core/AlertNotifier.js +363 -0
  12. package/src/core/CorrelationAnalyzer.js +774 -0
  13. package/src/core/CurationEngine.js +688 -0
  14. package/src/core/LLMPatternDetector.js +508 -0
  15. package/src/core/LoadBearingDetector.js +242 -0
  16. package/src/core/NotificationService.js +1032 -0
  17. package/src/core/PatternValidator.js +355 -0
  18. package/src/core/README.md +160 -0
  19. package/src/core/RapportOrchestrator.js +446 -0
  20. package/src/core/RelevanceDetector.js +577 -0
  21. package/src/core/StandardsIngestion.js +575 -0
  22. package/src/core/TeamLoadBearingDetector.js +431 -0
  23. package/src/database/dbOperations.js +105 -0
  24. package/src/handlers/activity/activityGetMe.js +98 -0
  25. package/src/handlers/activity/activityGetTeam.js +130 -0
  26. package/src/handlers/alerts/alertsAcknowledge.js +91 -0
  27. package/src/handlers/alerts/alertsGet.js +250 -0
  28. package/src/handlers/collaborators/collaboratorAdd.js +201 -0
  29. package/src/handlers/collaborators/collaboratorInvite.js +218 -0
  30. package/src/handlers/collaborators/collaboratorList.js +88 -0
  31. package/src/handlers/collaborators/collaboratorRemove.js +127 -0
  32. package/src/handlers/collaborators/inviteAccept.js +122 -0
  33. package/src/handlers/context/contextGet.js +57 -0
  34. package/src/handlers/context/invariantsGet.js +74 -0
  35. package/src/handlers/context/loopsGet.js +82 -0
  36. package/src/handlers/context/notesCreate.js +74 -0
  37. package/src/handlers/context/purposeGet.js +78 -0
  38. package/src/handlers/correlations/correlationsDeveloperGet.js +226 -0
  39. package/src/handlers/correlations/correlationsGet.js +93 -0
  40. package/src/handlers/correlations/correlationsProjectGet.js +161 -0
  41. package/src/handlers/github/githubConnectionStatus.js +49 -0
  42. package/src/handlers/github/githubDiscoverPatterns.js +364 -0
  43. package/src/handlers/github/githubOAuthCallback.js +166 -0
  44. package/src/handlers/github/githubOAuthStart.js +59 -0
  45. package/src/handlers/github/githubPatternsReview.js +109 -0
  46. package/src/handlers/github/githubReposList.js +105 -0
  47. package/src/handlers/helpers/checkSuperAdmin.js +85 -0
  48. package/src/handlers/helpers/dbOperations.js +53 -0
  49. package/src/handlers/helpers/errorHandler.js +49 -0
  50. package/src/handlers/helpers/index.js +106 -0
  51. package/src/handlers/helpers/lambdaWrapper.js +60 -0
  52. package/src/handlers/helpers/responseUtil.js +55 -0
  53. package/src/handlers/helpers/subscriptionTiers.js +1168 -0
  54. package/src/handlers/notifications/getPreferences.js +84 -0
  55. package/src/handlers/notifications/sendNotification.js +170 -0
  56. package/src/handlers/notifications/updatePreferences.js +316 -0
  57. package/src/handlers/patterns/patternUsagePost.js +182 -0
  58. package/src/handlers/patterns/patternViolationPost.js +185 -0
  59. package/src/handlers/projects/projectCreate.js +107 -0
  60. package/src/handlers/projects/projectDelete.js +82 -0
  61. package/src/handlers/projects/projectGet.js +95 -0
  62. package/src/handlers/projects/projectUpdate.js +118 -0
  63. package/src/handlers/reports/aiLeverage.js +206 -0
  64. package/src/handlers/reports/engineeringInvestment.js +132 -0
  65. package/src/handlers/reports/riskForecast.js +186 -0
  66. package/src/handlers/reports/standardsRoi.js +162 -0
  67. package/src/handlers/scheduled/analyzeCorrelations.js +178 -0
  68. package/src/handlers/scheduled/analyzeGitHistory.js +510 -0
  69. package/src/handlers/scheduled/generateAlerts.js +135 -0
  70. package/src/handlers/scheduled/refreshActivity.js +21 -0
  71. package/src/handlers/scheduled/scanCompliance.js +334 -0
  72. package/src/handlers/sessions/sessionEndPost.js +180 -0
  73. package/src/handlers/sessions/sessionStandardsPost.js +135 -0
  74. package/src/handlers/stripe/addonManagePost.js +240 -0
  75. package/src/handlers/stripe/billingPortalPost.js +93 -0
  76. package/src/handlers/stripe/enterpriseCheckoutPost.js +272 -0
  77. package/src/handlers/stripe/seatsUpdatePost.js +185 -0
  78. package/src/handlers/stripe/subscriptionCancelDelete.js +169 -0
  79. package/src/handlers/stripe/subscriptionCreatePost.js +221 -0
  80. package/src/handlers/stripe/subscriptionUpdatePut.js +163 -0
  81. package/src/handlers/stripe/webhookPost.js +454 -0
  82. package/src/handlers/users/cognitoPostConfirmation.js +150 -0
  83. package/src/handlers/users/userEntitlementsGet.js +89 -0
  84. package/src/handlers/users/userGet.js +114 -0
  85. package/src/handlers/webhooks/githubWebhook.js +223 -0
  86. package/src/index.js +969 -0
@@ -0,0 +1,182 @@
1
+ /**
2
+ * Pattern Usage Handler
3
+ * Records pattern usage (success/failure) from Claude Code sessions
4
+ *
5
+ * POST /api/patterns/usage
6
+ * Body: { pattern, session_id, user_id, project_id, success, context, timestamp }
7
+ *
8
+ * Called by: pre-compact.js hook (reinforcePattern method)
9
+ */
10
+
11
+ const { wrapHandler, executeQuery, createSuccessResponse, createErrorResponse, handleError } = require('./helpers');
12
+
13
+ /**
14
+ * Record pattern usage
15
+ * Updates usage count and maturity level based on success
16
+ */
17
+ async function recordPatternUsage({ body: requestBody = {}, requestContext }) {
18
+ try {
19
+ const Request_ID = requestContext?.requestId || 'unknown';
20
+
21
+ const {
22
+ pattern,
23
+ session_id,
24
+ user_id,
25
+ project_id,
26
+ success = true,
27
+ context = {},
28
+ timestamp
29
+ } = requestBody;
30
+
31
+ // Validate required fields
32
+ if (!pattern || !pattern.element) {
33
+ return createErrorResponse(400, 'pattern.element is required');
34
+ }
35
+
36
+ if (!session_id) {
37
+ return createErrorResponse(400, 'session_id is required');
38
+ }
39
+
40
+ // Generate pattern_id from element if not provided
41
+ const patternId = pattern.pattern_id || `pat_${pattern.element.toLowerCase().replace(/\s+/g, '_').substring(0, 50)}`;
42
+
43
+ // If no project_id, we can't create a pattern record (FK constraint)
44
+ // Return success anyway - hooks should not fail
45
+ if (!project_id) {
46
+ console.log('[patternUsagePost] No project_id provided, skipping pattern record');
47
+ return createSuccessResponse(
48
+ {
49
+ Records: [{
50
+ pattern_id: patternId,
51
+ recorded: false,
52
+ reason: 'no_project_id',
53
+ message: 'Pattern usage logged but not recorded (no project context)'
54
+ }]
55
+ },
56
+ 'Pattern usage acknowledged (no project context)',
57
+ {
58
+ Total_Records: 0,
59
+ Request_ID,
60
+ Timestamp: new Date().toISOString()
61
+ }
62
+ );
63
+ }
64
+
65
+ // First, ensure the pattern exists in rapport.patterns (upsert)
66
+ const upsertPatternQuery = `
67
+ INSERT INTO rapport.patterns (
68
+ pattern_id,
69
+ project_id,
70
+ intent,
71
+ constraints,
72
+ outcome_criteria,
73
+ maturity,
74
+ handoff_count,
75
+ successful_handoffs,
76
+ failed_handoffs,
77
+ discovered_by,
78
+ discovered_at,
79
+ last_used,
80
+ pattern_data
81
+ ) VALUES (
82
+ $1, $2, $3, $4, $5, 'provisional', 1,
83
+ CASE WHEN $6 THEN 1 ELSE 0 END,
84
+ CASE WHEN $6 THEN 0 ELSE 1 END,
85
+ $7, NOW(), NOW(), $8
86
+ )
87
+ ON CONFLICT (pattern_id) DO UPDATE SET
88
+ handoff_count = rapport.patterns.handoff_count + 1,
89
+ successful_handoffs = rapport.patterns.successful_handoffs + CASE WHEN $6 THEN 1 ELSE 0 END,
90
+ failed_handoffs = rapport.patterns.failed_handoffs + CASE WHEN $6 THEN 0 ELSE 1 END,
91
+ last_used = NOW(),
92
+ maturity = CASE
93
+ WHEN rapport.patterns.handoff_count >= 9 AND
94
+ (rapport.patterns.successful_handoffs + CASE WHEN $6 THEN 1 ELSE 0 END)::float /
95
+ (rapport.patterns.handoff_count + 1) >= 0.7
96
+ THEN 'reinforced'
97
+ WHEN rapport.patterns.handoff_count >= 4 AND
98
+ (rapport.patterns.successful_handoffs + CASE WHEN $6 THEN 1 ELSE 0 END)::float /
99
+ (rapport.patterns.handoff_count + 1) >= 0.6
100
+ THEN 'validated'
101
+ ELSE rapport.patterns.maturity
102
+ END
103
+ RETURNING pattern_id, maturity, handoff_count, successful_handoffs, failed_handoffs
104
+ `;
105
+
106
+ const patternResult = await executeQuery(upsertPatternQuery, [
107
+ patternId,
108
+ project_id || null,
109
+ pattern.intent || pattern.element,
110
+ JSON.stringify(pattern.constraints || []),
111
+ JSON.stringify(pattern.outcome_criteria || []),
112
+ success,
113
+ user_id || 'anonymous',
114
+ JSON.stringify({
115
+ type: pattern.type,
116
+ category: pattern.category,
117
+ file: pattern.file,
118
+ confidence: pattern.confidence,
119
+ evidence: pattern.evidence
120
+ })
121
+ ]);
122
+
123
+ const updatedPattern = patternResult.rows[0];
124
+
125
+ // Record the individual usage in pattern_usage table
126
+ const usageQuery = `
127
+ INSERT INTO rapport.pattern_usage (
128
+ pattern_id,
129
+ email_address,
130
+ session_id,
131
+ success,
132
+ context,
133
+ used_at
134
+ ) VALUES (
135
+ $1, $2, $3, $4, $5, $6
136
+ )
137
+ RETURNING usage_id
138
+ `;
139
+
140
+ const usageResult = await executeQuery(usageQuery, [
141
+ patternId,
142
+ user_id || 'anonymous',
143
+ session_id,
144
+ success,
145
+ JSON.stringify({
146
+ working_directory: context.working_directory,
147
+ source: context.source,
148
+ file: pattern.file,
149
+ category: pattern.category
150
+ }),
151
+ timestamp ? new Date(timestamp) : new Date()
152
+ ]);
153
+
154
+ // Calculate success rate
155
+ const successRate = updatedPattern.successful_handoffs / updatedPattern.handoff_count;
156
+
157
+ return createSuccessResponse(
158
+ {
159
+ Records: [{
160
+ usage_id: usageResult.rows[0]?.usage_id,
161
+ pattern_id: updatedPattern.pattern_id,
162
+ maturity: updatedPattern.maturity,
163
+ usage_count: updatedPattern.handoff_count,
164
+ success_rate: successRate.toFixed(2),
165
+ recorded: true
166
+ }]
167
+ },
168
+ success ? 'Pattern usage recorded (success)' : 'Pattern usage recorded (failure)',
169
+ {
170
+ Total_Records: 1,
171
+ Request_ID,
172
+ Timestamp: new Date().toISOString()
173
+ }
174
+ );
175
+
176
+ } catch (error) {
177
+ console.error('Handler Error:', error);
178
+ return handleError(error);
179
+ }
180
+ }
181
+
182
+ exports.handler = wrapHandler(recordPatternUsage);
@@ -0,0 +1,185 @@
1
+ /**
2
+ * Pattern Violation Handler
3
+ * Records standards violations detected during Claude Code sessions
4
+ *
5
+ * POST /api/patterns/violations
6
+ * Body: { pattern, violations[], session_id, user_id, project_id, timestamp }
7
+ *
8
+ * Called by: pre-compact.js hook (recordViolation method)
9
+ */
10
+
11
+ const { wrapHandler, executeQuery, createSuccessResponse, createErrorResponse, handleError } = require('./helpers');
12
+
13
+ /**
14
+ * Record pattern violation
15
+ * Stores violation for compliance tracking and team learning
16
+ */
17
+ async function recordPatternViolation({ body: requestBody = {}, requestContext }) {
18
+ try {
19
+ const Request_ID = requestContext?.requestId || 'unknown';
20
+
21
+ const {
22
+ pattern,
23
+ violations = [],
24
+ session_id,
25
+ user_id,
26
+ project_id,
27
+ timestamp
28
+ } = requestBody;
29
+
30
+ // Validate required fields
31
+ if (!pattern || !pattern.element) {
32
+ return createErrorResponse(400, 'pattern.element is required');
33
+ }
34
+
35
+ if (!violations || violations.length === 0) {
36
+ return createErrorResponse(400, 'violations array is required');
37
+ }
38
+
39
+ if (!session_id) {
40
+ return createErrorResponse(400, 'session_id is required');
41
+ }
42
+
43
+ // Generate pattern_id from element if not provided
44
+ const patternId = pattern.pattern_id || `pat_${pattern.element.toLowerCase().replace(/\s+/g, '_').substring(0, 50)}`;
45
+
46
+ // If no project_id, we can't create a pattern record (FK constraint)
47
+ // Return success anyway - hooks should not fail
48
+ if (!project_id) {
49
+ console.log('[patternViolationPost] No project_id provided, skipping violation record');
50
+ return createSuccessResponse(
51
+ {
52
+ Records: [{
53
+ pattern_id: patternId,
54
+ violation_count: violations.length,
55
+ recorded: false,
56
+ reason: 'no_project_id',
57
+ message: 'Violation logged but not recorded (no project context)'
58
+ }]
59
+ },
60
+ 'Violation acknowledged (no project context)',
61
+ {
62
+ Total_Records: 0,
63
+ Request_ID,
64
+ Timestamp: new Date().toISOString()
65
+ }
66
+ );
67
+ }
68
+
69
+ // Ensure the pattern exists (upsert with violation count)
70
+ const upsertPatternQuery = `
71
+ INSERT INTO rapport.patterns (
72
+ pattern_id,
73
+ project_id,
74
+ intent,
75
+ constraints,
76
+ outcome_criteria,
77
+ maturity,
78
+ handoff_count,
79
+ successful_handoffs,
80
+ failed_handoffs,
81
+ discovered_by,
82
+ discovered_at,
83
+ last_used,
84
+ pattern_data
85
+ ) VALUES (
86
+ $1, $2, $3, $4, $5, 'provisional', 1, 0, 1, $6, NOW(), NOW(), $7
87
+ )
88
+ ON CONFLICT (pattern_id) DO UPDATE SET
89
+ handoff_count = rapport.patterns.handoff_count + 1,
90
+ failed_handoffs = rapport.patterns.failed_handoffs + 1,
91
+ last_used = NOW()
92
+ RETURNING pattern_id
93
+ `;
94
+
95
+ await executeQuery(upsertPatternQuery, [
96
+ patternId,
97
+ project_id || null,
98
+ pattern.intent || pattern.element,
99
+ JSON.stringify(pattern.constraints || []),
100
+ JSON.stringify(pattern.outcome_criteria || []),
101
+ user_id || 'anonymous',
102
+ JSON.stringify({
103
+ type: pattern.type,
104
+ category: pattern.category,
105
+ file: pattern.file
106
+ })
107
+ ]);
108
+
109
+ // Record each violation
110
+ const violationRecords = [];
111
+ for (const violation of violations) {
112
+ const violationQuery = `
113
+ INSERT INTO rapport.pattern_usage (
114
+ pattern_id,
115
+ email_address,
116
+ session_id,
117
+ success,
118
+ context,
119
+ used_at
120
+ ) VALUES (
121
+ $1, $2, $3, false, $4, $5
122
+ )
123
+ RETURNING usage_id
124
+ `;
125
+
126
+ const result = await executeQuery(violationQuery, [
127
+ patternId,
128
+ user_id || 'anonymous',
129
+ session_id,
130
+ JSON.stringify({
131
+ violation: true,
132
+ standard_id: violation.standard_id,
133
+ rule: violation.rule,
134
+ description: violation.description,
135
+ file: pattern.file,
136
+ category: pattern.category
137
+ }),
138
+ timestamp ? new Date(timestamp) : new Date()
139
+ ]);
140
+
141
+ violationRecords.push({
142
+ usage_id: result.rows[0]?.usage_id,
143
+ standard_id: violation.standard_id,
144
+ rule: violation.rule
145
+ });
146
+ }
147
+
148
+ // Also record in session_standards if tracking standards shown
149
+ // Update to mark standard as violated
150
+ const updateStandardsQuery = `
151
+ UPDATE rapport.session_standards
152
+ SET violated = true
153
+ WHERE session_id = $1
154
+ AND pattern_id = ANY($2::varchar[])
155
+ `;
156
+
157
+ const standardIds = violations.map(v => v.standard_id).filter(Boolean);
158
+ if (standardIds.length > 0) {
159
+ await executeQuery(updateStandardsQuery, [session_id, standardIds]);
160
+ }
161
+
162
+ return createSuccessResponse(
163
+ {
164
+ Records: [{
165
+ pattern_id: patternId,
166
+ violation_count: violationRecords.length,
167
+ violations: violationRecords,
168
+ recorded: true
169
+ }]
170
+ },
171
+ `Recorded ${violationRecords.length} violation(s)`,
172
+ {
173
+ Total_Records: violationRecords.length,
174
+ Request_ID,
175
+ Timestamp: new Date().toISOString()
176
+ }
177
+ );
178
+
179
+ } catch (error) {
180
+ console.error('Handler Error:', error);
181
+ return handleError(error);
182
+ }
183
+ }
184
+
185
+ exports.handler = wrapHandler(recordPatternViolation);
@@ -0,0 +1,107 @@
1
+ /**
2
+ * Project Create Handler
3
+ * Creates new project for a company
4
+ *
5
+ * POST /api/projects
6
+ * Body: { Company_ID, project_name, description, private }
7
+ */
8
+
9
+ const { wrapHandler, executeQuery, createSuccessResponse, createErrorResponse, handleError } = require('./helpers');
10
+
11
+ /**
12
+ * Create project
13
+ * Requires Admin access to company
14
+ */
15
+ async function createProject({ body: requestBody = {}, requestContext }) {
16
+ try {
17
+ const Request_ID = requestContext.requestId;
18
+ // REST API: requestContext.authorizer.claims.email
19
+ // HTTP API: requestContext.authorizer.jwt.claims.email
20
+ const email = requestContext.authorizer?.claims?.email || requestContext.authorizer?.jwt?.claims?.email;
21
+ const { Company_ID, project_name, description, private: isPrivate, repo_url } = requestBody;
22
+
23
+ // Validate required fields
24
+ if (!Company_ID || !project_name) {
25
+ return createErrorResponse(400, 'Company_ID and project_name are required');
26
+ }
27
+
28
+ // Check user has admin access to company
29
+ const adminQuery = `
30
+ SELECT ue."Admin", c."Company_Name"
31
+ FROM "UserEntitlements" ue
32
+ JOIN "Company" c ON ue."Company_ID" = c."Company_ID"
33
+ WHERE ue."Email_Address" = $1
34
+ AND ue."Company_ID" = $2
35
+ `;
36
+ const adminCheck = await executeQuery(adminQuery, [email, Company_ID]);
37
+
38
+ if (adminCheck.rowCount === 0 || !adminCheck.rows[0].Admin) {
39
+ return createErrorResponse(403, 'Admin access required to create projects');
40
+ }
41
+
42
+ // Generate business-scoped project ID
43
+ // Format: prj_COMPANYID_timestamp
44
+ const timestamp = Date.now();
45
+ const project_id = `prj_${Company_ID}_${timestamp}`.toLowerCase().replace(/\s/g, '_');
46
+
47
+ // Create project
48
+ const query = `
49
+ INSERT INTO rapport.projects (
50
+ project_id,
51
+ company_id,
52
+ project_name,
53
+ description,
54
+ private,
55
+ repo_url
56
+ )
57
+ VALUES ($1, $2, $3, $4, $5, $6)
58
+ RETURNING
59
+ project_id,
60
+ company_id,
61
+ project_name,
62
+ description,
63
+ private,
64
+ repo_url,
65
+ created_at
66
+ `;
67
+
68
+ const result = await executeQuery(query, [
69
+ project_id,
70
+ Company_ID,
71
+ project_name,
72
+ description || null,
73
+ isPrivate || false,
74
+ repo_url || null
75
+ ]);
76
+
77
+ // Add creator as owner
78
+ const collabQuery = `
79
+ INSERT INTO rapport.project_collaborators (
80
+ project_id,
81
+ email_address,
82
+ role
83
+ )
84
+ VALUES ($1, $2, 'owner')
85
+ `;
86
+ await executeQuery(collabQuery, [project_id, email]);
87
+
88
+ return createSuccessResponse(
89
+ { Records: result.rows },
90
+ 'Project created successfully',
91
+ {
92
+ Total_Records: result.rowCount,
93
+ Request_ID,
94
+ Timestamp: new Date().toISOString()
95
+ }
96
+ );
97
+
98
+ } catch (error) {
99
+ console.error('Handler Error:', error);
100
+ if (error.code === '23505') {
101
+ return createErrorResponse(409, 'Project already exists');
102
+ }
103
+ return handleError(error);
104
+ }
105
+ }
106
+
107
+ exports.handler = wrapHandler(createProject);
@@ -0,0 +1,82 @@
1
+ /**
2
+ * Project Delete Handler
3
+ * Archives (soft deletes) a project
4
+ *
5
+ * DELETE /api/projects/{projectId}
6
+ */
7
+
8
+ const { wrapHandler, executeQuery, createSuccessResponse, createErrorResponse, handleError } = require('./helpers');
9
+
10
+ /**
11
+ * Archive project (soft delete)
12
+ * Requires owner role or company admin
13
+ */
14
+ async function deleteProject({ pathParameters = {}, queryStringParameters: queryParams = {}, requestContext }) {
15
+ try {
16
+ const Request_ID = requestContext.requestId;
17
+ // REST API: requestContext.authorizer.claims.email
18
+ // HTTP API: requestContext.authorizer.jwt.claims.email
19
+ const email = requestContext.authorizer?.claims?.email || requestContext.authorizer?.jwt?.claims?.email;
20
+ const { projectId } = pathParameters;
21
+
22
+ if (!projectId) {
23
+ return createErrorResponse(400, 'projectId is required');
24
+ }
25
+
26
+ // Check user has access to project
27
+ const accessQuery = `
28
+ SELECT
29
+ p.project_id,
30
+ p.company_id,
31
+ pc.role,
32
+ ue."Admin" as company_admin
33
+ FROM rapport.projects p
34
+ LEFT JOIN rapport.project_collaborators pc
35
+ ON p.project_id = pc.project_id
36
+ AND pc.email_address = $1
37
+ LEFT JOIN "UserEntitlements" ue
38
+ ON ue."Email_Address" = $1
39
+ AND ue."Company_ID" = p.company_id
40
+ WHERE p.project_id = $2
41
+ `;
42
+ const accessCheck = await executeQuery(accessQuery, [email, projectId]);
43
+
44
+ if (accessCheck.rowCount === 0) {
45
+ return createErrorResponse(404, 'Project not found');
46
+ }
47
+
48
+ const access = accessCheck.rows[0];
49
+
50
+ // Check permissions (only owner or company admin can delete)
51
+ const canDelete = access.role === 'owner' || access.company_admin === true;
52
+ if (!canDelete) {
53
+ return createErrorResponse(403, 'Only project owner or company admin can delete project');
54
+ }
55
+
56
+ // Archive project (soft delete)
57
+ const query = `
58
+ UPDATE rapport.projects
59
+ SET archived = true, updated_at = NOW()
60
+ WHERE project_id = $1
61
+ RETURNING project_id, project_name, archived
62
+ `;
63
+
64
+ const result = await executeQuery(query, [projectId]);
65
+
66
+ return createSuccessResponse(
67
+ { Records: result.rows },
68
+ 'Project archived successfully',
69
+ {
70
+ Total_Records: result.rowCount,
71
+ Request_ID,
72
+ Timestamp: new Date().toISOString()
73
+ }
74
+ );
75
+
76
+ } catch (error) {
77
+ console.error('Handler Error:', error);
78
+ return handleError(error);
79
+ }
80
+ }
81
+
82
+ exports.handler = wrapHandler(deleteProject);
@@ -0,0 +1,95 @@
1
+ /**
2
+ * Project Get Handler
3
+ * Retrieves projects for a company with collaborator access check
4
+ *
5
+ * GET /api/projects?Company_ID=xxx
6
+ */
7
+
8
+ const { wrapHandler, executeQuery, createSuccessResponse, createErrorResponse, handleError } = require('./helpers');
9
+
10
+ /**
11
+ * Get projects
12
+ * Returns all projects user has access to (via company entitlements or direct collaboration)
13
+ */
14
+ async function getProjects({ queryStringParameters: queryParams = {}, requestContext }) {
15
+ try {
16
+ const Request_ID = requestContext.requestId;
17
+ // REST API: requestContext.authorizer.claims.email
18
+ // HTTP API: requestContext.authorizer.jwt.claims.email
19
+ const email = requestContext.authorizer?.claims?.email || requestContext.authorizer?.jwt?.claims?.email;
20
+
21
+ // Get user's company access
22
+ const entitlementQuery = `
23
+ SELECT ue."Company_ID", ue."Admin", c."Company_Name"
24
+ FROM "UserEntitlements" ue
25
+ JOIN "Company" c ON ue."Company_ID" = c."Company_ID"
26
+ WHERE ue."Email_Address" = $1
27
+ `;
28
+ const entitlements = await executeQuery(entitlementQuery, [email]);
29
+
30
+ if (entitlements.rowCount === 0) {
31
+ return createErrorResponse(403, 'No company access');
32
+ }
33
+
34
+ const companyIds = entitlements.rows.map(e => e.Company_ID);
35
+
36
+ // Optional filter by specific company
37
+ let targetCompanyIds = companyIds;
38
+ if (queryParams.Company_ID) {
39
+ if (!companyIds.includes(queryParams.Company_ID)) {
40
+ return createErrorResponse(403, 'No access to specified company');
41
+ }
42
+ targetCompanyIds = [queryParams.Company_ID];
43
+ }
44
+
45
+ // Get projects for user's companies
46
+ const query = `
47
+ SELECT
48
+ p.project_id,
49
+ p.company_id,
50
+ p.project_name,
51
+ p.description,
52
+ p.private,
53
+ p.created_at,
54
+ p.last_active,
55
+ p.archived,
56
+ c."Company_Name",
57
+ COUNT(DISTINCT pc.email_address) as collaborator_count,
58
+ COALESCE(
59
+ json_agg(
60
+ json_build_object(
61
+ 'email', pc.email_address,
62
+ 'role', pc.role,
63
+ 'is_external', pc.is_external
64
+ )
65
+ ) FILTER (WHERE pc.email_address IS NOT NULL),
66
+ '[]'
67
+ ) as collaborators
68
+ FROM rapport.projects p
69
+ JOIN "Company" c ON p.company_id = c."Company_ID"
70
+ LEFT JOIN rapport.project_collaborators pc ON p.project_id = pc.project_id
71
+ WHERE p.company_id = ANY($1::varchar[])
72
+ AND p.archived = false
73
+ GROUP BY p.project_id, c."Company_Name"
74
+ ORDER BY p.last_active DESC NULLS LAST, p.created_at DESC
75
+ `;
76
+
77
+ const result = await executeQuery(query, [targetCompanyIds]);
78
+
79
+ return createSuccessResponse(
80
+ { Records: result.rows },
81
+ 'Projects retrieved successfully',
82
+ {
83
+ Total_Records: result.rowCount,
84
+ Request_ID,
85
+ Timestamp: new Date().toISOString()
86
+ }
87
+ );
88
+
89
+ } catch (error) {
90
+ console.error('Handler Error:', error);
91
+ return handleError(error);
92
+ }
93
+ }
94
+
95
+ exports.handler = wrapHandler(getProjects);