@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.
@@ -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
+ });