parse-dashboard 7.3.0-alpha.9 → 7.3.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.
@@ -7,6 +7,7 @@ const Authentication = require('./Authentication.js');
7
7
  const fs = require('fs');
8
8
  const ConfigKeyCache = require('./configKeyCache.js');
9
9
  const currentVersionFeatures = require('../package.json').parseDashboardFeatures;
10
+ const Parse = require('parse/node');
10
11
 
11
12
  let newFeaturesInLatestVersion = [];
12
13
 
@@ -50,8 +51,7 @@ function checkIfIconsExistForApps(apps, iconsFolder) {
50
51
  if ('ENOENT' == err.code) {// file does not exist
51
52
  console.warn('Icon with file name: ' + iconName + ' couldn\'t be found in icons folder!');
52
53
  } else {
53
- console.log(
54
- 'An error occurd while checking for icons, please check permission!');
54
+ console.warn('An error occurred while checking for icons, please check permission!');
55
55
  }
56
56
  } else {
57
57
  //every thing was ok so for example you can read it and send it to client
@@ -63,6 +63,11 @@ function checkIfIconsExistForApps(apps, iconsFolder) {
63
63
  module.exports = function(config, options) {
64
64
  options = options || {};
65
65
  const app = express();
66
+
67
+ // Parse JSON and URL-encoded request bodies
68
+ app.use(express.json());
69
+ app.use(express.urlencoded({ extended: true }));
70
+
66
71
  // Serve public files.
67
72
  app.use(express.static(path.join(__dirname,'public')));
68
73
 
@@ -84,8 +89,12 @@ module.exports = function(config, options) {
84
89
  if (err.code !== 'EBADCSRFTOKEN') {return next(err)}
85
90
 
86
91
  // handle CSRF token errors here
87
- res.status(403)
88
- res.send('form tampered with')
92
+ res.status(403);
93
+ if (req.xhr || req.headers.accept?.indexOf('json') > -1) {
94
+ res.json({ error: 'CSRF token validation failed. Please refresh the page and try again.' });
95
+ } else {
96
+ res.send('CSRF token validation failed. Please refresh the page and try again.');
97
+ }
89
98
  });
90
99
 
91
100
  // Serve the configuration.
@@ -94,6 +103,7 @@ module.exports = function(config, options) {
94
103
  const response = {
95
104
  apps,
96
105
  newFeaturesInLatestVersion,
106
+ agent: config.agent,
97
107
  };
98
108
 
99
109
  //Based on advice from Doug Wilson here:
@@ -181,6 +191,857 @@ module.exports = function(config, options) {
181
191
  res.send({ success: false, error: 'Something went wrong.' });
182
192
  });
183
193
 
194
+ // In-memory conversation storage (consider using Redis in future)
195
+ const conversations = new Map();
196
+
197
+ // Agent API endpoint for handling AI requests - scoped to specific app
198
+ app.post('/apps/:appId/agent', async (req, res) => {
199
+ try {
200
+ const { message, modelName, conversationId, permissions } = req.body;
201
+ const { appId } = req.params;
202
+
203
+ if (!message || typeof message !== 'string' || message.trim() === '') {
204
+ return res.status(400).json({ error: 'Message is required' });
205
+ }
206
+
207
+ if (!modelName || typeof modelName !== 'string') {
208
+ return res.status(400).json({ error: 'Model name is required' });
209
+ }
210
+
211
+ if (!appId || typeof appId !== 'string') {
212
+ return res.status(400).json({ error: 'App ID is required' });
213
+ }
214
+
215
+ // Check if agent configuration exists
216
+ if (!config.agent || !config.agent.models || !Array.isArray(config.agent.models)) {
217
+ return res.status(400).json({ error: 'No agent configuration found' });
218
+ }
219
+
220
+ // Find the app in the configuration
221
+ const app = config.apps.find(app => (app.appNameForURL || app.appName) === appId);
222
+ if (!app) {
223
+ return res.status(404).json({ error: `App "${appId}" not found` });
224
+ }
225
+
226
+ // Find the requested model
227
+ const modelConfig = config.agent.models.find(model => model.name === modelName);
228
+ if (!modelConfig) {
229
+ return res.status(400).json({ error: `Model "${modelName}" not found in configuration` });
230
+ }
231
+
232
+ // Validate model configuration
233
+ const { provider, model, apiKey } = modelConfig;
234
+ if (!provider || !model || !apiKey) {
235
+ return res.status(400).json({ error: 'Model configuration is incomplete' });
236
+ }
237
+
238
+ if (apiKey === 'xxxxx' || apiKey.includes('xxx')) {
239
+ return res.status(400).json({ error: 'Please replace the placeholder API key with your actual API key' });
240
+ }
241
+
242
+ // Only support OpenAI for now
243
+ if (provider.toLowerCase() !== 'openai') {
244
+ return res.status(400).json({ error: `Provider "${provider}" is not supported yet` });
245
+ }
246
+
247
+ // Get or create conversation history
248
+ const conversationKey = `${appId}_${conversationId || 'default'}`;
249
+ if (!conversations.has(conversationKey)) {
250
+ conversations.set(conversationKey, []);
251
+ }
252
+
253
+ const conversationHistory = conversations.get(conversationKey);
254
+
255
+ // Array to track database operations for this request
256
+ const operationLog = [];
257
+
258
+ // Make request to OpenAI API with app context and conversation history
259
+ const response = await makeOpenAIRequest(message, model, apiKey, app, conversationHistory, operationLog, permissions);
260
+
261
+ // Update conversation history with user message and AI response
262
+ conversationHistory.push(
263
+ { role: 'user', content: message },
264
+ { role: 'assistant', content: response || 'Operation completed successfully.' }
265
+ );
266
+
267
+ // Keep conversation history to a reasonable size (last 20 messages)
268
+ if (conversationHistory.length > 20) {
269
+ conversationHistory.splice(0, conversationHistory.length - 20);
270
+ }
271
+
272
+ // Generate or use provided conversation ID
273
+ const finalConversationId = conversationId || `conv_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
274
+
275
+ res.json({
276
+ response,
277
+ conversationId: finalConversationId,
278
+ debug: {
279
+ timestamp: new Date().toISOString(),
280
+ appId: app.appId,
281
+ modelUsed: model,
282
+ operations: operationLog
283
+ }
284
+ });
285
+
286
+ } catch (error) {
287
+ // Return the full error message to help with debugging
288
+ const errorMessage = error.message || 'Provider error';
289
+ res.status(500).json({ error: `Error: ${errorMessage}` });
290
+ }
291
+ });
292
+
293
+ /**
294
+ * Database function tools for the AI agent
295
+ */
296
+ const databaseTools = [
297
+ {
298
+ type: 'function',
299
+ function: {
300
+ name: 'queryClass',
301
+ description: 'Query a Parse class/table to retrieve objects. Use this to fetch data from the database.',
302
+ parameters: {
303
+ type: 'object',
304
+ properties: {
305
+ className: {
306
+ type: 'string',
307
+ description: 'The name of the Parse class to query'
308
+ },
309
+ where: {
310
+ type: 'object',
311
+ description: 'Query constraints as a JSON object (e.g., {"name": "John", "age": {"$gte": 18}})'
312
+ },
313
+ limit: {
314
+ type: 'number',
315
+ description: 'Maximum number of results to return (default 100, max 1000)'
316
+ },
317
+ skip: {
318
+ type: 'number',
319
+ description: 'Number of results to skip for pagination'
320
+ },
321
+ order: {
322
+ type: 'string',
323
+ description: 'Field to order by (prefix with \'-\' for descending, e.g., \'-createdAt\')'
324
+ },
325
+ include: {
326
+ type: 'array',
327
+ items: { type: 'string' },
328
+ description: 'Array of pointer fields to include/populate'
329
+ },
330
+ select: {
331
+ type: 'array',
332
+ items: { type: 'string' },
333
+ description: 'Array of fields to select (if not provided, all fields are returned)'
334
+ }
335
+ },
336
+ required: ['className']
337
+ }
338
+ }
339
+ },
340
+ {
341
+ type: 'function',
342
+ function: {
343
+ name: 'createObject',
344
+ description: 'Create a new object in a Parse class/table. IMPORTANT: This is a write operation that requires explicit user confirmation before execution. You must ask the user to confirm before calling this function. You MUST provide the objectData parameter with the actual field values to be saved in the object.',
345
+ parameters: {
346
+ type: 'object',
347
+ properties: {
348
+ className: {
349
+ type: 'string',
350
+ description: 'The name of the Parse class to create an object in'
351
+ },
352
+ objectData: {
353
+ type: 'object',
354
+ description: 'REQUIRED: The object fields and values for the new object as a JSON object. Example: {\'model\': \'Honda Civic\', \'year\': 2023, \'brand\': \'Honda\'}. This parameter is mandatory and cannot be empty.',
355
+ additionalProperties: true
356
+ },
357
+ confirmed: {
358
+ type: 'boolean',
359
+ description: 'Must be true to indicate user has explicitly confirmed this write operation',
360
+ default: false
361
+ }
362
+ },
363
+ required: ['className', 'objectData', 'confirmed']
364
+ }
365
+ }
366
+ },
367
+ {
368
+ type: 'function',
369
+ function: {
370
+ name: 'updateObject',
371
+ description: 'Update an existing object in a Parse class/table. IMPORTANT: This is a write operation that requires explicit user confirmation before execution. You must ask the user to confirm before calling this function.',
372
+ parameters: {
373
+ type: 'object',
374
+ properties: {
375
+ className: {
376
+ type: 'string',
377
+ description: 'The name of the Parse class containing the object'
378
+ },
379
+ objectId: {
380
+ type: 'string',
381
+ description: 'The objectId of the object to update'
382
+ },
383
+ objectData: {
384
+ type: 'object',
385
+ description: 'The fields to update as a JSON object'
386
+ },
387
+ confirmed: {
388
+ type: 'boolean',
389
+ description: 'Must be true to indicate user has explicitly confirmed this write operation',
390
+ default: false
391
+ }
392
+ },
393
+ required: ['className', 'objectId', 'objectData', 'confirmed']
394
+ }
395
+ }
396
+ },
397
+ {
398
+ type: 'function',
399
+ function: {
400
+ name: 'deleteObject',
401
+ description: 'Delete a SINGLE OBJECT/ROW from a Parse class/table using its objectId. Use this when you want to delete one specific record/object, not the entire class. IMPORTANT: This is a destructive write operation that requires explicit user confirmation before execution. You must ask the user to confirm before calling this function.',
402
+ parameters: {
403
+ type: 'object',
404
+ properties: {
405
+ className: {
406
+ type: 'string',
407
+ description: 'The name of the Parse class containing the object'
408
+ },
409
+ objectId: {
410
+ type: 'string',
411
+ description: 'The objectId of the specific object/record to delete'
412
+ },
413
+ confirmed: {
414
+ type: 'boolean',
415
+ description: 'Must be true to indicate user has explicitly confirmed this destructive operation',
416
+ default: false
417
+ }
418
+ },
419
+ required: ['className', 'objectId', 'confirmed']
420
+ }
421
+ }
422
+ },
423
+ {
424
+ type: 'function',
425
+ function: {
426
+ name: 'getSchema',
427
+ description: 'Get the schema information for Parse classes. Use this to understand the structure of classes/tables.',
428
+ parameters: {
429
+ type: 'object',
430
+ properties: {
431
+ className: {
432
+ type: 'string',
433
+ description: 'The name of the Parse class to get schema for (optional - if not provided, returns all schemas)'
434
+ }
435
+ }
436
+ }
437
+ }
438
+ },
439
+ {
440
+ type: 'function',
441
+ function: {
442
+ name: 'countObjects',
443
+ description: 'Count objects in a Parse class/table that match given constraints.',
444
+ parameters: {
445
+ type: 'object',
446
+ properties: {
447
+ className: {
448
+ type: 'string',
449
+ description: 'The name of the Parse class to count objects in'
450
+ },
451
+ where: {
452
+ type: 'object',
453
+ description: 'Query constraints as a JSON object (optional)'
454
+ }
455
+ },
456
+ required: ['className']
457
+ }
458
+ }
459
+ },
460
+ {
461
+ type: 'function',
462
+ function: {
463
+ name: 'createClass',
464
+ description: 'Create a new Parse class/table with specified fields. This creates the class structure without any objects.',
465
+ parameters: {
466
+ type: 'object',
467
+ properties: {
468
+ className: {
469
+ type: 'string',
470
+ description: 'The name of the Parse class to create'
471
+ },
472
+ fields: {
473
+ type: 'object',
474
+ description: 'Fields to define for the class as a JSON object where keys are field names and values are field types (e.g., {"name": "String", "age": "Number", "email": "String"})'
475
+ },
476
+ confirmed: {
477
+ type: 'boolean',
478
+ description: 'Must be true to indicate user has explicitly confirmed this operation',
479
+ default: false
480
+ }
481
+ },
482
+ required: ['className', 'confirmed']
483
+ }
484
+ }
485
+ },
486
+ {
487
+ type: 'function',
488
+ function: {
489
+ name: 'deleteClass',
490
+ description: 'Delete an ENTIRE Parse class/table (the class itself) and ALL its data. Use this when the user wants to delete/remove the entire class/table, not individual objects. This completely removes the class schema and all objects within it. IMPORTANT: This is a highly destructive operation that permanently removes the entire class structure and all objects within it. Requires explicit user confirmation before execution.',
491
+ parameters: {
492
+ type: 'object',
493
+ properties: {
494
+ className: {
495
+ type: 'string',
496
+ description: 'The name of the Parse class/table to completely delete/remove'
497
+ },
498
+ confirmed: {
499
+ type: 'boolean',
500
+ description: 'Must be true to indicate user has explicitly confirmed this highly destructive operation',
501
+ default: false
502
+ }
503
+ },
504
+ required: ['className', 'confirmed']
505
+ }
506
+ }
507
+ }
508
+ ];
509
+
510
+ /**
511
+ * Execute database function calls
512
+ */
513
+ async function executeDatabaseFunction(functionName, args, appContext, operationLog = [], permissions = {}) {
514
+ // Check permissions before executing write operations
515
+ const writeOperations = ['deleteObject', 'deleteClass', 'updateObject', 'createObject', 'createClass'];
516
+
517
+ if (writeOperations.includes(functionName)) {
518
+ // Handle both boolean and string values for permissions
519
+ const permissionValue = permissions && permissions[functionName];
520
+ const hasPermission = permissionValue === true || permissionValue === 'true';
521
+
522
+ if (!hasPermission) {
523
+ throw new Error(`Permission denied: The "${functionName}" operation is currently disabled in the permissions settings. Please enable this permission in the Parse Dashboard Permissions menu if you want to allow this operation.`);
524
+ }
525
+ }
526
+
527
+ // Configure Parse for this app context
528
+ Parse.initialize(appContext.appId, undefined, appContext.masterKey);
529
+ Parse.serverURL = appContext.serverURL;
530
+ Parse.masterKey = appContext.masterKey;
531
+
532
+ try {
533
+ switch (functionName) {
534
+ case 'queryClass': {
535
+ const { className, where = {}, limit = 100, skip = 0, order, include = [], select = [] } = args;
536
+ const query = new Parse.Query(className);
537
+
538
+ // Apply constraints
539
+ Object.keys(where).forEach(key => {
540
+ const value = where[key];
541
+ if (typeof value === 'object' && value !== null) {
542
+ // Handle complex queries like {$gte: 18}
543
+ Object.keys(value).forEach(op => {
544
+ switch (op) {
545
+ case '$gt': query.greaterThan(key, value[op]); break;
546
+ case '$gte': query.greaterThanOrEqualTo(key, value[op]); break;
547
+ case '$lt': query.lessThan(key, value[op]); break;
548
+ case '$lte': query.lessThanOrEqualTo(key, value[op]); break;
549
+ case '$ne': query.notEqualTo(key, value[op]); break;
550
+ case '$in': query.containedIn(key, value[op]); break;
551
+ case '$nin': query.notContainedIn(key, value[op]); break;
552
+ case '$exists':
553
+ if (value[op]) {query.exists(key);}
554
+ else {query.doesNotExist(key);}
555
+ break;
556
+ case '$regex': query.matches(key, new RegExp(value[op], value.$options || '')); break;
557
+ }
558
+ });
559
+ } else {
560
+ query.equalTo(key, value);
561
+ }
562
+ });
563
+
564
+ if (limit) {query.limit(Math.min(limit, 1000));}
565
+ if (skip) {query.skip(skip);}
566
+ if (order) {
567
+ if (order.startsWith('-')) {
568
+ query.descending(order.substring(1));
569
+ } else {
570
+ query.ascending(order);
571
+ }
572
+ }
573
+ if (include.length > 0) {query.include(include);}
574
+ if (select.length > 0) {query.select(select);}
575
+
576
+ const results = await query.find({ useMasterKey: true });
577
+ const resultData = results.map(obj => obj.toJSON());
578
+ const operationSummary = {
579
+ operation: 'queryClass',
580
+ className,
581
+ resultCount: results.length,
582
+ timestamp: new Date().toISOString()
583
+ };
584
+
585
+ operationLog.push(operationSummary);
586
+ return resultData;
587
+ }
588
+
589
+ case 'createObject': {
590
+ const { className, objectData, confirmed } = args;
591
+
592
+ // Validate required parameters
593
+ if (!objectData || typeof objectData !== 'object' || Object.keys(objectData).length === 0) {
594
+ throw new Error('Missing or empty \'objectData\' parameter. To create an object, you must provide the objectData fields and values as a JSON object. For example: {\'model\': \'Honda Civic\', \'year\': 2023, \'brand\': \'Honda\'}');
595
+ }
596
+
597
+ // Require explicit confirmation for write operations
598
+ if (!confirmed) {
599
+ throw new Error(`Creating objects requires user confirmation. The AI should ask for permission before creating objects in the ${className} class.`);
600
+ }
601
+
602
+ const ParseObject = Parse.Object.extend(className);
603
+ const object = new ParseObject();
604
+
605
+ Object.keys(objectData).forEach(key => {
606
+ object.set(key, objectData[key]);
607
+ });
608
+
609
+ const result = await object.save(null, { useMasterKey: true });
610
+ const resultData = result.toJSON();
611
+
612
+ return resultData;
613
+ }
614
+
615
+ case 'updateObject': {
616
+ const { className, objectId, objectData, confirmed } = args;
617
+
618
+ // Require explicit confirmation for write operations
619
+ if (!confirmed) {
620
+ throw new Error(`Updating objects requires user confirmation. The AI should ask for permission before updating object ${objectId} in the ${className} class.`);
621
+ }
622
+
623
+ const query = new Parse.Query(className);
624
+ const object = await query.get(objectId, { useMasterKey: true });
625
+
626
+ Object.keys(objectData).forEach(key => {
627
+ object.set(key, objectData[key]);
628
+ });
629
+
630
+ const result = await object.save(null, { useMasterKey: true });
631
+ const resultData = result.toJSON();
632
+
633
+ return resultData;
634
+ }
635
+
636
+ case 'deleteObject': {
637
+ const { className, objectId, confirmed } = args;
638
+
639
+ // Require explicit confirmation for destructive operations
640
+ if (!confirmed) {
641
+ throw new Error(`Deleting objects requires user confirmation. The AI should ask for permission before permanently deleting object ${objectId} from the ${className} class.`);
642
+ }
643
+
644
+ const query = new Parse.Query(className);
645
+ const object = await query.get(objectId, { useMasterKey: true });
646
+
647
+ await object.destroy({ useMasterKey: true });
648
+
649
+ const result = { success: true, objectId };
650
+ return result;
651
+ }
652
+
653
+ case 'getSchema': {
654
+ const { className } = args;
655
+ let result;
656
+ if (className) {
657
+ result = await new Parse.Schema(className).get({ useMasterKey: true });
658
+ } else {
659
+ result = await Parse.Schema.all({ useMasterKey: true });
660
+ }
661
+ return result;
662
+ }
663
+
664
+ case 'countObjects': {
665
+ const { className, where = {} } = args;
666
+ const query = new Parse.Query(className);
667
+
668
+ Object.keys(where).forEach(key => {
669
+ const value = where[key];
670
+ if (typeof value === 'object' && value !== null) {
671
+ Object.keys(value).forEach(op => {
672
+ switch (op) {
673
+ case '$gt': query.greaterThan(key, value[op]); break;
674
+ case '$gte': query.greaterThanOrEqualTo(key, value[op]); break;
675
+ case '$lt': query.lessThan(key, value[op]); break;
676
+ case '$lte': query.lessThanOrEqualTo(key, value[op]); break;
677
+ case '$ne': query.notEqualTo(key, value[op]); break;
678
+ case '$in': query.containedIn(key, value[op]); break;
679
+ case '$nin': query.notContainedIn(key, value[op]); break;
680
+ case '$exists':
681
+ if (value[op]) {query.exists(key);}
682
+ else {query.doesNotExist(key);}
683
+ break;
684
+ }
685
+ });
686
+ } else {
687
+ query.equalTo(key, value);
688
+ }
689
+ });
690
+
691
+ const count = await query.count({ useMasterKey: true });
692
+
693
+ const result = { count };
694
+ return result;
695
+ }
696
+
697
+ case 'createClass': {
698
+ const { className, fields = {}, confirmed } = args;
699
+
700
+ // Require explicit confirmation for class creation
701
+ if (!confirmed) {
702
+ throw new Error(`Creating classes requires user confirmation. The AI should ask for permission before creating the ${className} class.`);
703
+ }
704
+
705
+ const schema = new Parse.Schema(className);
706
+
707
+ // Add fields to the schema
708
+ Object.keys(fields).forEach(fieldName => {
709
+ const fieldType = fields[fieldName];
710
+ switch (fieldType.toLowerCase()) {
711
+ case 'string':
712
+ schema.addString(fieldName);
713
+ break;
714
+ case 'number':
715
+ schema.addNumber(fieldName);
716
+ break;
717
+ case 'boolean':
718
+ schema.addBoolean(fieldName);
719
+ break;
720
+ case 'date':
721
+ schema.addDate(fieldName);
722
+ break;
723
+ case 'array':
724
+ schema.addArray(fieldName);
725
+ break;
726
+ case 'object':
727
+ schema.addObject(fieldName);
728
+ break;
729
+ case 'geopoint':
730
+ schema.addGeoPoint(fieldName);
731
+ break;
732
+ case 'file':
733
+ schema.addFile(fieldName);
734
+ break;
735
+ default:
736
+ // For pointer fields or unknown types, try to add as string
737
+ schema.addString(fieldName);
738
+ break;
739
+ }
740
+ });
741
+
742
+ const result = await schema.save({ useMasterKey: true });
743
+
744
+ const resultData = { success: true, className, schema: result };
745
+ return resultData;
746
+ }
747
+
748
+ case 'deleteClass': {
749
+ const { className, confirmed } = args;
750
+
751
+ // Require explicit confirmation for class deletion - this is highly destructive
752
+ if (!confirmed) {
753
+ throw new Error(`Deleting classes requires user confirmation. The AI should ask for permission before permanently deleting the ${className} class and ALL its data.`);
754
+ }
755
+
756
+ // Check if the class exists first
757
+ try {
758
+ await new Parse.Schema(className).get({ useMasterKey: true });
759
+ } catch (error) {
760
+ if (error.code === 103) {
761
+ throw new Error(`Class "${className}" does not exist.`);
762
+ }
763
+ throw error;
764
+ }
765
+
766
+ // Delete the class and all its data
767
+ const schema = new Parse.Schema(className);
768
+
769
+ try {
770
+ // First purge all objects from the class
771
+ await schema.purge({ useMasterKey: true });
772
+
773
+ // Then delete the class schema itself
774
+ await schema.delete({ useMasterKey: true });
775
+
776
+ const resultData = { success: true, className, message: `Class "${className}" and all its data have been permanently deleted.` };
777
+ return resultData;
778
+ } catch (deleteError) {
779
+ throw new Error(`Failed to delete class "${className}": ${deleteError.message}`);
780
+ }
781
+ }
782
+
783
+ default:
784
+ throw new Error(`Unknown function: ${functionName}`);
785
+ }
786
+ } catch (error) {
787
+ console.error('Database operation error:', {
788
+ functionName,
789
+ args,
790
+ appId: appContext.appId,
791
+ serverURL: appContext.serverURL,
792
+ error: error.message,
793
+ stack: error.stack
794
+ });
795
+ throw new Error(`Database operation failed: ${error.message}`);
796
+ }
797
+ }
798
+
799
+ /**
800
+ * Make a request to OpenAI API
801
+ */
802
+ async function makeOpenAIRequest(userMessage, model, apiKey, appContext = null, conversationHistory = [], operationLog = [], permissions = {}) {
803
+ const fetch = (await import('node-fetch')).default;
804
+
805
+ const url = 'https://api.openai.com/v1/chat/completions';
806
+
807
+ const appInfo = appContext ?
808
+ `\n\nContext: You are currently helping with the Parse Server app "${appContext.appName}" (ID: ${appContext.appId}) at ${appContext.serverURL}.` :
809
+ '';
810
+
811
+ // Build messages array starting with system message
812
+ const messages = [
813
+ {
814
+ role: 'system',
815
+ content: `You are an AI assistant integrated into Parse Dashboard, a data management interface for Parse Server applications.
816
+
817
+ Your role is to help users with:
818
+ - Database queries and data operations using the Parse JS SDK
819
+ - Understanding Parse Server concepts and best practices
820
+ - Troubleshooting common issues
821
+ - Best practices for data modeling
822
+ - Cloud Code and server configuration guidance
823
+
824
+ You have access to database function tools that allow you to:
825
+ - Query classes/tables to retrieve objects (read-only, no confirmation needed)
826
+ - Create new objects in classes (REQUIRES USER CONFIRMATION)
827
+ - Update existing objects (REQUIRES USER CONFIRMATION)
828
+ - Delete INDIVIDUAL objects by objectId (REQUIRES USER CONFIRMATION)
829
+ - Delete ENTIRE classes/tables and all their data (REQUIRES USER CONFIRMATION)
830
+ - Get schema information for classes (read-only, no confirmation needed)
831
+ - Count objects that match certain criteria (read-only, no confirmation needed)
832
+ - Create new empty classes/tables (REQUIRES USER CONFIRMATION)
833
+
834
+ IMPORTANT: Choose the correct function based on what the user wants to delete:
835
+ - Use 'deleteObject' when deleting a specific object/record by its objectId
836
+ - Use 'deleteClass' when deleting an entire class/table (the class itself and all its data)
837
+
838
+ CRITICAL SECURITY RULE FOR WRITE OPERATIONS:
839
+ - ANY write operation (create, update, delete) MUST have explicit user confirmation through conversation
840
+ - When a user requests a write operation, explain what you will do and ask for confirmation
841
+ - Only call the write operation functions with confirmed=true after the user has explicitly agreed
842
+ - If a user says "Create a new class", treat this as confirmation to create objects in that class
843
+ - You CANNOT perform write operations without the user's knowledge and consent
844
+ - Read operations (query, schema, count) can be performed immediately without confirmation
845
+
846
+ Confirmation Pattern:
847
+ 1. User requests operation (e.g., "Create a new class called Products")
848
+ 2. You ask: "I'll create a new object in the Products class. Should I proceed?"
849
+ 3. User confirms: "Yes" / "Go ahead" / "Do it"
850
+ 4. You call the function with confirmed=true
851
+
852
+ When working with the database:
853
+ - Read operations (query, getSchema, count) can be performed immediately
854
+ - Write operations require the pattern: 1) Explain what you'll do, 2) Ask for confirmation, 3) Only then execute if confirmed
855
+ - Always use the provided database functions instead of writing code
856
+ - Class names are case-sensitive
857
+ - Use proper Parse query syntax for complex queries
858
+ - Handle objectId fields correctly
859
+ - Be mindful of data types (Date, Pointer, etc.)
860
+ - Always consider security and use appropriate query constraints
861
+ - Provide clear explanations of what database operations you're performing
862
+ - If any database function returns an error, you MUST include the full error message in your response to the user. Never hide error details or give vague responses like "there was an issue" - always show the specific error message.
863
+ - IMPORTANT: When creating objects, you MUST provide the 'objectData' parameter with actual field values. Never call createObject with only className and confirmed - always include the objectData object with the fields and values to be saved.
864
+ - IMPORTANT: When updating objects, you MUST provide the 'objectData' parameter with the fields you want to update. Include the objectData object with field names and new values.
865
+
866
+ CRITICAL RULE FOR createObject FUNCTION:
867
+ - The createObject function REQUIRES THREE parameters: className, objectData, and confirmed
868
+ - The 'objectData' parameter MUST contain the actual field values as a JSON object
869
+ - NEVER call createObject with only className and confirmed - this will fail
870
+ - Example: createObject({className: 'TestCars', objectData: {model: 'Honda Civic', year: 2023, brand: 'Honda'}, confirmed: true})
871
+ - The objectData object should contain all the fields and their values that you want to save
872
+
873
+ When responding:
874
+ - Be concise and helpful
875
+ - Provide practical examples when relevant
876
+ - Ask clarifying questions if the user's request is unclear
877
+ - Focus on Parse-specific solutions and recommendations
878
+ - If you perform database operations, explain what you did and show the results
879
+ - For write operations, always explain the impact and ask for explicit confirmation
880
+ - Format your responses using Markdown for better readability:
881
+ * Use **bold** for important information
882
+ * Use *italic* for emphasis
883
+ * Use \`code\` for field names, class names, and values
884
+ * Use numbered lists for step-by-step instructions
885
+ * Use bullet points for listing items
886
+ * Use tables when showing structured data
887
+ * Use code blocks with language specification for code examples
888
+ * Use headers (##, ###) to organize longer responses
889
+ * When listing database classes, format as a numbered list with descriptions
890
+ * Use tables for structured data comparison
891
+
892
+ You have direct access to the Parse database through function calls, so you can query actual data and provide real-time information.${appInfo}`
893
+ }
894
+ ];
895
+
896
+ // Add conversation history if it exists
897
+ if (conversationHistory && conversationHistory.length > 0) {
898
+ // Filter out any messages with null or undefined content to prevent API errors
899
+ const validHistory = conversationHistory.filter(msg =>
900
+ msg && typeof msg === 'object' && msg.role &&
901
+ (msg.content !== null && msg.content !== undefined && msg.content !== '')
902
+ );
903
+ messages.push(...validHistory);
904
+ }
905
+
906
+ // Add the current user message
907
+ messages.push({
908
+ role: 'user',
909
+ content: userMessage
910
+ });
911
+
912
+ const requestBody = {
913
+ model: model,
914
+ messages: messages,
915
+ temperature: 0.7,
916
+ max_tokens: 2000,
917
+ tools: databaseTools,
918
+ tool_choice: 'auto',
919
+ stream: false
920
+ };
921
+
922
+ const response = await fetch(url, {
923
+ method: 'POST',
924
+ headers: {
925
+ 'Content-Type': 'application/json',
926
+ 'Authorization': `Bearer ${apiKey}`
927
+ },
928
+ body: JSON.stringify(requestBody)
929
+ });
930
+
931
+ if (!response.ok) {
932
+ if (response.status === 401) {
933
+ throw new Error('Invalid API key. Please check your OpenAI API key configuration.');
934
+ } else if (response.status === 429) {
935
+ throw new Error('Rate limit exceeded. Please try again in a moment.');
936
+ } else if (response.status === 403) {
937
+ throw new Error('Access forbidden. Please check your API key permissions.');
938
+ } else if (response.status >= 500) {
939
+ throw new Error('OpenAI service is temporarily unavailable. Please try again later.');
940
+ }
941
+
942
+ const errorData = await response.json().catch(() => ({}));
943
+ const errorMessage = (errorData && typeof errorData === 'object' && 'error' in errorData && errorData.error && typeof errorData.error === 'object' && 'message' in errorData.error)
944
+ ? errorData.error.message
945
+ : `HTTP ${response.status}: ${response.statusText}`;
946
+ throw new Error(`OpenAI API error: ${errorMessage}`);
947
+ }
948
+
949
+ const data = await response.json();
950
+
951
+ if (!data || typeof data !== 'object' || !('choices' in data) || !Array.isArray(data.choices) || data.choices.length === 0) {
952
+ throw new Error('No response received from OpenAI API');
953
+ }
954
+
955
+ const choice = data.choices[0];
956
+ const responseMessage = choice.message;
957
+
958
+ // Handle function calls
959
+ if (responseMessage.tool_calls && responseMessage.tool_calls.length > 0) {
960
+ const toolCalls = responseMessage.tool_calls;
961
+ const toolResponses = [];
962
+
963
+ for (const toolCall of toolCalls) {
964
+ if (toolCall.type === 'function') {
965
+ try {
966
+ const functionName = toolCall.function.name;
967
+ const functionArgs = JSON.parse(toolCall.function.arguments);
968
+
969
+ console.log('Executing database function:', {
970
+ functionName,
971
+ args: functionArgs,
972
+ appId: appContext.appId,
973
+ serverURL: appContext.serverURL,
974
+ timestamp: new Date().toISOString()
975
+ });
976
+
977
+ // Execute the database function
978
+ const result = await executeDatabaseFunction(functionName, functionArgs, appContext, operationLog, permissions);
979
+
980
+ toolResponses.push({
981
+ tool_call_id: toolCall.id,
982
+ role: 'tool',
983
+ content: result ? JSON.stringify(result) : JSON.stringify({ success: true })
984
+ });
985
+ } catch (error) {
986
+ toolResponses.push({
987
+ tool_call_id: toolCall.id,
988
+ role: 'tool',
989
+ content: JSON.stringify({ error: error.message || 'Unknown error occurred' })
990
+ });
991
+ }
992
+ }
993
+ }
994
+
995
+ // Make a second request with the tool responses
996
+ const followUpMessages = [
997
+ ...messages,
998
+ responseMessage,
999
+ ...toolResponses
1000
+ ];
1001
+
1002
+ const followUpRequestBody = {
1003
+ model: model,
1004
+ messages: followUpMessages,
1005
+ temperature: 0.7,
1006
+ max_tokens: 2000,
1007
+ tools: databaseTools,
1008
+ tool_choice: 'auto',
1009
+ stream: false
1010
+ };
1011
+
1012
+ const followUpResponse = await fetch(url, {
1013
+ method: 'POST',
1014
+ headers: {
1015
+ 'Content-Type': 'application/json',
1016
+ 'Authorization': `Bearer ${apiKey}`
1017
+ },
1018
+ body: JSON.stringify(followUpRequestBody)
1019
+ });
1020
+
1021
+ if (!followUpResponse.ok) {
1022
+ throw new Error(`Follow-up request failed: ${followUpResponse.statusText}`);
1023
+ }
1024
+
1025
+ const followUpData = await followUpResponse.json();
1026
+
1027
+ if (!followUpData || typeof followUpData !== 'object' || !('choices' in followUpData) || !Array.isArray(followUpData.choices) || followUpData.choices.length === 0) {
1028
+ throw new Error('No follow-up response received from OpenAI API');
1029
+ }
1030
+
1031
+ const followUpContent = followUpData.choices[0].message.content;
1032
+ if (!followUpContent) {
1033
+ console.warn('OpenAI returned null content in follow-up response, using fallback message');
1034
+ }
1035
+ return followUpContent || 'Done.';
1036
+ }
1037
+
1038
+ const content = responseMessage.content;
1039
+ if (!content) {
1040
+ console.warn('OpenAI returned null content in initial response, using fallback message');
1041
+ }
1042
+ return content || 'Done.';
1043
+ }
1044
+
184
1045
  // Serve the app icons. Uses the optional `iconsFolder` parameter as
185
1046
  // directory name, that was setup in the config file.
186
1047
  // We are explicitly not using `__dirpath` here because one may be
@@ -251,6 +1112,7 @@ module.exports = function(config, options) {
251
1112
  <base href="${mountPath}"/>
252
1113
  <script>
253
1114
  PARSE_DASHBOARD_PATH = "${mountPath}";
1115
+ PARSE_DASHBOARD_ENABLE_RESOURCE_CACHE = ${config.enableResourceCache ? 'true' : 'false'};
254
1116
  </script>
255
1117
  <title>Parse Dashboard</title>
256
1118
  </head>