@l4yercak3/cli 1.1.12 → 1.2.1
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/bin/cli.js +6 -0
- package/docs/mcp_server/MCP_SERVER_ARCHITECTURE.md +1481 -0
- package/docs/mcp_server/applicationOntology.ts +817 -0
- package/docs/mcp_server/cliApplications.ts +639 -0
- package/docs/mcp_server/crmOntology.ts +1063 -0
- package/docs/mcp_server/eventOntology.ts +1183 -0
- package/docs/mcp_server/formsOntology.ts +1401 -0
- package/docs/mcp_server/ontologySchemas.ts +185 -0
- package/docs/mcp_server/schema.ts +250 -0
- package/package.json +5 -2
- package/src/commands/login.js +0 -6
- package/src/commands/mcp-server.js +32 -0
- package/src/commands/spread.js +54 -1
- package/src/detectors/expo-detector.js +163 -0
- package/src/detectors/registry.js +2 -0
- package/src/mcp/auth.js +127 -0
- package/src/mcp/registry/domains/applications.js +516 -0
- package/src/mcp/registry/domains/codegen.js +894 -0
- package/src/mcp/registry/domains/core.js +326 -0
- package/src/mcp/registry/domains/crm.js +591 -0
- package/src/mcp/registry/domains/events.js +649 -0
- package/src/mcp/registry/domains/forms.js +696 -0
- package/src/mcp/registry/index.js +162 -0
- package/src/mcp/server.js +116 -0
|
@@ -0,0 +1,817 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* APPLICATION ONTOLOGY
|
|
3
|
+
*
|
|
4
|
+
* Manages connected_application objects for CLI-connected external applications.
|
|
5
|
+
* This bridges external apps (Next.js, Remix, etc.) to the L4YERCAK3 backend.
|
|
6
|
+
*
|
|
7
|
+
* Application Source Types:
|
|
8
|
+
* - "cli" - Connected via CLI tool (l4yercak3 init)
|
|
9
|
+
* - "boilerplate" - Created from L4YERCAK3 template
|
|
10
|
+
* - "manual" - Manually configured (API key only)
|
|
11
|
+
*
|
|
12
|
+
* Status Workflow:
|
|
13
|
+
* - "connecting" - Initial registration in progress
|
|
14
|
+
* - "active" - Application is connected and working
|
|
15
|
+
* - "paused" - Temporarily paused by user
|
|
16
|
+
* - "disconnected" - Connection lost or API key revoked
|
|
17
|
+
* - "archived" - Soft deleted
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { query, mutation, internalQuery, internalMutation } from "./_generated/server";
|
|
21
|
+
import { v } from "convex/values";
|
|
22
|
+
import { requireAuthenticatedUser } from "./rbacHelpers";
|
|
23
|
+
|
|
24
|
+
// ============================================================================
|
|
25
|
+
// TYPE DEFINITIONS
|
|
26
|
+
// ============================================================================
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Connected Application custom properties schema
|
|
30
|
+
*/
|
|
31
|
+
const applicationPropertiesValidator = v.object({
|
|
32
|
+
// Source information
|
|
33
|
+
source: v.object({
|
|
34
|
+
type: v.union(v.literal("cli"), v.literal("boilerplate"), v.literal("manual")),
|
|
35
|
+
projectPathHash: v.optional(v.string()), // SHA256 of absolute path (for CLI)
|
|
36
|
+
cliVersion: v.optional(v.string()),
|
|
37
|
+
framework: v.string(), // "nextjs", "remix", "astro", etc.
|
|
38
|
+
frameworkVersion: v.optional(v.string()),
|
|
39
|
+
hasTypeScript: v.optional(v.boolean()),
|
|
40
|
+
routerType: v.optional(v.string()), // "app" or "pages" for Next.js
|
|
41
|
+
}),
|
|
42
|
+
|
|
43
|
+
// Connection configuration
|
|
44
|
+
connection: v.object({
|
|
45
|
+
apiKeyId: v.optional(v.id("apiKeys")),
|
|
46
|
+
backendUrl: v.string(), // Convex site URL
|
|
47
|
+
features: v.array(v.string()), // ["crm", "events", "checkout", etc.]
|
|
48
|
+
hasFrontendDatabase: v.optional(v.boolean()),
|
|
49
|
+
frontendDatabaseType: v.optional(v.string()), // "convex", "prisma", "drizzle"
|
|
50
|
+
}),
|
|
51
|
+
|
|
52
|
+
// Model mappings (local models → L4YERCAK3 types)
|
|
53
|
+
modelMappings: v.optional(v.array(v.object({
|
|
54
|
+
localModel: v.string(), // "User", "Event", etc.
|
|
55
|
+
layerCakeType: v.string(), // "contact", "event", etc.
|
|
56
|
+
syncDirection: v.union(
|
|
57
|
+
v.literal("push"),
|
|
58
|
+
v.literal("pull"),
|
|
59
|
+
v.literal("bidirectional"),
|
|
60
|
+
v.literal("none")
|
|
61
|
+
),
|
|
62
|
+
confidence: v.number(), // 0-100
|
|
63
|
+
isAutoDetected: v.boolean(),
|
|
64
|
+
fieldMappings: v.optional(v.array(v.object({
|
|
65
|
+
localField: v.string(),
|
|
66
|
+
layerCakeField: v.string(),
|
|
67
|
+
transform: v.optional(v.string()),
|
|
68
|
+
}))),
|
|
69
|
+
}))),
|
|
70
|
+
|
|
71
|
+
// Deployment info (if connected to Web Publishing)
|
|
72
|
+
deployment: v.optional(v.object({
|
|
73
|
+
configurationId: v.optional(v.id("objects")), // deployment_configuration
|
|
74
|
+
productionUrl: v.optional(v.string()),
|
|
75
|
+
stagingUrl: v.optional(v.string()),
|
|
76
|
+
lastDeployedAt: v.optional(v.number()),
|
|
77
|
+
})),
|
|
78
|
+
|
|
79
|
+
// Associated pages
|
|
80
|
+
pageIds: v.optional(v.array(v.id("objects"))),
|
|
81
|
+
|
|
82
|
+
// Sync status
|
|
83
|
+
sync: v.optional(v.object({
|
|
84
|
+
enabled: v.boolean(),
|
|
85
|
+
lastSyncAt: v.optional(v.number()),
|
|
86
|
+
lastSyncStatus: v.optional(v.union(
|
|
87
|
+
v.literal("success"),
|
|
88
|
+
v.literal("partial"),
|
|
89
|
+
v.literal("failed")
|
|
90
|
+
)),
|
|
91
|
+
stats: v.optional(v.object({
|
|
92
|
+
totalPushed: v.number(),
|
|
93
|
+
totalPulled: v.number(),
|
|
94
|
+
lastPushCount: v.number(),
|
|
95
|
+
lastPullCount: v.number(),
|
|
96
|
+
})),
|
|
97
|
+
})),
|
|
98
|
+
|
|
99
|
+
// CLI metadata
|
|
100
|
+
cli: v.optional(v.object({
|
|
101
|
+
registeredAt: v.number(),
|
|
102
|
+
lastActivityAt: v.number(),
|
|
103
|
+
generatedFiles: v.optional(v.array(v.object({
|
|
104
|
+
path: v.string(),
|
|
105
|
+
type: v.string(),
|
|
106
|
+
generatedAt: v.number(),
|
|
107
|
+
}))),
|
|
108
|
+
})),
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
// ============================================================================
|
|
112
|
+
// PUBLIC QUERIES
|
|
113
|
+
// ============================================================================
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Get all connected applications for an organization
|
|
117
|
+
*/
|
|
118
|
+
export const getApplications = query({
|
|
119
|
+
args: {
|
|
120
|
+
sessionId: v.string(),
|
|
121
|
+
organizationId: v.id("organizations"),
|
|
122
|
+
status: v.optional(v.string()),
|
|
123
|
+
},
|
|
124
|
+
handler: async (ctx, args) => {
|
|
125
|
+
await requireAuthenticatedUser(ctx, args.sessionId);
|
|
126
|
+
|
|
127
|
+
const apps = await ctx.db
|
|
128
|
+
.query("objects")
|
|
129
|
+
.withIndex("by_org_type", (q) =>
|
|
130
|
+
q.eq("organizationId", args.organizationId).eq("type", "connected_application")
|
|
131
|
+
)
|
|
132
|
+
.collect();
|
|
133
|
+
|
|
134
|
+
// Filter by status if provided
|
|
135
|
+
if (args.status) {
|
|
136
|
+
return apps.filter((app) => app.status === args.status);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return apps;
|
|
140
|
+
},
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Get a single application by ID
|
|
145
|
+
*/
|
|
146
|
+
export const getApplication = query({
|
|
147
|
+
args: {
|
|
148
|
+
sessionId: v.string(),
|
|
149
|
+
applicationId: v.id("objects"),
|
|
150
|
+
},
|
|
151
|
+
handler: async (ctx, args) => {
|
|
152
|
+
await requireAuthenticatedUser(ctx, args.sessionId);
|
|
153
|
+
|
|
154
|
+
const app = await ctx.db.get(args.applicationId);
|
|
155
|
+
if (!app || app.type !== "connected_application") {
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return app;
|
|
160
|
+
},
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Get application by project path hash (for CLI deduplication)
|
|
165
|
+
*/
|
|
166
|
+
export const getApplicationByPathHash = query({
|
|
167
|
+
args: {
|
|
168
|
+
sessionId: v.string(),
|
|
169
|
+
organizationId: v.id("organizations"),
|
|
170
|
+
projectPathHash: v.string(),
|
|
171
|
+
},
|
|
172
|
+
handler: async (ctx, args) => {
|
|
173
|
+
await requireAuthenticatedUser(ctx, args.sessionId);
|
|
174
|
+
|
|
175
|
+
const apps = await ctx.db
|
|
176
|
+
.query("objects")
|
|
177
|
+
.withIndex("by_org_type", (q) =>
|
|
178
|
+
q.eq("organizationId", args.organizationId).eq("type", "connected_application")
|
|
179
|
+
)
|
|
180
|
+
.collect();
|
|
181
|
+
|
|
182
|
+
// Find by projectPathHash in customProperties
|
|
183
|
+
const app = apps.find((a) => {
|
|
184
|
+
const props = a.customProperties as { source?: { projectPathHash?: string } } | undefined;
|
|
185
|
+
return props?.source?.projectPathHash === args.projectPathHash;
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
return app || null;
|
|
189
|
+
},
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
// ============================================================================
|
|
193
|
+
// PUBLIC MUTATIONS
|
|
194
|
+
// ============================================================================
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Register a new connected application (called by CLI)
|
|
198
|
+
*/
|
|
199
|
+
export const registerApplication = mutation({
|
|
200
|
+
args: {
|
|
201
|
+
sessionId: v.string(),
|
|
202
|
+
organizationId: v.id("organizations"),
|
|
203
|
+
name: v.string(),
|
|
204
|
+
description: v.optional(v.string()),
|
|
205
|
+
source: v.object({
|
|
206
|
+
type: v.union(v.literal("cli"), v.literal("boilerplate"), v.literal("manual")),
|
|
207
|
+
projectPathHash: v.optional(v.string()),
|
|
208
|
+
cliVersion: v.optional(v.string()),
|
|
209
|
+
framework: v.string(),
|
|
210
|
+
frameworkVersion: v.optional(v.string()),
|
|
211
|
+
hasTypeScript: v.optional(v.boolean()),
|
|
212
|
+
routerType: v.optional(v.string()),
|
|
213
|
+
}),
|
|
214
|
+
connection: v.object({
|
|
215
|
+
features: v.array(v.string()),
|
|
216
|
+
hasFrontendDatabase: v.optional(v.boolean()),
|
|
217
|
+
frontendDatabaseType: v.optional(v.string()),
|
|
218
|
+
}),
|
|
219
|
+
modelMappings: v.optional(v.array(v.object({
|
|
220
|
+
localModel: v.string(),
|
|
221
|
+
layerCakeType: v.string(),
|
|
222
|
+
syncDirection: v.union(
|
|
223
|
+
v.literal("push"),
|
|
224
|
+
v.literal("pull"),
|
|
225
|
+
v.literal("bidirectional"),
|
|
226
|
+
v.literal("none")
|
|
227
|
+
),
|
|
228
|
+
confidence: v.number(),
|
|
229
|
+
isAutoDetected: v.boolean(),
|
|
230
|
+
}))),
|
|
231
|
+
},
|
|
232
|
+
handler: async (ctx, args) => {
|
|
233
|
+
const { userId } = await requireAuthenticatedUser(ctx, args.sessionId);
|
|
234
|
+
|
|
235
|
+
// Check if application already exists with this path hash
|
|
236
|
+
if (args.source.projectPathHash) {
|
|
237
|
+
const existingApps = await ctx.db
|
|
238
|
+
.query("objects")
|
|
239
|
+
.withIndex("by_org_type", (q) =>
|
|
240
|
+
q.eq("organizationId", args.organizationId).eq("type", "connected_application")
|
|
241
|
+
)
|
|
242
|
+
.collect();
|
|
243
|
+
|
|
244
|
+
const existing = existingApps.find((a) => {
|
|
245
|
+
const props = a.customProperties as { source?: { projectPathHash?: string } } | undefined;
|
|
246
|
+
return props?.source?.projectPathHash === args.source.projectPathHash;
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
if (existing) {
|
|
250
|
+
// Return existing application instead of creating duplicate
|
|
251
|
+
return {
|
|
252
|
+
applicationId: existing._id,
|
|
253
|
+
existingApplication: true,
|
|
254
|
+
message: "Application already registered for this project",
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Get backend URL from environment
|
|
260
|
+
const backendUrl = process.env.CONVEX_SITE_URL || "https://agreeable-lion-828.convex.site";
|
|
261
|
+
|
|
262
|
+
// Build custom properties
|
|
263
|
+
const customProperties = {
|
|
264
|
+
source: args.source,
|
|
265
|
+
connection: {
|
|
266
|
+
...args.connection,
|
|
267
|
+
backendUrl,
|
|
268
|
+
},
|
|
269
|
+
modelMappings: args.modelMappings || [],
|
|
270
|
+
sync: {
|
|
271
|
+
enabled: false,
|
|
272
|
+
},
|
|
273
|
+
cli: {
|
|
274
|
+
registeredAt: Date.now(),
|
|
275
|
+
lastActivityAt: Date.now(),
|
|
276
|
+
generatedFiles: [],
|
|
277
|
+
},
|
|
278
|
+
};
|
|
279
|
+
|
|
280
|
+
// Create application object
|
|
281
|
+
const applicationId = await ctx.db.insert("objects", {
|
|
282
|
+
organizationId: args.organizationId,
|
|
283
|
+
type: "connected_application",
|
|
284
|
+
subtype: args.source.framework,
|
|
285
|
+
name: args.name,
|
|
286
|
+
description: args.description,
|
|
287
|
+
status: "active",
|
|
288
|
+
customProperties,
|
|
289
|
+
createdBy: userId,
|
|
290
|
+
createdAt: Date.now(),
|
|
291
|
+
updatedAt: Date.now(),
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
// Log activity
|
|
295
|
+
await ctx.db.insert("objectActions", {
|
|
296
|
+
organizationId: args.organizationId,
|
|
297
|
+
objectId: applicationId,
|
|
298
|
+
actionType: "application_registered",
|
|
299
|
+
actionData: {
|
|
300
|
+
source: args.source.type,
|
|
301
|
+
framework: args.source.framework,
|
|
302
|
+
features: args.connection.features,
|
|
303
|
+
},
|
|
304
|
+
performedBy: userId,
|
|
305
|
+
performedAt: Date.now(),
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
return {
|
|
309
|
+
applicationId,
|
|
310
|
+
existingApplication: false,
|
|
311
|
+
backendUrl,
|
|
312
|
+
};
|
|
313
|
+
},
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Update an existing connected application
|
|
318
|
+
*/
|
|
319
|
+
export const updateApplication = mutation({
|
|
320
|
+
args: {
|
|
321
|
+
sessionId: v.string(),
|
|
322
|
+
applicationId: v.id("objects"),
|
|
323
|
+
name: v.optional(v.string()),
|
|
324
|
+
description: v.optional(v.string()),
|
|
325
|
+
status: v.optional(v.union(
|
|
326
|
+
v.literal("active"),
|
|
327
|
+
v.literal("paused"),
|
|
328
|
+
v.literal("disconnected"),
|
|
329
|
+
v.literal("archived")
|
|
330
|
+
)),
|
|
331
|
+
connection: v.optional(v.object({
|
|
332
|
+
features: v.optional(v.array(v.string())),
|
|
333
|
+
apiKeyId: v.optional(v.id("apiKeys")),
|
|
334
|
+
})),
|
|
335
|
+
modelMappings: v.optional(v.array(v.object({
|
|
336
|
+
localModel: v.string(),
|
|
337
|
+
layerCakeType: v.string(),
|
|
338
|
+
syncDirection: v.union(
|
|
339
|
+
v.literal("push"),
|
|
340
|
+
v.literal("pull"),
|
|
341
|
+
v.literal("bidirectional"),
|
|
342
|
+
v.literal("none")
|
|
343
|
+
),
|
|
344
|
+
confidence: v.number(),
|
|
345
|
+
isAutoDetected: v.boolean(),
|
|
346
|
+
fieldMappings: v.optional(v.array(v.object({
|
|
347
|
+
localField: v.string(),
|
|
348
|
+
layerCakeField: v.string(),
|
|
349
|
+
transform: v.optional(v.string()),
|
|
350
|
+
}))),
|
|
351
|
+
}))),
|
|
352
|
+
deployment: v.optional(v.object({
|
|
353
|
+
configurationId: v.optional(v.id("objects")),
|
|
354
|
+
productionUrl: v.optional(v.string()),
|
|
355
|
+
stagingUrl: v.optional(v.string()),
|
|
356
|
+
})),
|
|
357
|
+
},
|
|
358
|
+
handler: async (ctx, args) => {
|
|
359
|
+
const { userId } = await requireAuthenticatedUser(ctx, args.sessionId);
|
|
360
|
+
|
|
361
|
+
const app = await ctx.db.get(args.applicationId);
|
|
362
|
+
if (!app || app.type !== "connected_application") {
|
|
363
|
+
throw new Error("Application not found");
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
const currentProps = (app.customProperties || {}) as Record<string, unknown>;
|
|
367
|
+
const updates: Record<string, unknown> = {
|
|
368
|
+
updatedAt: Date.now(),
|
|
369
|
+
};
|
|
370
|
+
|
|
371
|
+
if (args.name !== undefined) updates.name = args.name;
|
|
372
|
+
if (args.description !== undefined) updates.description = args.description;
|
|
373
|
+
if (args.status !== undefined) updates.status = args.status;
|
|
374
|
+
|
|
375
|
+
// Update customProperties
|
|
376
|
+
const newProps = { ...currentProps };
|
|
377
|
+
|
|
378
|
+
if (args.connection) {
|
|
379
|
+
newProps.connection = {
|
|
380
|
+
...(currentProps.connection as Record<string, unknown> || {}),
|
|
381
|
+
...args.connection,
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
if (args.modelMappings !== undefined) {
|
|
386
|
+
newProps.modelMappings = args.modelMappings;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
if (args.deployment !== undefined) {
|
|
390
|
+
newProps.deployment = {
|
|
391
|
+
...(currentProps.deployment as Record<string, unknown> || {}),
|
|
392
|
+
...args.deployment,
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// Update CLI activity timestamp
|
|
397
|
+
if (newProps.cli) {
|
|
398
|
+
(newProps.cli as Record<string, unknown>).lastActivityAt = Date.now();
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
updates.customProperties = newProps;
|
|
402
|
+
|
|
403
|
+
await ctx.db.patch(args.applicationId, updates);
|
|
404
|
+
|
|
405
|
+
// Log activity
|
|
406
|
+
await ctx.db.insert("objectActions", {
|
|
407
|
+
organizationId: app.organizationId,
|
|
408
|
+
objectId: args.applicationId,
|
|
409
|
+
actionType: "application_updated",
|
|
410
|
+
actionData: {
|
|
411
|
+
updatedFields: Object.keys(args).filter(k => k !== "sessionId" && k !== "applicationId"),
|
|
412
|
+
},
|
|
413
|
+
performedBy: userId,
|
|
414
|
+
performedAt: Date.now(),
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
return { success: true };
|
|
418
|
+
},
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
/**
|
|
422
|
+
* Link API key to application
|
|
423
|
+
*/
|
|
424
|
+
export const linkApiKey = mutation({
|
|
425
|
+
args: {
|
|
426
|
+
sessionId: v.string(),
|
|
427
|
+
applicationId: v.id("objects"),
|
|
428
|
+
apiKeyId: v.id("apiKeys"),
|
|
429
|
+
},
|
|
430
|
+
handler: async (ctx, args) => {
|
|
431
|
+
const { userId } = await requireAuthenticatedUser(ctx, args.sessionId);
|
|
432
|
+
|
|
433
|
+
const app = await ctx.db.get(args.applicationId);
|
|
434
|
+
if (!app || app.type !== "connected_application") {
|
|
435
|
+
throw new Error("Application not found");
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// Verify API key exists and belongs to same org
|
|
439
|
+
const apiKey = await ctx.db.get(args.apiKeyId);
|
|
440
|
+
if (!apiKey || apiKey.organizationId !== app.organizationId) {
|
|
441
|
+
throw new Error("Invalid API key");
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
const currentProps = (app.customProperties || {}) as Record<string, unknown>;
|
|
445
|
+
const connection = (currentProps.connection || {}) as Record<string, unknown>;
|
|
446
|
+
|
|
447
|
+
await ctx.db.patch(args.applicationId, {
|
|
448
|
+
customProperties: {
|
|
449
|
+
...currentProps,
|
|
450
|
+
connection: {
|
|
451
|
+
...connection,
|
|
452
|
+
apiKeyId: args.apiKeyId,
|
|
453
|
+
},
|
|
454
|
+
},
|
|
455
|
+
updatedAt: Date.now(),
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
// Create object link
|
|
459
|
+
await ctx.db.insert("objectLinks", {
|
|
460
|
+
organizationId: app.organizationId,
|
|
461
|
+
fromObjectId: args.applicationId,
|
|
462
|
+
toObjectId: args.applicationId, // Self-link with API key in properties
|
|
463
|
+
linkType: "uses_api_key",
|
|
464
|
+
properties: {
|
|
465
|
+
apiKeyId: args.apiKeyId,
|
|
466
|
+
},
|
|
467
|
+
createdBy: userId,
|
|
468
|
+
createdAt: Date.now(),
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
return { success: true };
|
|
472
|
+
},
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
/**
|
|
476
|
+
* Archive application (soft delete)
|
|
477
|
+
*/
|
|
478
|
+
export const archiveApplication = mutation({
|
|
479
|
+
args: {
|
|
480
|
+
sessionId: v.string(),
|
|
481
|
+
applicationId: v.id("objects"),
|
|
482
|
+
},
|
|
483
|
+
handler: async (ctx, args) => {
|
|
484
|
+
const { userId } = await requireAuthenticatedUser(ctx, args.sessionId);
|
|
485
|
+
|
|
486
|
+
const app = await ctx.db.get(args.applicationId);
|
|
487
|
+
if (!app || app.type !== "connected_application") {
|
|
488
|
+
throw new Error("Application not found");
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
await ctx.db.patch(args.applicationId, {
|
|
492
|
+
status: "archived",
|
|
493
|
+
updatedAt: Date.now(),
|
|
494
|
+
});
|
|
495
|
+
|
|
496
|
+
// Log activity
|
|
497
|
+
await ctx.db.insert("objectActions", {
|
|
498
|
+
organizationId: app.organizationId,
|
|
499
|
+
objectId: args.applicationId,
|
|
500
|
+
actionType: "application_archived",
|
|
501
|
+
actionData: {},
|
|
502
|
+
performedBy: userId,
|
|
503
|
+
performedAt: Date.now(),
|
|
504
|
+
});
|
|
505
|
+
|
|
506
|
+
return { success: true };
|
|
507
|
+
},
|
|
508
|
+
});
|
|
509
|
+
|
|
510
|
+
/**
|
|
511
|
+
* Record generated file (for tracking what CLI generated)
|
|
512
|
+
*/
|
|
513
|
+
export const recordGeneratedFile = mutation({
|
|
514
|
+
args: {
|
|
515
|
+
sessionId: v.string(),
|
|
516
|
+
applicationId: v.id("objects"),
|
|
517
|
+
filePath: v.string(),
|
|
518
|
+
fileType: v.string(), // "api-client", "types", "env", etc.
|
|
519
|
+
},
|
|
520
|
+
handler: async (ctx, args) => {
|
|
521
|
+
await requireAuthenticatedUser(ctx, args.sessionId);
|
|
522
|
+
|
|
523
|
+
const app = await ctx.db.get(args.applicationId);
|
|
524
|
+
if (!app || app.type !== "connected_application") {
|
|
525
|
+
throw new Error("Application not found");
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
const currentProps = (app.customProperties || {}) as Record<string, unknown>;
|
|
529
|
+
const cli = (currentProps.cli || {}) as Record<string, unknown>;
|
|
530
|
+
const generatedFiles = (cli.generatedFiles || []) as Array<{
|
|
531
|
+
path: string;
|
|
532
|
+
type: string;
|
|
533
|
+
generatedAt: number;
|
|
534
|
+
}>;
|
|
535
|
+
|
|
536
|
+
// Update or add file record
|
|
537
|
+
const existingIndex = generatedFiles.findIndex((f) => f.path === args.filePath);
|
|
538
|
+
const newFile = {
|
|
539
|
+
path: args.filePath,
|
|
540
|
+
type: args.fileType,
|
|
541
|
+
generatedAt: Date.now(),
|
|
542
|
+
};
|
|
543
|
+
|
|
544
|
+
if (existingIndex >= 0) {
|
|
545
|
+
generatedFiles[existingIndex] = newFile;
|
|
546
|
+
} else {
|
|
547
|
+
generatedFiles.push(newFile);
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
await ctx.db.patch(args.applicationId, {
|
|
551
|
+
customProperties: {
|
|
552
|
+
...currentProps,
|
|
553
|
+
cli: {
|
|
554
|
+
...cli,
|
|
555
|
+
generatedFiles,
|
|
556
|
+
lastActivityAt: Date.now(),
|
|
557
|
+
},
|
|
558
|
+
},
|
|
559
|
+
updatedAt: Date.now(),
|
|
560
|
+
});
|
|
561
|
+
|
|
562
|
+
return { success: true };
|
|
563
|
+
},
|
|
564
|
+
});
|
|
565
|
+
|
|
566
|
+
// ============================================================================
|
|
567
|
+
// INTERNAL QUERIES (for HTTP endpoints)
|
|
568
|
+
// ============================================================================
|
|
569
|
+
|
|
570
|
+
/**
|
|
571
|
+
* Get application by path hash (internal - no session required)
|
|
572
|
+
*/
|
|
573
|
+
export const getApplicationByPathHashInternal = internalQuery({
|
|
574
|
+
args: {
|
|
575
|
+
organizationId: v.id("organizations"),
|
|
576
|
+
projectPathHash: v.string(),
|
|
577
|
+
},
|
|
578
|
+
handler: async (ctx, args) => {
|
|
579
|
+
const apps = await ctx.db
|
|
580
|
+
.query("objects")
|
|
581
|
+
.withIndex("by_org_type", (q) =>
|
|
582
|
+
q.eq("organizationId", args.organizationId).eq("type", "connected_application")
|
|
583
|
+
)
|
|
584
|
+
.collect();
|
|
585
|
+
|
|
586
|
+
const app = apps.find((a) => {
|
|
587
|
+
const props = a.customProperties as { source?: { projectPathHash?: string } } | undefined;
|
|
588
|
+
return props?.source?.projectPathHash === args.projectPathHash;
|
|
589
|
+
});
|
|
590
|
+
|
|
591
|
+
if (!app) return null;
|
|
592
|
+
|
|
593
|
+
return {
|
|
594
|
+
id: app._id,
|
|
595
|
+
name: app.name,
|
|
596
|
+
status: app.status,
|
|
597
|
+
features: ((app.customProperties as any)?.connection?.features || []) as string[],
|
|
598
|
+
lastActivityAt: ((app.customProperties as any)?.cli?.lastActivityAt || app.updatedAt) as number,
|
|
599
|
+
};
|
|
600
|
+
},
|
|
601
|
+
});
|
|
602
|
+
|
|
603
|
+
/**
|
|
604
|
+
* Get application by ID (internal)
|
|
605
|
+
*/
|
|
606
|
+
export const getApplicationInternal = internalQuery({
|
|
607
|
+
args: {
|
|
608
|
+
applicationId: v.id("objects"),
|
|
609
|
+
organizationId: v.id("organizations"),
|
|
610
|
+
},
|
|
611
|
+
handler: async (ctx, args) => {
|
|
612
|
+
const app = await ctx.db.get(args.applicationId);
|
|
613
|
+
if (!app || app.type !== "connected_application" || app.organizationId !== args.organizationId) {
|
|
614
|
+
return null;
|
|
615
|
+
}
|
|
616
|
+
return app;
|
|
617
|
+
},
|
|
618
|
+
});
|
|
619
|
+
|
|
620
|
+
/**
|
|
621
|
+
* List applications for organization (internal)
|
|
622
|
+
*/
|
|
623
|
+
export const listApplicationsInternal = internalQuery({
|
|
624
|
+
args: {
|
|
625
|
+
organizationId: v.id("organizations"),
|
|
626
|
+
status: v.optional(v.string()),
|
|
627
|
+
limit: v.optional(v.number()),
|
|
628
|
+
offset: v.optional(v.number()),
|
|
629
|
+
},
|
|
630
|
+
handler: async (ctx, args) => {
|
|
631
|
+
let apps = await ctx.db
|
|
632
|
+
.query("objects")
|
|
633
|
+
.withIndex("by_org_type", (q) =>
|
|
634
|
+
q.eq("organizationId", args.organizationId).eq("type", "connected_application")
|
|
635
|
+
)
|
|
636
|
+
.collect();
|
|
637
|
+
|
|
638
|
+
// Filter by status
|
|
639
|
+
if (args.status) {
|
|
640
|
+
apps = apps.filter((app) => app.status === args.status);
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
const total = apps.length;
|
|
644
|
+
|
|
645
|
+
// Apply pagination
|
|
646
|
+
const offset = args.offset || 0;
|
|
647
|
+
const limit = args.limit || 50;
|
|
648
|
+
apps = apps.slice(offset, offset + limit);
|
|
649
|
+
|
|
650
|
+
return {
|
|
651
|
+
applications: apps,
|
|
652
|
+
total,
|
|
653
|
+
hasMore: offset + apps.length < total,
|
|
654
|
+
};
|
|
655
|
+
},
|
|
656
|
+
});
|
|
657
|
+
|
|
658
|
+
// ============================================================================
|
|
659
|
+
// INTERNAL MUTATIONS (for HTTP endpoints)
|
|
660
|
+
// ============================================================================
|
|
661
|
+
|
|
662
|
+
/**
|
|
663
|
+
* Register application (internal - API key auth)
|
|
664
|
+
*/
|
|
665
|
+
export const registerApplicationInternal = internalMutation({
|
|
666
|
+
args: {
|
|
667
|
+
organizationId: v.id("organizations"),
|
|
668
|
+
name: v.string(),
|
|
669
|
+
description: v.optional(v.string()),
|
|
670
|
+
source: v.object({
|
|
671
|
+
type: v.union(v.literal("cli"), v.literal("boilerplate"), v.literal("manual")),
|
|
672
|
+
projectPathHash: v.optional(v.string()),
|
|
673
|
+
cliVersion: v.optional(v.string()),
|
|
674
|
+
framework: v.string(),
|
|
675
|
+
frameworkVersion: v.optional(v.string()),
|
|
676
|
+
hasTypeScript: v.optional(v.boolean()),
|
|
677
|
+
routerType: v.optional(v.string()),
|
|
678
|
+
}),
|
|
679
|
+
connection: v.object({
|
|
680
|
+
features: v.array(v.string()),
|
|
681
|
+
hasFrontendDatabase: v.optional(v.boolean()),
|
|
682
|
+
frontendDatabaseType: v.optional(v.string()),
|
|
683
|
+
}),
|
|
684
|
+
modelMappings: v.optional(v.array(v.object({
|
|
685
|
+
localModel: v.string(),
|
|
686
|
+
layerCakeType: v.string(),
|
|
687
|
+
syncDirection: v.string(),
|
|
688
|
+
confidence: v.number(),
|
|
689
|
+
isAutoDetected: v.boolean(),
|
|
690
|
+
}))),
|
|
691
|
+
},
|
|
692
|
+
handler: async (ctx, args) => {
|
|
693
|
+
// Check for existing application with same path hash
|
|
694
|
+
if (args.source.projectPathHash) {
|
|
695
|
+
const existingApps = await ctx.db
|
|
696
|
+
.query("objects")
|
|
697
|
+
.withIndex("by_org_type", (q) =>
|
|
698
|
+
q.eq("organizationId", args.organizationId).eq("type", "connected_application")
|
|
699
|
+
)
|
|
700
|
+
.collect();
|
|
701
|
+
|
|
702
|
+
const existing = existingApps.find((a) => {
|
|
703
|
+
const props = a.customProperties as { source?: { projectPathHash?: string } } | undefined;
|
|
704
|
+
return props?.source?.projectPathHash === args.source.projectPathHash;
|
|
705
|
+
});
|
|
706
|
+
|
|
707
|
+
if (existing) {
|
|
708
|
+
return {
|
|
709
|
+
applicationId: existing._id,
|
|
710
|
+
existingApplication: true,
|
|
711
|
+
};
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
const backendUrl = process.env.CONVEX_SITE_URL || "https://agreeable-lion-828.convex.site";
|
|
716
|
+
|
|
717
|
+
const customProperties = {
|
|
718
|
+
source: args.source,
|
|
719
|
+
connection: {
|
|
720
|
+
...args.connection,
|
|
721
|
+
backendUrl,
|
|
722
|
+
},
|
|
723
|
+
modelMappings: args.modelMappings || [],
|
|
724
|
+
sync: { enabled: false },
|
|
725
|
+
cli: {
|
|
726
|
+
registeredAt: Date.now(),
|
|
727
|
+
lastActivityAt: Date.now(),
|
|
728
|
+
generatedFiles: [],
|
|
729
|
+
},
|
|
730
|
+
};
|
|
731
|
+
|
|
732
|
+
const applicationId = await ctx.db.insert("objects", {
|
|
733
|
+
organizationId: args.organizationId,
|
|
734
|
+
type: "connected_application",
|
|
735
|
+
subtype: args.source.framework,
|
|
736
|
+
name: args.name,
|
|
737
|
+
description: args.description,
|
|
738
|
+
status: "active",
|
|
739
|
+
customProperties,
|
|
740
|
+
createdAt: Date.now(),
|
|
741
|
+
updatedAt: Date.now(),
|
|
742
|
+
});
|
|
743
|
+
|
|
744
|
+
return {
|
|
745
|
+
applicationId,
|
|
746
|
+
existingApplication: false,
|
|
747
|
+
backendUrl,
|
|
748
|
+
};
|
|
749
|
+
},
|
|
750
|
+
});
|
|
751
|
+
|
|
752
|
+
/**
|
|
753
|
+
* Update application (internal - API key auth)
|
|
754
|
+
*/
|
|
755
|
+
export const updateApplicationInternal = internalMutation({
|
|
756
|
+
args: {
|
|
757
|
+
applicationId: v.id("objects"),
|
|
758
|
+
organizationId: v.id("organizations"),
|
|
759
|
+
name: v.optional(v.string()),
|
|
760
|
+
description: v.optional(v.string()),
|
|
761
|
+
status: v.optional(v.string()),
|
|
762
|
+
connection: v.optional(v.object({
|
|
763
|
+
features: v.optional(v.array(v.string())),
|
|
764
|
+
hasFrontendDatabase: v.optional(v.boolean()),
|
|
765
|
+
frontendDatabaseType: v.optional(v.string()),
|
|
766
|
+
})),
|
|
767
|
+
deployment: v.optional(v.object({
|
|
768
|
+
githubRepo: v.optional(v.string()),
|
|
769
|
+
productionUrl: v.optional(v.string()),
|
|
770
|
+
stagingUrl: v.optional(v.string()),
|
|
771
|
+
})),
|
|
772
|
+
modelMappings: v.optional(v.array(v.any())),
|
|
773
|
+
},
|
|
774
|
+
handler: async (ctx, args) => {
|
|
775
|
+
const app = await ctx.db.get(args.applicationId);
|
|
776
|
+
if (!app || app.type !== "connected_application" || app.organizationId !== args.organizationId) {
|
|
777
|
+
throw new Error("Application not found");
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
const currentProps = (app.customProperties || {}) as Record<string, unknown>;
|
|
781
|
+
const updates: Record<string, unknown> = { updatedAt: Date.now() };
|
|
782
|
+
|
|
783
|
+
if (args.name !== undefined) updates.name = args.name;
|
|
784
|
+
if (args.description !== undefined) updates.description = args.description;
|
|
785
|
+
if (args.status !== undefined) updates.status = args.status;
|
|
786
|
+
|
|
787
|
+
const newProps = { ...currentProps };
|
|
788
|
+
|
|
789
|
+
if (args.connection) {
|
|
790
|
+
newProps.connection = {
|
|
791
|
+
...(currentProps.connection as Record<string, unknown> || {}),
|
|
792
|
+
...args.connection,
|
|
793
|
+
};
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
if (args.deployment) {
|
|
797
|
+
newProps.deployment = {
|
|
798
|
+
...(currentProps.deployment as Record<string, unknown> || {}),
|
|
799
|
+
...args.deployment,
|
|
800
|
+
};
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
if (args.modelMappings !== undefined) {
|
|
804
|
+
newProps.modelMappings = args.modelMappings;
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
if (newProps.cli) {
|
|
808
|
+
(newProps.cli as Record<string, unknown>).lastActivityAt = Date.now();
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
updates.customProperties = newProps;
|
|
812
|
+
|
|
813
|
+
await ctx.db.patch(args.applicationId, updates);
|
|
814
|
+
|
|
815
|
+
return { success: true };
|
|
816
|
+
},
|
|
817
|
+
});
|