@vantageos/vantage-crm-mcp 0.1.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 +260 -0
- package/dist/convex/crm/_helpers.js +24 -0
- package/dist/convex/crm/activities.js +220 -0
- package/dist/convex/crm/briefing.js +198 -0
- package/dist/convex/crm/calendarCron.js +92 -0
- package/dist/convex/crm/calendarCronDispatch.js +83 -0
- package/dist/convex/crm/calendarSync.js +294 -0
- package/dist/convex/crm/companies.js +323 -0
- package/dist/convex/crm/contacts.js +346 -0
- package/dist/convex/crm/deals.js +481 -0
- package/dist/convex/crm/emailActions.js +158 -0
- package/dist/convex/crm/emailCron.js +210 -0
- package/dist/convex/crm/emailCronDispatch.js +76 -0
- package/dist/convex/crm/emailSync.js +260 -0
- package/dist/convex/crm/onboarding.js +185 -0
- package/dist/convex/crm/stats.js +75 -0
- package/dist/convex/crm/tasks.js +109 -0
- package/dist/convex/crons.js +25 -0
- package/dist/convex/integrations.js +183 -0
- package/dist/convex/lib/auditLog.js +109 -0
- package/dist/convex/lib/auth.js +372 -0
- package/dist/convex/lib/rbac.js +123 -0
- package/dist/convex/lib/workspace.js +171 -0
- package/dist/convex/organizations.js +192 -0
- package/dist/convex/schema.js +690 -0
- package/dist/convex/users.js +217 -0
- package/dist/convex/workspaces.js +603 -0
- package/dist/mcp-server/lib/convexClient.js +50 -0
- package/dist/mcp-server/lib/scopeEnforcement.js +76 -0
- package/dist/mcp-server/registry.js +116 -0
- package/dist/mcp-server/server.js +97 -0
- package/dist/mcp-server/tests/registry.test.js +163 -0
- package/dist/mcp-server/tests/scopeEnforcement.test.js +137 -0
- package/dist/mcp-server/tests/security.test.js +257 -0
- package/dist/mcp-server/tests/tools.test.js +272 -0
- package/dist/mcp-server/tools/activities.js +207 -0
- package/dist/mcp-server/tools/admin.js +190 -0
- package/dist/mcp-server/tools/companies.js +233 -0
- package/dist/mcp-server/tools/contacts.js +306 -0
- package/dist/mcp-server/tools/customFields.js +222 -0
- package/dist/mcp-server/tools/customObjects.js +235 -0
- package/dist/mcp-server/tools/deals.js +297 -0
- package/dist/mcp-server/tools/rbac.js +177 -0
- package/dist/mcp-server/tools/search.js +155 -0
- package/dist/mcp-server/tools/workflows.js +234 -0
- package/dist/mcp-server/transport/http.js +257 -0
- package/dist/mcp-server/transport/stdio.js +90 -0
- package/package.json +45 -0
package/README.md
ADDED
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
# VantageCRM MCP Server
|
|
2
|
+
|
|
3
|
+
Model Context Protocol server for VantageCRM — 60 tools across 10 categories exposing the full CRM backend to AI agents.
|
|
4
|
+
|
|
5
|
+
## Transports
|
|
6
|
+
|
|
7
|
+
### stdio (local / Claude Desktop)
|
|
8
|
+
|
|
9
|
+
Runs as a subprocess. Full tool access, no scope filtering (trusted local user).
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
CONVEX_URL=https://your-deployment.convex.cloud node dist/server.js
|
|
13
|
+
# or explicitly:
|
|
14
|
+
CONVEX_URL=https://... node dist/server.js --mode stdio
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Claude Desktop `.mcp.json`:
|
|
18
|
+
|
|
19
|
+
```json
|
|
20
|
+
{
|
|
21
|
+
"mcpServers": {
|
|
22
|
+
"vantageCRM": {
|
|
23
|
+
"command": "node",
|
|
24
|
+
"args": ["/path/to/vantageos-crm/mcp-server/dist/server.js"],
|
|
25
|
+
"env": {
|
|
26
|
+
"CONVEX_URL": "https://your-deployment.convex.cloud"
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### HTTP (multi-tenant / Railway)
|
|
34
|
+
|
|
35
|
+
Runs as an HTTP server. OAuth Bearer tokens required. Tools filtered by token scope.
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
CONVEX_URL=https://your-deployment.convex.cloud \
|
|
39
|
+
VANTAGE_MCP_MODE=http \
|
|
40
|
+
PORT=3001 \
|
|
41
|
+
node dist/server.js --mode http
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Endpoints:
|
|
45
|
+
|
|
46
|
+
| Method | Path | Description |
|
|
47
|
+
|--------|------|-------------|
|
|
48
|
+
| GET | `/health` | Liveness probe |
|
|
49
|
+
| GET | `/mcp/tools` | List tools visible to token |
|
|
50
|
+
| POST | `/mcp/call` | Call a tool |
|
|
51
|
+
|
|
52
|
+
Request format:
|
|
53
|
+
|
|
54
|
+
```http
|
|
55
|
+
POST /mcp/call
|
|
56
|
+
Authorization: Bearer <token>
|
|
57
|
+
Content-Type: application/json
|
|
58
|
+
|
|
59
|
+
{
|
|
60
|
+
"tool": "create_contact",
|
|
61
|
+
"args": { "workspaceId": "ws_xxx", "firstName": "Jane", "lastName": "Doe" }
|
|
62
|
+
}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Environment Variables
|
|
66
|
+
|
|
67
|
+
| Variable | Required | Description |
|
|
68
|
+
|----------|----------|-------------|
|
|
69
|
+
| `CONVEX_URL` | Yes | Convex deployment URL |
|
|
70
|
+
| `VANTAGE_MCP_MODE` | No | `stdio` (default) or `http` |
|
|
71
|
+
| `PORT` | No | HTTP port (default: 3001) |
|
|
72
|
+
|
|
73
|
+
## Tool Catalog
|
|
74
|
+
|
|
75
|
+
### Contacts (8 tools)
|
|
76
|
+
|
|
77
|
+
| Tool | Scope | Description |
|
|
78
|
+
|------|-------|-------------|
|
|
79
|
+
| `create_contact` | write | Create a CRM contact |
|
|
80
|
+
| `get_contact` | read | Retrieve contact by ID |
|
|
81
|
+
| `update_contact` | write | Update contact fields |
|
|
82
|
+
| `list_contacts` | read | List by workspace/type |
|
|
83
|
+
| `search_contacts` | read | Full-text search |
|
|
84
|
+
| `delete_contact` | write | Soft delete (30-day grace) |
|
|
85
|
+
| `list_contacts_by_custom_field` | read | Filter by custom field value |
|
|
86
|
+
| `restore_contact` | write | Restore soft-deleted contact |
|
|
87
|
+
|
|
88
|
+
### Companies (6 tools)
|
|
89
|
+
|
|
90
|
+
| Tool | Scope | Description |
|
|
91
|
+
|------|-------|-------------|
|
|
92
|
+
| `create_company` | write | Create company record |
|
|
93
|
+
| `get_company` | read | Retrieve by ID |
|
|
94
|
+
| `update_company` | write | Update fields |
|
|
95
|
+
| `list_companies` | read | List by workspace/industry |
|
|
96
|
+
| `search_companies` | read | Full-text search |
|
|
97
|
+
| `delete_company` | write | Soft delete |
|
|
98
|
+
|
|
99
|
+
### Deals (8 tools)
|
|
100
|
+
|
|
101
|
+
| Tool | Scope | Description |
|
|
102
|
+
|------|-------|-------------|
|
|
103
|
+
| `create_deal` | write | Create deal in pipeline |
|
|
104
|
+
| `get_deal` | read | Retrieve by ID |
|
|
105
|
+
| `update_deal` | write | Update fields |
|
|
106
|
+
| `list_deals` | read | List by workspace/stage/contact |
|
|
107
|
+
| `search_deals` | read | Search by title |
|
|
108
|
+
| `delete_deal` | write | Soft delete |
|
|
109
|
+
| `move_deal_stage` | write | Move stage with optional probability override |
|
|
110
|
+
| `forecast_pipeline` | read | Weighted pipeline value (Σ value × probability) |
|
|
111
|
+
|
|
112
|
+
### Activities (5 tools — no delete per OQ-4)
|
|
113
|
+
|
|
114
|
+
| Tool | Scope | Description |
|
|
115
|
+
|------|-------|-------------|
|
|
116
|
+
| `create_activity` | write | Create activity (call/email/meeting/note/task) |
|
|
117
|
+
| `get_activity` | read | Retrieve by ID |
|
|
118
|
+
| `update_activity` | write | Update mutable fields (description, dueAt, completedAt) |
|
|
119
|
+
| `list_activities` | read | List by workspace/contact/deal |
|
|
120
|
+
| `list_activities_by_type` | read | Filter by type with time range |
|
|
121
|
+
|
|
122
|
+
Activities are immutable audit trail — subject and type cannot be changed after creation.
|
|
123
|
+
|
|
124
|
+
### Custom Fields (5 tools)
|
|
125
|
+
|
|
126
|
+
| Tool | Scope | Description |
|
|
127
|
+
|------|-------|-------------|
|
|
128
|
+
| `add_custom_field` | write | Define custom field for entity type |
|
|
129
|
+
| `update_custom_field_definition` | write | Update label/options/validation |
|
|
130
|
+
| `list_custom_fields` | read | List definitions by workspace |
|
|
131
|
+
| `set_custom_field_value` | write | Set value on an entity |
|
|
132
|
+
| `delete_custom_field` | admin | Delete definition (irreversible) |
|
|
133
|
+
|
|
134
|
+
### Custom Objects (7 tools)
|
|
135
|
+
|
|
136
|
+
| Tool | Scope | Description |
|
|
137
|
+
|------|-------|-------------|
|
|
138
|
+
| `add_custom_object` | admin | DDL: define new object type |
|
|
139
|
+
| `list_custom_objects` | read | List object type definitions |
|
|
140
|
+
| `create_record` | write | Create a custom object record |
|
|
141
|
+
| `update_record` | write | Update record fields |
|
|
142
|
+
| `delete_record` | write | Soft delete record |
|
|
143
|
+
| `list_records` | read | List records by type |
|
|
144
|
+
| `search_records` | read | Search records by field values |
|
|
145
|
+
|
|
146
|
+
### Workflows (7 tools)
|
|
147
|
+
|
|
148
|
+
| Tool | Scope | Description |
|
|
149
|
+
|------|-------|-------------|
|
|
150
|
+
| `create_workflow` | write | Create automation workflow |
|
|
151
|
+
| `update_workflow` | write | Update workflow config |
|
|
152
|
+
| `list_workflows` | read | List by workspace |
|
|
153
|
+
| `pause_workflow` | write | Pause (disable) workflow |
|
|
154
|
+
| `resume_workflow` | write | Resume (enable) workflow |
|
|
155
|
+
| `list_executions` | read | List workflow execution history |
|
|
156
|
+
| `replay_execution` | workflow-trigger | Re-run a failed execution |
|
|
157
|
+
|
|
158
|
+
### Search / Analytics (4 tools)
|
|
159
|
+
|
|
160
|
+
| Tool | Scope | Description |
|
|
161
|
+
|------|-------|-------------|
|
|
162
|
+
| `pipeline_value` | read | Weighted pipeline Σ value × probability |
|
|
163
|
+
| `win_rate` | read | Deals won / total closed in period |
|
|
164
|
+
| `conversion_rate` | read | Stage conversion rate |
|
|
165
|
+
| `activity_summary` | read | Activity counts grouped by type |
|
|
166
|
+
|
|
167
|
+
### Admin (5 tools)
|
|
168
|
+
|
|
169
|
+
| Tool | Scope | Description |
|
|
170
|
+
|------|-------|-------------|
|
|
171
|
+
| `list_audit_log` | admin | Query audit event log |
|
|
172
|
+
| `get_entity_history` | admin | Change history for an entity |
|
|
173
|
+
| `get_actor_history` | admin | All actions by a user |
|
|
174
|
+
| `purge_archived_records` | admin | Hard delete archived records (30+ days) |
|
|
175
|
+
| `list_workspace_limits` | admin | Workspace plan limits |
|
|
176
|
+
|
|
177
|
+
### RBAC (5 tools)
|
|
178
|
+
|
|
179
|
+
| Tool | Scope | Description |
|
|
180
|
+
|------|-------|-------------|
|
|
181
|
+
| `list_workspace_members` | read | List workspace members and roles |
|
|
182
|
+
| `add_workspace_member` | admin | Add member with role |
|
|
183
|
+
| `update_member_role` | admin | Change member role |
|
|
184
|
+
| `remove_workspace_member` | admin | Remove member from workspace |
|
|
185
|
+
| `update_member_permissions` | admin | Fine-grained permission overrides |
|
|
186
|
+
|
|
187
|
+
## Scope Tier Matrix
|
|
188
|
+
|
|
189
|
+
```
|
|
190
|
+
cloud-admin ──── satisfies all scopes
|
|
191
|
+
│
|
|
192
|
+
admin ──────── satisfies read + write
|
|
193
|
+
│
|
|
194
|
+
write ──────── satisfies read
|
|
195
|
+
│
|
|
196
|
+
read ──────── base tier
|
|
197
|
+
|
|
198
|
+
workflow-trigger ── lateral tier (peer of write, different domain)
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
Token scopes are extracted from the Bearer token via `validateToken` Convex query. In stdio mode, all tools are always accessible.
|
|
202
|
+
|
|
203
|
+
## Response Envelope
|
|
204
|
+
|
|
205
|
+
All tool responses use the same envelope:
|
|
206
|
+
|
|
207
|
+
```typescript
|
|
208
|
+
// Success
|
|
209
|
+
{ success: true, data: <result> }
|
|
210
|
+
|
|
211
|
+
// Failure
|
|
212
|
+
{ success: false, error: { code: string, message: string } }
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
Common error codes: `CONVEX_ERROR`, `SCOPE_ERROR`, `ZOD_VALIDATION_ERROR`, `NOT_IMPLEMENTED`.
|
|
216
|
+
|
|
217
|
+
## Development
|
|
218
|
+
|
|
219
|
+
```bash
|
|
220
|
+
# Install dependencies
|
|
221
|
+
npm install
|
|
222
|
+
|
|
223
|
+
# Run tests
|
|
224
|
+
npm test
|
|
225
|
+
|
|
226
|
+
# Type check
|
|
227
|
+
npx tsc --noEmit --skipLibCheck
|
|
228
|
+
|
|
229
|
+
# Run in stdio mode (local dev)
|
|
230
|
+
CONVEX_URL=https://... npx tsx mcp-server/server.ts
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
## Architecture
|
|
234
|
+
|
|
235
|
+
```
|
|
236
|
+
mcp-server/
|
|
237
|
+
├── server.ts # Entry point — mode detection
|
|
238
|
+
├── registry.ts # All 60 tools + handler map
|
|
239
|
+
├── lib/
|
|
240
|
+
│ ├── convexClient.ts # ConvexHttpClient wrapper + ToolResult envelope
|
|
241
|
+
│ └── scopeEnforcement.ts # 4-tier scope hierarchy
|
|
242
|
+
├── transport/
|
|
243
|
+
│ ├── stdio.ts # MCP SDK stdio transport
|
|
244
|
+
│ └── http.ts # HTTP transport with OAuth
|
|
245
|
+
├── tools/
|
|
246
|
+
│ ├── contacts.ts # 8 tools
|
|
247
|
+
│ ├── companies.ts # 6 tools
|
|
248
|
+
│ ├── deals.ts # 8 tools
|
|
249
|
+
│ ├── activities.ts # 5 tools
|
|
250
|
+
│ ├── customFields.ts # 5 tools
|
|
251
|
+
│ ├── customObjects.ts # 7 tools
|
|
252
|
+
│ ├── workflows.ts # 7 tools
|
|
253
|
+
│ ├── search.ts # 4 tools
|
|
254
|
+
│ ├── admin.ts # 5 tools
|
|
255
|
+
│ └── rbac.ts # 5 tools
|
|
256
|
+
└── tests/
|
|
257
|
+
├── tools.test.ts # Zod validation + envelope per category
|
|
258
|
+
├── scopeEnforcement.test.ts # Scope hierarchy coverage
|
|
259
|
+
└── registry.test.ts # 60-tool count + integrity checks
|
|
260
|
+
```
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* CRM Shared Helpers
|
|
4
|
+
*
|
|
5
|
+
* Search text builders for contacts, companies, and deals.
|
|
6
|
+
* Used by create/update mutations to populate searchableText fields.
|
|
7
|
+
*/
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.buildContactSearchText = buildContactSearchText;
|
|
10
|
+
exports.buildCompanySearchText = buildCompanySearchText;
|
|
11
|
+
exports.buildDealSearchText = buildDealSearchText;
|
|
12
|
+
function buildContactSearchText(contact, companyName) {
|
|
13
|
+
return [contact.firstName, contact.lastName, contact.email, companyName]
|
|
14
|
+
.filter(Boolean)
|
|
15
|
+
.join(' ');
|
|
16
|
+
}
|
|
17
|
+
function buildCompanySearchText(company) {
|
|
18
|
+
return [company.name, company.domain, company.industry]
|
|
19
|
+
.filter(Boolean)
|
|
20
|
+
.join(' ');
|
|
21
|
+
}
|
|
22
|
+
function buildDealSearchText(deal, companyName, contactName) {
|
|
23
|
+
return [deal.title, companyName, contactName].filter(Boolean).join(' ');
|
|
24
|
+
}
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* convex/crm/activities.ts
|
|
4
|
+
*
|
|
5
|
+
* V0.1.0 — Extended from T3 stub.
|
|
6
|
+
* Functions: createActivity, updateActivity, getActivity, listActivities.
|
|
7
|
+
* NO archive, NO delete — OQ-4 audit trail doctrine (Salesforce-grade).
|
|
8
|
+
* OQ-1: actorType includes 'agent:composio' via actorId override.
|
|
9
|
+
* Every create/update mutation creates audit_log entry.
|
|
10
|
+
*
|
|
11
|
+
* Ref: vantage-crm-spec-2026-05-20.md §2 + §5 OQ-1/OQ-4
|
|
12
|
+
*/
|
|
13
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
14
|
+
exports.update = exports.updateActivity = exports.list = exports.listActivities = exports.get = exports.getActivity = exports.create = exports.createActivity = void 0;
|
|
15
|
+
const server_1 = require("../_generated/server");
|
|
16
|
+
const values_1 = require("convex/values");
|
|
17
|
+
const workspace_1 = require("../lib/workspace");
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
// Validators
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
const activityTypeValidator = values_1.v.union(values_1.v.literal('call'), values_1.v.literal('email'), values_1.v.literal('meeting'), values_1.v.literal('note'), values_1.v.literal('task'), values_1.v.literal('stage-change'), values_1.v.literal('email-received'), values_1.v.literal('email-sent'), values_1.v.literal('calendar-event'), values_1.v.literal('workflow-action'));
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
// createActivity
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
exports.createActivity = (0, server_1.mutation)({
|
|
26
|
+
args: {
|
|
27
|
+
workspaceId: values_1.v.id('workspaces'),
|
|
28
|
+
type: activityTypeValidator,
|
|
29
|
+
subject: values_1.v.string(),
|
|
30
|
+
description: values_1.v.optional(values_1.v.string()),
|
|
31
|
+
contactId: values_1.v.optional(values_1.v.id('contacts')),
|
|
32
|
+
companyId: values_1.v.optional(values_1.v.id('companies')),
|
|
33
|
+
dealId: values_1.v.optional(values_1.v.id('deals')),
|
|
34
|
+
dueAt: values_1.v.optional(values_1.v.number()),
|
|
35
|
+
occurredAt: values_1.v.optional(values_1.v.number()),
|
|
36
|
+
// OQ-1: actor tracking — 'agent:composio' supported via actorId
|
|
37
|
+
actorType: values_1.v.optional(values_1.v.union(values_1.v.literal('user'), values_1.v.literal('agent'), values_1.v.literal('system'))),
|
|
38
|
+
actorId: values_1.v.optional(values_1.v.string()), // clerkId | 'agent:composio' | 'system'
|
|
39
|
+
customFields: values_1.v.optional(values_1.v.record(values_1.v.string(), values_1.v.any())),
|
|
40
|
+
},
|
|
41
|
+
returns: values_1.v.id('activities'),
|
|
42
|
+
handler: async (ctx, args) => {
|
|
43
|
+
// Build actor — if actorId='agent:composio', auditActorType='agent'
|
|
44
|
+
const { clerkId, canWrite, actor } = await (0, workspace_1.assertWorkspaceAccess)(ctx, args.workspaceId, args.actorId);
|
|
45
|
+
if (!canWrite)
|
|
46
|
+
throw new Error('Insufficient permissions');
|
|
47
|
+
// Normalize actorType for schema (schema accepts 'user'|'agent'|'system')
|
|
48
|
+
// 'agent:composio' collapses to 'agent' in the activities table
|
|
49
|
+
const schemaActorType = args.actorId === 'agent:composio'
|
|
50
|
+
? 'agent'
|
|
51
|
+
: args.actorType ?? 'user';
|
|
52
|
+
const now = Date.now();
|
|
53
|
+
const activityId = await ctx.db.insert('activities', {
|
|
54
|
+
type: args.type,
|
|
55
|
+
subject: args.subject,
|
|
56
|
+
description: args.description,
|
|
57
|
+
contactId: args.contactId,
|
|
58
|
+
companyId: args.companyId,
|
|
59
|
+
dealId: args.dealId,
|
|
60
|
+
dueAt: args.dueAt,
|
|
61
|
+
occurredAt: args.occurredAt ?? now,
|
|
62
|
+
ownerId: clerkId,
|
|
63
|
+
workspaceId: args.workspaceId,
|
|
64
|
+
actorType: schemaActorType,
|
|
65
|
+
actorId: actor.actorId,
|
|
66
|
+
customFields: args.customFields,
|
|
67
|
+
createdAt: now,
|
|
68
|
+
updatedAt: now,
|
|
69
|
+
});
|
|
70
|
+
// Update lastActivityAt on the deal if linked
|
|
71
|
+
if (args.dealId) {
|
|
72
|
+
await ctx.db.patch(args.dealId, { lastActivityAt: now, updatedAt: now });
|
|
73
|
+
}
|
|
74
|
+
// Update lastContactedAt on the contact if linked (non-task/note only)
|
|
75
|
+
if (args.contactId && args.type !== 'task' && args.type !== 'note') {
|
|
76
|
+
await ctx.db.patch(args.contactId, { lastContactedAt: now, updatedAt: now });
|
|
77
|
+
}
|
|
78
|
+
// Audit log — note activities themselves ARE the audit trail,
|
|
79
|
+
// but we also log the mutation for compliance completeness.
|
|
80
|
+
await ctx.db.insert('audit_log', {
|
|
81
|
+
workspaceId: args.workspaceId,
|
|
82
|
+
actorId: actor.actorId,
|
|
83
|
+
actorType: actor.auditActorType,
|
|
84
|
+
entityType: 'activity',
|
|
85
|
+
entityId: activityId,
|
|
86
|
+
action: 'create',
|
|
87
|
+
timestamp: Date.now(),
|
|
88
|
+
});
|
|
89
|
+
return activityId;
|
|
90
|
+
},
|
|
91
|
+
});
|
|
92
|
+
// Backward-compatible alias
|
|
93
|
+
exports.create = exports.createActivity;
|
|
94
|
+
// ---------------------------------------------------------------------------
|
|
95
|
+
// getActivity / get
|
|
96
|
+
// ---------------------------------------------------------------------------
|
|
97
|
+
exports.getActivity = (0, server_1.query)({
|
|
98
|
+
args: {
|
|
99
|
+
activityId: values_1.v.id('activities'),
|
|
100
|
+
},
|
|
101
|
+
returns: values_1.v.union(values_1.v.null(), values_1.v.any()),
|
|
102
|
+
handler: async (ctx, args) => {
|
|
103
|
+
const activity = await ctx.db.get(args.activityId);
|
|
104
|
+
if (!activity)
|
|
105
|
+
return null;
|
|
106
|
+
await (0, workspace_1.validateWorkspaceAccess)(ctx, activity.workspaceId);
|
|
107
|
+
return activity;
|
|
108
|
+
},
|
|
109
|
+
});
|
|
110
|
+
exports.get = exports.getActivity;
|
|
111
|
+
// ---------------------------------------------------------------------------
|
|
112
|
+
// listActivities / list
|
|
113
|
+
// ---------------------------------------------------------------------------
|
|
114
|
+
exports.listActivities = (0, server_1.query)({
|
|
115
|
+
args: {
|
|
116
|
+
workspaceId: values_1.v.id('workspaces'),
|
|
117
|
+
contactId: values_1.v.optional(values_1.v.id('contacts')),
|
|
118
|
+
companyId: values_1.v.optional(values_1.v.id('companies')),
|
|
119
|
+
dealId: values_1.v.optional(values_1.v.id('deals')),
|
|
120
|
+
type: values_1.v.optional(activityTypeValidator),
|
|
121
|
+
limit: values_1.v.optional(values_1.v.number()),
|
|
122
|
+
},
|
|
123
|
+
returns: values_1.v.array(values_1.v.any()),
|
|
124
|
+
handler: async (ctx, args) => {
|
|
125
|
+
await (0, workspace_1.validateWorkspaceAccess)(ctx, args.workspaceId);
|
|
126
|
+
const limit = args.limit ?? 20;
|
|
127
|
+
let activities;
|
|
128
|
+
if (args.dealId) {
|
|
129
|
+
const deal = await ctx.db.get(args.dealId);
|
|
130
|
+
if (!deal || deal.workspaceId !== args.workspaceId)
|
|
131
|
+
return [];
|
|
132
|
+
activities = await ctx.db
|
|
133
|
+
.query('activities')
|
|
134
|
+
.withIndex('by_deal', (q) => q.eq('dealId', args.dealId))
|
|
135
|
+
.order('desc')
|
|
136
|
+
.take(limit + 1);
|
|
137
|
+
}
|
|
138
|
+
else if (args.contactId) {
|
|
139
|
+
const contact = await ctx.db.get(args.contactId);
|
|
140
|
+
if (!contact || contact.workspaceId !== args.workspaceId)
|
|
141
|
+
return [];
|
|
142
|
+
activities = await ctx.db
|
|
143
|
+
.query('activities')
|
|
144
|
+
.withIndex('by_contact', (q) => q.eq('contactId', args.contactId))
|
|
145
|
+
.order('desc')
|
|
146
|
+
.take(limit + 1);
|
|
147
|
+
}
|
|
148
|
+
else if (args.companyId) {
|
|
149
|
+
const company = await ctx.db.get(args.companyId);
|
|
150
|
+
if (!company || company.workspaceId !== args.workspaceId)
|
|
151
|
+
return [];
|
|
152
|
+
activities = await ctx.db
|
|
153
|
+
.query('activities')
|
|
154
|
+
.withIndex('by_company', (q) => q.eq('companyId', args.companyId))
|
|
155
|
+
.order('desc')
|
|
156
|
+
.take(limit + 1);
|
|
157
|
+
}
|
|
158
|
+
else if (args.type) {
|
|
159
|
+
activities = await ctx.db
|
|
160
|
+
.query('activities')
|
|
161
|
+
.withIndex('by_workspace_type', (q) => q.eq('workspaceId', args.workspaceId).eq('type', args.type))
|
|
162
|
+
.order('desc')
|
|
163
|
+
.take(limit + 1);
|
|
164
|
+
}
|
|
165
|
+
else {
|
|
166
|
+
activities = await ctx.db
|
|
167
|
+
.query('activities')
|
|
168
|
+
.withIndex('by_workspace', (q) => q.eq('workspaceId', args.workspaceId))
|
|
169
|
+
.order('desc')
|
|
170
|
+
.take(limit + 1);
|
|
171
|
+
}
|
|
172
|
+
return activities.slice(0, limit);
|
|
173
|
+
},
|
|
174
|
+
});
|
|
175
|
+
exports.list = exports.listActivities;
|
|
176
|
+
// ---------------------------------------------------------------------------
|
|
177
|
+
// updateActivity — intentionally limited (OQ-4 audit trail integrity)
|
|
178
|
+
// Only description + dueAt + completedAt + customFields may be updated.
|
|
179
|
+
// subject, type, occurredAt, links (contactId/dealId/companyId) are immutable.
|
|
180
|
+
// ---------------------------------------------------------------------------
|
|
181
|
+
exports.updateActivity = (0, server_1.mutation)({
|
|
182
|
+
args: {
|
|
183
|
+
activityId: values_1.v.id('activities'),
|
|
184
|
+
description: values_1.v.optional(values_1.v.string()),
|
|
185
|
+
dueAt: values_1.v.optional(values_1.v.number()),
|
|
186
|
+
completedAt: values_1.v.optional(values_1.v.number()),
|
|
187
|
+
customFields: values_1.v.optional(values_1.v.record(values_1.v.string(), values_1.v.any())),
|
|
188
|
+
actorId: values_1.v.optional(values_1.v.string()),
|
|
189
|
+
},
|
|
190
|
+
returns: values_1.v.id('activities'),
|
|
191
|
+
handler: async (ctx, args) => {
|
|
192
|
+
const activity = await ctx.db.get(args.activityId);
|
|
193
|
+
if (!activity)
|
|
194
|
+
throw new Error('Activity not found');
|
|
195
|
+
const { canWrite, actor } = await (0, workspace_1.assertWorkspaceAccess)(ctx, activity.workspaceId, args.actorId);
|
|
196
|
+
if (!canWrite)
|
|
197
|
+
throw new Error('Insufficient permissions');
|
|
198
|
+
const { activityId, actorId: _actorId, ...updates } = args;
|
|
199
|
+
await ctx.db.patch(activityId, {
|
|
200
|
+
...updates,
|
|
201
|
+
updatedAt: Date.now(),
|
|
202
|
+
});
|
|
203
|
+
await ctx.db.insert('audit_log', {
|
|
204
|
+
workspaceId: activity.workspaceId,
|
|
205
|
+
actorId: actor.actorId,
|
|
206
|
+
actorType: actor.auditActorType,
|
|
207
|
+
entityType: 'activity',
|
|
208
|
+
entityId: activityId,
|
|
209
|
+
action: 'update',
|
|
210
|
+
timestamp: Date.now(),
|
|
211
|
+
});
|
|
212
|
+
return activityId;
|
|
213
|
+
},
|
|
214
|
+
});
|
|
215
|
+
exports.update = exports.updateActivity;
|
|
216
|
+
// ---------------------------------------------------------------------------
|
|
217
|
+
// NO remove/delete mutation.
|
|
218
|
+
// Activities are the immutable audit trail. OQ-4 / Salesforce doctrine.
|
|
219
|
+
// This comment is intentional — do not add a remove mutation here.
|
|
220
|
+
// ---------------------------------------------------------------------------
|