@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.
- package/README.md +300 -0
- package/hooks/README.md +494 -0
- package/hooks/pre-compact.js +392 -0
- package/hooks/session-start.js +264 -0
- package/package.json +90 -0
- package/scripts/harvest.js +561 -0
- package/scripts/init-project.js +437 -0
- package/scripts/inject.js +388 -0
- package/src/collaboration/CollaborationPrompt.js +460 -0
- package/src/core/AlertEngine.js +813 -0
- package/src/core/AlertNotifier.js +363 -0
- package/src/core/CorrelationAnalyzer.js +774 -0
- package/src/core/CurationEngine.js +688 -0
- package/src/core/LLMPatternDetector.js +508 -0
- package/src/core/LoadBearingDetector.js +242 -0
- package/src/core/NotificationService.js +1032 -0
- package/src/core/PatternValidator.js +355 -0
- package/src/core/README.md +160 -0
- package/src/core/RapportOrchestrator.js +446 -0
- package/src/core/RelevanceDetector.js +577 -0
- package/src/core/StandardsIngestion.js +575 -0
- package/src/core/TeamLoadBearingDetector.js +431 -0
- package/src/database/dbOperations.js +105 -0
- package/src/handlers/activity/activityGetMe.js +98 -0
- package/src/handlers/activity/activityGetTeam.js +130 -0
- package/src/handlers/alerts/alertsAcknowledge.js +91 -0
- package/src/handlers/alerts/alertsGet.js +250 -0
- package/src/handlers/collaborators/collaboratorAdd.js +201 -0
- package/src/handlers/collaborators/collaboratorInvite.js +218 -0
- package/src/handlers/collaborators/collaboratorList.js +88 -0
- package/src/handlers/collaborators/collaboratorRemove.js +127 -0
- package/src/handlers/collaborators/inviteAccept.js +122 -0
- package/src/handlers/context/contextGet.js +57 -0
- package/src/handlers/context/invariantsGet.js +74 -0
- package/src/handlers/context/loopsGet.js +82 -0
- package/src/handlers/context/notesCreate.js +74 -0
- package/src/handlers/context/purposeGet.js +78 -0
- package/src/handlers/correlations/correlationsDeveloperGet.js +226 -0
- package/src/handlers/correlations/correlationsGet.js +93 -0
- package/src/handlers/correlations/correlationsProjectGet.js +161 -0
- package/src/handlers/github/githubConnectionStatus.js +49 -0
- package/src/handlers/github/githubDiscoverPatterns.js +364 -0
- package/src/handlers/github/githubOAuthCallback.js +166 -0
- package/src/handlers/github/githubOAuthStart.js +59 -0
- package/src/handlers/github/githubPatternsReview.js +109 -0
- package/src/handlers/github/githubReposList.js +105 -0
- package/src/handlers/helpers/checkSuperAdmin.js +85 -0
- package/src/handlers/helpers/dbOperations.js +53 -0
- package/src/handlers/helpers/errorHandler.js +49 -0
- package/src/handlers/helpers/index.js +106 -0
- package/src/handlers/helpers/lambdaWrapper.js +60 -0
- package/src/handlers/helpers/responseUtil.js +55 -0
- package/src/handlers/helpers/subscriptionTiers.js +1168 -0
- package/src/handlers/notifications/getPreferences.js +84 -0
- package/src/handlers/notifications/sendNotification.js +170 -0
- package/src/handlers/notifications/updatePreferences.js +316 -0
- package/src/handlers/patterns/patternUsagePost.js +182 -0
- package/src/handlers/patterns/patternViolationPost.js +185 -0
- package/src/handlers/projects/projectCreate.js +107 -0
- package/src/handlers/projects/projectDelete.js +82 -0
- package/src/handlers/projects/projectGet.js +95 -0
- package/src/handlers/projects/projectUpdate.js +118 -0
- package/src/handlers/reports/aiLeverage.js +206 -0
- package/src/handlers/reports/engineeringInvestment.js +132 -0
- package/src/handlers/reports/riskForecast.js +186 -0
- package/src/handlers/reports/standardsRoi.js +162 -0
- package/src/handlers/scheduled/analyzeCorrelations.js +178 -0
- package/src/handlers/scheduled/analyzeGitHistory.js +510 -0
- package/src/handlers/scheduled/generateAlerts.js +135 -0
- package/src/handlers/scheduled/refreshActivity.js +21 -0
- package/src/handlers/scheduled/scanCompliance.js +334 -0
- package/src/handlers/sessions/sessionEndPost.js +180 -0
- package/src/handlers/sessions/sessionStandardsPost.js +135 -0
- package/src/handlers/stripe/addonManagePost.js +240 -0
- package/src/handlers/stripe/billingPortalPost.js +93 -0
- package/src/handlers/stripe/enterpriseCheckoutPost.js +272 -0
- package/src/handlers/stripe/seatsUpdatePost.js +185 -0
- package/src/handlers/stripe/subscriptionCancelDelete.js +169 -0
- package/src/handlers/stripe/subscriptionCreatePost.js +221 -0
- package/src/handlers/stripe/subscriptionUpdatePut.js +163 -0
- package/src/handlers/stripe/webhookPost.js +454 -0
- package/src/handlers/users/cognitoPostConfirmation.js +150 -0
- package/src/handlers/users/userEntitlementsGet.js +89 -0
- package/src/handlers/users/userGet.js +114 -0
- package/src/handlers/webhooks/githubWebhook.js +223 -0
- 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);
|