@mongoosejs/studio 0.5.5 → 0.5.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,14 +1,14 @@
1
1
  # Mongoose Studio
2
2
 
3
- A sleek, powerful MongoDB UI with built-in dashboarding and auth, seamlessly integrated with your Express, Vercel, or Netlify app.
3
+ An AI-Powered Data Workspace for MongoDB: Turn your MongoDB data into dashboards, maps, and interactive workflows - powered by your Mongoose models.
4
4
 
5
5
  ![NPM Version](https://img.shields.io/npm/v/@mongoosejs/studio)
6
6
 
7
7
  ## Getting Started
8
8
 
9
9
  Mongoose Studio is meant to run as a [sidecar](https://learn.microsoft.com/en-us/azure/architecture/patterns/sidecar) to your Node.js application, using the same Mongoose connection config.
10
- If your app runs on `acme.app`, Studio will be on `acme.app/studio` or whichever path you prefer.
11
- For local dev, if your app runs on `localhost:3000`, Studio will be on `localhost:3000/studio`.
10
+ If your app runs on `acme.app`, Mongoose Studio will be on `acme.app/studio` or whichever path you prefer.
11
+ For local dev, if your app runs on `localhost:3000`, Mongoose Studio will be on `localhost:3000/studio`.
12
12
 
13
13
  By default, Mongoose Studio does **not** provide any authentication or authorization.
14
14
  You can use Mongoose Studio for free for local development, but we recommend [Mongoose Studio Pro](https://studio.mongoosejs.io/#pricing) for when you want to go into production.
@@ -85,6 +85,38 @@ const handler = studio(
85
85
  export default handler;
86
86
  ```
87
87
 
88
+ ### Nest.js
89
+
90
+ Add `MongooseStudioModule` to your app module as follows.
91
+ Use `getConnectionToken()` so Mongoose Studio uses the same connection that `MongooseModule.forRoot()` creates.
92
+
93
+ ```typescript
94
+ import { Module } from '@nestjs/common';
95
+ import { MongooseModule, getConnectionToken } from '@nestjs/mongoose';
96
+ import { MongooseStudioModule } from '@mongoosejs/studio/nest';
97
+
98
+ @Module({
99
+ imports: [
100
+ MongooseModule.forRoot(process.env.MONGODB_URI),
101
+ MongooseStudioModule.forRoot({
102
+ connectionToken: getConnectionToken(),
103
+ apiKey: process.env.MONGOOSE_STUDIO_API_KEY // optional
104
+ })
105
+ ]
106
+ })
107
+ export class AppModule {}
108
+ ```
109
+
110
+ With this setup, Mongoose Studio is available at `/studio`.
111
+ If you mount Mongoose Studio on a different path, update the `path` option as follows.
112
+
113
+ ```typescript
114
+ MongooseStudioModule.forRoot({
115
+ path: '/__studio', // Serve on `/__studio` rather than `/studio`
116
+ connectionToken: getConnectionToken()
117
+ })
118
+ ```
119
+
88
120
  ### Netlify
89
121
 
90
122
  [Here is a full example of how to add Mongoose Studio to a Netlify repo](https://github.com/mongoosejs/studio.mongoosejs.io/commit/8b02ea367c8a1b7b4bcab290708f57d58f08210b).
@@ -2,6 +2,7 @@
2
2
 
3
3
  const Archetype = require('archetype');
4
4
  const authorize = require('../../authorize');
5
+ const mongoose = require('mongoose');
5
6
 
6
7
  const CreateDashboardParams = new Archetype({
7
8
  title: {
@@ -12,18 +13,27 @@ const CreateDashboardParams = new Archetype({
12
13
  $type: 'string',
13
14
  $required: true
14
15
  },
16
+ initiatedById: {
17
+ $type: mongoose.Types.ObjectId,
18
+ $required: false
19
+ },
15
20
  roles: {
16
21
  $type: ['string']
17
22
  }
18
23
  }).compile('CreateDashboardParams');
19
24
 
20
25
  module.exports = ({ studioConnection }) => async function createDashboard(params) {
21
- const { title, code, roles } = new CreateDashboardParams(params);
26
+ const { title, code, initiatedById, roles } = new CreateDashboardParams(params);
22
27
  const Dashboard = studioConnection.model('__Studio_Dashboard');
23
28
 
24
29
  await authorize('Dashboard.createDashboard', roles);
25
30
 
26
- const dashboard = await Dashboard.create({ title, code });
31
+ const dashboard = await Dashboard.create({
32
+ title,
33
+ code,
34
+ createdById: initiatedById,
35
+ createdBy: params.initiatedBy
36
+ });
27
37
 
28
38
  return { dashboard };
29
39
  };
@@ -51,6 +51,7 @@ module.exports = ({ db, studioConnection, options }) => async function getDashbo
51
51
  return {};
52
52
  }
53
53
  return completeDashboardEvaluate(
54
+ Dashboard,
54
55
  DashboardResult,
55
56
  dashboardResult._id,
56
57
  null,
@@ -73,6 +74,7 @@ module.exports = ({ db, studioConnection, options }) => async function getDashbo
73
74
  return {};
74
75
  }
75
76
  return completeDashboardEvaluate(
77
+ Dashboard,
76
78
  DashboardResult,
77
79
  dashboardResult._id,
78
80
  result,
@@ -97,13 +99,15 @@ module.exports = ({ db, studioConnection, options }) => async function getDashbo
97
99
  }
98
100
  };
99
101
 
100
- async function completeDashboardEvaluate(DashboardResult, dashboardResultId, result, error, status) {
102
+ async function completeDashboardEvaluate(Dashboard, DashboardResult, dashboardResultId, result, error, status) {
101
103
  const dashboardResult = await DashboardResult.findById(dashboardResultId).orFail();
102
- dashboardResult.finishedEvaluatingAt = new Date();
104
+ const finishedEvaluatingAt = new Date();
105
+ dashboardResult.finishedEvaluatingAt = finishedEvaluatingAt;
103
106
  dashboardResult.result = result;
104
107
  dashboardResult.error = error;
105
108
  dashboardResult.status = status;
106
109
  await dashboardResult.save();
110
+ await Dashboard.updateOne({ _id: dashboardResult.dashboardId }, { $set: { lastEvaluatedAt: finishedEvaluatingAt } });
107
111
  return { dashboardResult };
108
112
  }
109
113
 
@@ -15,7 +15,9 @@ module.exports = ({ studioConnection }) => async function getDashboards(params)
15
15
 
16
16
  await authorize('Dashboard.getDashboards', roles);
17
17
 
18
- const dashboards = await Dashboard.find();
18
+ const dashboards = await Dashboard
19
+ .find()
20
+ .sort({ isPinned: -1, createdAt: -1, _id: -1 });
19
21
 
20
22
  return { dashboards };
21
23
  };
@@ -9,8 +9,7 @@ const UpdateDashboardParams = new Archetype({
9
9
  $required: true
10
10
  },
11
11
  code: {
12
- $type: 'string',
13
- $required: true
12
+ $type: 'string'
14
13
  },
15
14
  title: {
16
15
  $type: 'string'
@@ -18,19 +17,26 @@ const UpdateDashboardParams = new Archetype({
18
17
  description: {
19
18
  $type: 'string'
20
19
  },
20
+ isPinned: {
21
+ $type: 'boolean'
22
+ },
21
23
  roles: {
22
24
  $type: ['string']
23
25
  }
24
26
  }).compile('UpdateDashboardParams');
25
27
 
26
28
  module.exports = ({ studioConnection }) => async function updateDashboard(params) {
27
- const { dashboardId, code, title, description, roles, evaluate } = new UpdateDashboardParams(params);
29
+ const { dashboardId, code, title, description, isPinned, roles, evaluate } = new UpdateDashboardParams(params);
28
30
 
29
31
  const Dashboard = studioConnection.models['__Studio_Dashboard'];
30
32
 
31
33
  await authorize('Dashboard.updateDashboard', roles);
32
34
 
33
- const updateObj = { code };
35
+ const updateObj = {};
36
+
37
+ if (code != null) {
38
+ updateObj.code = code;
39
+ }
34
40
 
35
41
  if (title != null) {
36
42
  updateObj.title = title;
@@ -40,8 +46,15 @@ module.exports = ({ studioConnection }) => async function updateDashboard(params
40
46
  updateObj.description = description;
41
47
  }
42
48
 
43
- const doc = await Dashboard.
44
- findByIdAndUpdate(dashboardId, updateObj, { sanitizeFilter: true, returnDocument: 'after', overwriteImmutable: true });
49
+ if (isPinned != null) {
50
+ updateObj.isPinned = isPinned;
51
+ }
52
+
53
+ const doc = await Dashboard.findByIdAndUpdate(
54
+ dashboardId,
55
+ updateObj,
56
+ { sanitizeFilter: true, returnDocument: 'after' }
57
+ );
45
58
 
46
59
  return { doc };
47
60
  };
@@ -13,6 +13,37 @@ const dashboardSchema = new mongoose.Schema({
13
13
  },
14
14
  description: {
15
15
  type: String
16
+ },
17
+ isPinned: {
18
+ type: Boolean
19
+ },
20
+ createdById: {
21
+ type: mongoose.Schema.Types.ObjectId
22
+ },
23
+ createdBy: {
24
+ name: String,
25
+ email: String
26
+ },
27
+ lastEvaluatedAt: {
28
+ type: Date
29
+ }
30
+ }, { timestamps: true });
31
+
32
+ dashboardSchema.post(['find', 'findOne'], async function(docs) {
33
+ const dashboards = Array.isArray(docs) ? docs : [docs];
34
+ for (const dashboard of dashboards) {
35
+ if (dashboard != null && dashboard.createdAt == null && dashboard._id?.getTimestamp) {
36
+ const createdAt = dashboard._id.getTimestamp();
37
+ if (!createdAt.valueOf()) {
38
+ continue;
39
+ }
40
+ dashboard.set('createdAt', createdAt, { overwriteImmutable: true });
41
+ await this.model.db.model('__Studio_Dashboard').updateOne(
42
+ { _id: dashboard._id },
43
+ { createdAt },
44
+ { overwriteImmutable: true }
45
+ );
46
+ }
16
47
  }
17
48
  });
18
49
 
@@ -63,6 +63,7 @@ module.exports = function netlify(conn, options) {
63
63
  params.roles = roles;
64
64
  params.userId = user._id;
65
65
  params.initiatedById = user._id;
66
+ params.initiatedBy = user;
66
67
  }
67
68
 
68
69
  if (typeof actionName !== 'string') {
package/backend/next.js CHANGED
@@ -64,6 +64,7 @@ module.exports = function next(conn, options) {
64
64
  params.roles = roles;
65
65
  params.userId = user._id;
66
66
  params.initiatedById = user._id;
67
+ params.initiatedBy = user;
67
68
  }
68
69
 
69
70
  if (typeof actionName !== 'string') {
package/express.js CHANGED
@@ -9,6 +9,8 @@ const normalizeBindIPOption = require('./backend/helpers/normalizeBindIPOption')
9
9
  const { toRoute, objectRouter } = require('extrovert');
10
10
  const { defaultMothershipURL } = require('./constants');
11
11
 
12
+ const jsonParser = express.json();
13
+
12
14
  module.exports = async function mongooseStudioExpressApp(apiUrl, conn, options) {
13
15
  const router = express.Router();
14
16
  options = options ? { changeStream: true, ...options } : { changeStream: true };
@@ -87,6 +89,7 @@ module.exports = async function mongooseStudioExpressApp(apiUrl, conn, options)
87
89
  req._internals.initiatedById = user._id;
88
90
  req._internals.roles = roles;
89
91
  req._internals.$workspaceId = workspace._id;
92
+ req._internals.initiatedBy = user;
90
93
 
91
94
  next();
92
95
  })
@@ -94,7 +97,12 @@ module.exports = async function mongooseStudioExpressApp(apiUrl, conn, options)
94
97
  return res.status(500).json({ message: err.message });
95
98
  });
96
99
  },
97
- express.json(),
100
+ function parseJson(req, res, next) {
101
+ if (req.body !== undefined || req.readable === false) {
102
+ return next();
103
+ }
104
+ return jsonParser(req, res, next);
105
+ },
98
106
  objectRouter(backend, toRoute)
99
107
  );
100
108
 
@@ -3266,11 +3266,73 @@ module.exports = app => app.component('dashboards', {
3266
3266
  data: () => ({
3267
3267
  status: 'loading',
3268
3268
  dashboards: [],
3269
+ searchText: '',
3269
3270
  showCreateDashboardModal: false,
3270
3271
  showDeleteDashboardModal: null,
3271
3272
  openMenuId: null
3272
3273
  }),
3274
+ computed: {
3275
+ filteredDashboards() {
3276
+ const searchWords = getSearchWords(this.searchText);
3277
+ let dashboards = this.dashboards;
3278
+ if (searchWords.length === 0) {
3279
+ return sortPinnedFirst(dashboards);
3280
+ }
3281
+ const searchPatterns = searchWords.map(word => new RegExp(escapeRegExp(word), 'i'));
3282
+
3283
+ dashboards = dashboards.filter(dashboard => {
3284
+ const searchableText = `${dashboard.title || ''} ${dashboard.description || ''}`;
3285
+ return searchPatterns.every(pattern => pattern.test(searchableText));
3286
+ });
3287
+
3288
+ return sortPinnedFirst(dashboards);
3289
+ }
3290
+ },
3273
3291
  methods: {
3292
+ highlightTitle(value) {
3293
+ return this.highlightSearchText(value, 'underline decoration-primary underline-offset-2');
3294
+ },
3295
+ highlightDescription(value) {
3296
+ return this.highlightSearchText(value, 'font-semibold text-content-secondary');
3297
+ },
3298
+ highlightSearchText(value, className) {
3299
+ const text = value || '';
3300
+ const searchWords = getSearchWords(this.searchText);
3301
+ if (searchWords.length === 0) {
3302
+ return escapeHtml(text);
3303
+ }
3304
+ const searchPattern = new RegExp(searchWords.map(escapeRegExp).join('|'), 'ig');
3305
+
3306
+ let html = '';
3307
+ let lastIndex = 0;
3308
+ let match = searchPattern.exec(text);
3309
+ while (match != null) {
3310
+ html += escapeHtml(text.slice(lastIndex, match.index));
3311
+ html += `<span class="${className}">${escapeHtml(match[0])}</span>`;
3312
+ lastIndex = match.index + match[0].length;
3313
+ match = searchPattern.exec(text);
3314
+ }
3315
+ html += escapeHtml(text.slice(lastIndex));
3316
+ return html;
3317
+ },
3318
+ formatDate(value) {
3319
+ if (!value) {
3320
+ return '-';
3321
+ }
3322
+ const date = new Date(value);
3323
+ if (Number.isNaN(date.getTime())) {
3324
+ return '-';
3325
+ }
3326
+ return new Intl.DateTimeFormat(undefined, {
3327
+ dateStyle: 'medium',
3328
+ timeStyle: 'short'
3329
+ }).format(date);
3330
+ },
3331
+ formatUser(dashboard) {
3332
+ return dashboard.createdBy?.name ||
3333
+ dashboard.createdBy?.email ||
3334
+ (dashboard.createdById ? dashboard.createdById.toString() : '-');
3335
+ },
3274
3336
  toggleMenu(id) {
3275
3337
  this.openMenuId = this.openMenuId === id ? null : id;
3276
3338
  },
@@ -3290,8 +3352,19 @@ module.exports = app => app.component('dashboards', {
3290
3352
  this.$toast.success('Dashboard deleted!');
3291
3353
  },
3292
3354
  insertNewDashboard(dashboard) {
3293
- this.dashboards.push(dashboard);
3355
+ this.dashboards.unshift(dashboard);
3294
3356
  this.showCreateDashboardModal = false;
3357
+ },
3358
+ async togglePin(dashboard) {
3359
+ if (!dashboard) {
3360
+ return;
3361
+ }
3362
+
3363
+ const { doc } = await api.Dashboard.updateDashboard({
3364
+ dashboardId: dashboard._id,
3365
+ isPinned: !dashboard.isPinned
3366
+ });
3367
+ dashboard.isPinned = doc.isPinned;
3295
3368
  }
3296
3369
  },
3297
3370
  directives: {
@@ -3316,6 +3389,28 @@ module.exports = app => app.component('dashboards', {
3316
3389
  }
3317
3390
  });
3318
3391
 
3392
+ function escapeHtml(value) {
3393
+ return value.toString().
3394
+ replace(/&/g, '&amp;').
3395
+ replace(/</g, '&lt;').
3396
+ replace(/>/g, '&gt;').
3397
+ replace(/"/g, '&quot;').
3398
+ replace(/'/g, '&#39;');
3399
+ }
3400
+
3401
+ function escapeRegExp(value) {
3402
+ return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
3403
+ }
3404
+
3405
+ function getSearchWords(searchText) {
3406
+ return Array.from(new Set(searchText.trim().split(/\s+/).filter(Boolean))).
3407
+ sort((a, b) => b.length - a.length);
3408
+ }
3409
+
3410
+ function sortPinnedFirst(dashboards) {
3411
+ return dashboards.slice().sort((a, b) => Number(Boolean(b.isPinned)) - Number(Boolean(a.isPinned)));
3412
+ }
3413
+
3319
3414
 
3320
3415
  /***/ },
3321
3416
 
@@ -12604,7 +12699,7 @@ __webpack_require__.r(__webpack_exports__);
12604
12699
  /* harmony export */ });
12605
12700
  /* harmony import */ var _vue_shared__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! @vue/shared */ "./node_modules/@vue/shared/dist/shared.esm-bundler.js");
12606
12701
  /**
12607
- * @vue/reactivity v3.5.35
12702
+ * @vue/reactivity v3.5.38
12608
12703
  * (c) 2018-present Yuxi (Evan) You and Vue contributors
12609
12704
  * @license MIT
12610
12705
  **/
@@ -14501,8 +14596,9 @@ function watch(source, cb, options = _vue_shared__WEBPACK_IMPORTED_MODULE_0__.EM
14501
14596
  if (once && cb) {
14502
14597
  const _cb = cb;
14503
14598
  cb = (...args) => {
14504
- _cb(...args);
14599
+ const res = _cb(...args);
14505
14600
  watchHandle();
14601
+ return res;
14506
14602
  };
14507
14603
  }
14508
14604
  let oldValue = isMultiSource ? new Array(source.length).fill(INITIAL_WATCHER_VALUE) : INITIAL_WATCHER_VALUE;
@@ -14512,7 +14608,7 @@ function watch(source, cb, options = _vue_shared__WEBPACK_IMPORTED_MODULE_0__.EM
14512
14608
  }
14513
14609
  if (cb) {
14514
14610
  const newValue = effect.run();
14515
- if (deep || forceTrigger || (isMultiSource ? newValue.some((v, i) => (0,_vue_shared__WEBPACK_IMPORTED_MODULE_0__.hasChanged)(v, oldValue[i])) : (0,_vue_shared__WEBPACK_IMPORTED_MODULE_0__.hasChanged)(newValue, oldValue))) {
14611
+ if (immediateFirstRun || deep || forceTrigger || (isMultiSource ? newValue.some((v, i) => (0,_vue_shared__WEBPACK_IMPORTED_MODULE_0__.hasChanged)(v, oldValue[i])) : (0,_vue_shared__WEBPACK_IMPORTED_MODULE_0__.hasChanged)(newValue, oldValue))) {
14516
14612
  if (cleanup) {
14517
14613
  cleanup();
14518
14614
  }
@@ -14772,7 +14868,7 @@ __webpack_require__.r(__webpack_exports__);
14772
14868
  /* harmony import */ var _vue_reactivity__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! @vue/reactivity */ "./node_modules/@vue/reactivity/dist/reactivity.esm-bundler.js");
14773
14869
  /* harmony import */ var _vue_shared__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! @vue/shared */ "./node_modules/@vue/shared/dist/shared.esm-bundler.js");
14774
14870
  /**
14775
- * @vue/runtime-core v3.5.35
14871
+ * @vue/runtime-core v3.5.38
14776
14872
  * (c) 2018-present Yuxi (Evan) You and Vue contributors
14777
14873
  * @license MIT
14778
14874
  **/
@@ -17504,13 +17600,21 @@ function defineAsyncComponent(source) {
17504
17600
  const loaded = (0,_vue_reactivity__WEBPACK_IMPORTED_MODULE_0__.ref)(false);
17505
17601
  const error = (0,_vue_reactivity__WEBPACK_IMPORTED_MODULE_0__.ref)();
17506
17602
  const delayed = (0,_vue_reactivity__WEBPACK_IMPORTED_MODULE_0__.ref)(!!delay);
17603
+ let timeoutTimer;
17604
+ let delayTimer;
17605
+ onUnmounted(() => {
17606
+ if (timeoutTimer != null) clearTimeout(timeoutTimer);
17607
+ if (delayTimer != null) clearTimeout(delayTimer);
17608
+ });
17507
17609
  if (delay) {
17508
- setTimeout(() => {
17610
+ delayTimer = setTimeout(() => {
17611
+ if (instance.isUnmounted) return;
17509
17612
  delayed.value = false;
17510
17613
  }, delay);
17511
17614
  }
17512
17615
  if (timeout != null) {
17513
- setTimeout(() => {
17616
+ timeoutTimer = setTimeout(() => {
17617
+ if (instance.isUnmounted) return;
17514
17618
  if (!loaded.value && !error.value) {
17515
17619
  const err = new Error(
17516
17620
  `Async component timed out after ${timeout}ms.`
@@ -17521,11 +17625,16 @@ function defineAsyncComponent(source) {
17521
17625
  }, timeout);
17522
17626
  }
17523
17627
  load().then(() => {
17628
+ if (instance.isUnmounted) return;
17524
17629
  loaded.value = true;
17525
17630
  if (instance.parent && isKeepAlive(instance.parent.vnode)) {
17526
17631
  instance.parent.update();
17527
17632
  }
17528
17633
  }).catch((err) => {
17634
+ if (instance.isUnmounted) {
17635
+ pendingRequest = null;
17636
+ return;
17637
+ }
17529
17638
  onError(err);
17530
17639
  error.value = err;
17531
17640
  });
@@ -19107,13 +19216,20 @@ function useModel(props, name, options = _vue_shared__WEBPACK_IMPORTED_MODULE_1_
19107
19216
  return;
19108
19217
  }
19109
19218
  const rawProps = i.vnode.props;
19110
- if (!(rawProps && // check if parent has passed v-model
19111
- (name in rawProps || camelizedName in rawProps || hyphenatedName in rawProps) && (`onUpdate:${name}` in rawProps || `onUpdate:${camelizedName}` in rawProps || `onUpdate:${hyphenatedName}` in rawProps))) {
19219
+ const hasVModel = !!(rawProps && // check if parent has passed v-model
19220
+ (name in rawProps || camelizedName in rawProps || hyphenatedName in rawProps) && (`onUpdate:${name}` in rawProps || `onUpdate:${camelizedName}` in rawProps || `onUpdate:${hyphenatedName}` in rawProps));
19221
+ if (!hasVModel) {
19112
19222
  localValue = value;
19113
19223
  trigger();
19114
19224
  }
19115
19225
  i.emit(`update:${name}`, emittedValue);
19116
- if ((0,_vue_shared__WEBPACK_IMPORTED_MODULE_1__.hasChanged)(value, emittedValue) && (0,_vue_shared__WEBPACK_IMPORTED_MODULE_1__.hasChanged)(value, prevSetValue) && !(0,_vue_shared__WEBPACK_IMPORTED_MODULE_1__.hasChanged)(emittedValue, prevEmittedValue)) {
19226
+ if ((0,_vue_shared__WEBPACK_IMPORTED_MODULE_1__.hasChanged)(value, prevSetValue) && ((0,_vue_shared__WEBPACK_IMPORTED_MODULE_1__.hasChanged)(value, emittedValue) && !(0,_vue_shared__WEBPACK_IMPORTED_MODULE_1__.hasChanged)(emittedValue, prevEmittedValue) || // #13524: browsers differ in when they flush microtasks between
19227
+ // event listeners. If a v-model listener emits an intermediate value
19228
+ // and a following listener restores the model to its previous prop
19229
+ // value before parent updates are flushed, the parent render can be
19230
+ // deduped as having no prop change. Force a local update so DOM state
19231
+ // such as an input's value is synchronized back to the current model.
19232
+ hasVModel && prevSetValue !== _vue_shared__WEBPACK_IMPORTED_MODULE_1__.EMPTY_OBJ && !(0,_vue_shared__WEBPACK_IMPORTED_MODULE_1__.hasChanged)(emittedValue, localValue))) {
19117
19233
  trigger();
19118
19234
  }
19119
19235
  prevSetValue = value;
@@ -23375,7 +23491,7 @@ function isMemoSame(cached, memo) {
23375
23491
  return true;
23376
23492
  }
23377
23493
 
23378
- const version = "3.5.35";
23494
+ const version = "3.5.38";
23379
23495
  const warn = true ? warn$1 : 0;
23380
23496
  const ErrorTypeStrings = ErrorTypeStrings$1 ;
23381
23497
  const devtools = true ? devtools$1 : 0;
@@ -23586,7 +23702,7 @@ __webpack_require__.r(__webpack_exports__);
23586
23702
  /* harmony import */ var _vue_runtime_core__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! @vue/runtime-core */ "./node_modules/@vue/reactivity/dist/reactivity.esm-bundler.js");
23587
23703
  /* harmony import */ var _vue_runtime_core__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! @vue/shared */ "./node_modules/@vue/shared/dist/shared.esm-bundler.js");
23588
23704
  /**
23589
- * @vue/runtime-dom v3.5.35
23705
+ * @vue/runtime-dom v3.5.38
23590
23706
  * (c) 2018-present Yuxi (Evan) You and Vue contributors
23591
23707
  * @license MIT
23592
23708
  **/
@@ -25048,7 +25164,8 @@ const TransitionGroupImpl = /* @__PURE__ */ decorate({
25048
25164
  if (children) {
25049
25165
  for (let i = 0; i < children.length; i++) {
25050
25166
  const child = children[i];
25051
- if (child.el && child.el instanceof Element) {
25167
+ if (child.el && child.el instanceof Element && // Hidden v-show nodes have no previous layout box to animate from.
25168
+ !child.el[vShowHidden]) {
25052
25169
  prevChildren.push(child);
25053
25170
  (0,_vue_runtime_core__WEBPACK_IMPORTED_MODULE_0__.setTransitionHooks)(
25054
25171
  child,
@@ -25684,7 +25801,7 @@ __webpack_require__.r(__webpack_exports__);
25684
25801
  /* harmony export */ toTypeString: () => (/* binding */ toTypeString)
25685
25802
  /* harmony export */ });
25686
25803
  /**
25687
- * @vue/shared v3.5.35
25804
+ * @vue/shared v3.5.38
25688
25805
  * (c) 2018-present Yuxi (Evan) You and Vue contributors
25689
25806
  * @license MIT
25690
25807
  **/
@@ -50528,6 +50645,128 @@ module.exports = {
50528
50645
 
50529
50646
  "use strict";
50530
50647
 
50648
+ Object.defineProperty(exports, "__esModule", ({ value: true }));
50649
+ exports.setUint32LE = exports.readUint8 = exports.readInt32LE = exports.UUID = exports.Timestamp = exports.serialize = exports.ObjectId = exports.NumberUtils = exports.MinKey = exports.MaxKey = exports.Long = exports.Int32 = exports.EJSON = exports.Double = exports.deserialize = exports.Decimal128 = exports.DBRef = exports.Code = exports.calculateObjectSize = exports.ByteUtils = exports.BSONType = exports.BSONSymbol = exports.BSONRegExp = exports.BSONError = exports.BSON = exports.Binary = void 0;
50650
+ exports.parseToElementsToArray = parseToElementsToArray;
50651
+ exports.pluckBSONSerializeOptions = pluckBSONSerializeOptions;
50652
+ exports.resolveBSONOptions = resolveBSONOptions;
50653
+ exports.parseUtf8ValidationOption = parseUtf8ValidationOption;
50654
+ /* eslint-disable no-restricted-imports */
50655
+ const bson_1 = __webpack_require__(/*! bson */ "./node_modules/bson/lib/bson.mjs");
50656
+ var bson_2 = __webpack_require__(/*! bson */ "./node_modules/bson/lib/bson.mjs");
50657
+ Object.defineProperty(exports, "Binary", ({ enumerable: true, get: function () { return bson_2.Binary; } }));
50658
+ Object.defineProperty(exports, "BSON", ({ enumerable: true, get: function () { return bson_2.BSON; } }));
50659
+ Object.defineProperty(exports, "BSONError", ({ enumerable: true, get: function () { return bson_2.BSONError; } }));
50660
+ Object.defineProperty(exports, "BSONRegExp", ({ enumerable: true, get: function () { return bson_2.BSONRegExp; } }));
50661
+ Object.defineProperty(exports, "BSONSymbol", ({ enumerable: true, get: function () { return bson_2.BSONSymbol; } }));
50662
+ Object.defineProperty(exports, "BSONType", ({ enumerable: true, get: function () { return bson_2.BSONType; } }));
50663
+ Object.defineProperty(exports, "ByteUtils", ({ enumerable: true, get: function () { return bson_2.ByteUtils; } }));
50664
+ Object.defineProperty(exports, "calculateObjectSize", ({ enumerable: true, get: function () { return bson_2.calculateObjectSize; } }));
50665
+ Object.defineProperty(exports, "Code", ({ enumerable: true, get: function () { return bson_2.Code; } }));
50666
+ Object.defineProperty(exports, "DBRef", ({ enumerable: true, get: function () { return bson_2.DBRef; } }));
50667
+ Object.defineProperty(exports, "Decimal128", ({ enumerable: true, get: function () { return bson_2.Decimal128; } }));
50668
+ Object.defineProperty(exports, "deserialize", ({ enumerable: true, get: function () { return bson_2.deserialize; } }));
50669
+ Object.defineProperty(exports, "Double", ({ enumerable: true, get: function () { return bson_2.Double; } }));
50670
+ Object.defineProperty(exports, "EJSON", ({ enumerable: true, get: function () { return bson_2.EJSON; } }));
50671
+ Object.defineProperty(exports, "Int32", ({ enumerable: true, get: function () { return bson_2.Int32; } }));
50672
+ Object.defineProperty(exports, "Long", ({ enumerable: true, get: function () { return bson_2.Long; } }));
50673
+ Object.defineProperty(exports, "MaxKey", ({ enumerable: true, get: function () { return bson_2.MaxKey; } }));
50674
+ Object.defineProperty(exports, "MinKey", ({ enumerable: true, get: function () { return bson_2.MinKey; } }));
50675
+ Object.defineProperty(exports, "NumberUtils", ({ enumerable: true, get: function () { return bson_2.NumberUtils; } }));
50676
+ Object.defineProperty(exports, "ObjectId", ({ enumerable: true, get: function () { return bson_2.ObjectId; } }));
50677
+ Object.defineProperty(exports, "serialize", ({ enumerable: true, get: function () { return bson_2.serialize; } }));
50678
+ Object.defineProperty(exports, "Timestamp", ({ enumerable: true, get: function () { return bson_2.Timestamp; } }));
50679
+ Object.defineProperty(exports, "UUID", ({ enumerable: true, get: function () { return bson_2.UUID; } }));
50680
+ function parseToElementsToArray(bytes, offset) {
50681
+ const res = bson_1.BSON.onDemand.parseToElements(bytes, offset);
50682
+ return Array.isArray(res) ? res : [...res];
50683
+ }
50684
+ // validates buffer inputs, used for read operations
50685
+ const validateBufferInputs = (buffer, offset, length) => {
50686
+ if (offset < 0 || offset + length > buffer.length) {
50687
+ throw new RangeError(`Attempt to access memory outside buffer bounds: buffer length: ${buffer.length}, offset: ${offset}, length: ${length}`);
50688
+ }
50689
+ };
50690
+ // readInt32LE, reads a 32-bit integer from buffer at given offset
50691
+ // throws if offset is out of bounds
50692
+ const readInt32LE = (buffer, offset) => {
50693
+ validateBufferInputs(buffer, offset, 4);
50694
+ return bson_1.NumberUtils.getInt32LE(buffer, offset);
50695
+ };
50696
+ exports.readInt32LE = readInt32LE;
50697
+ // readUint8, reads a single unsigned byte from buffer at given offset
50698
+ const readUint8 = (buffer, offset) => {
50699
+ validateBufferInputs(buffer, offset, 1);
50700
+ return buffer[offset];
50701
+ };
50702
+ exports.readUint8 = readUint8;
50703
+ const setUint32LE = (destination, offset, value) => {
50704
+ destination[offset] = value;
50705
+ value >>>= 8;
50706
+ destination[offset + 1] = value;
50707
+ value >>>= 8;
50708
+ destination[offset + 2] = value;
50709
+ value >>>= 8;
50710
+ destination[offset + 3] = value;
50711
+ return 4;
50712
+ };
50713
+ exports.setUint32LE = setUint32LE;
50714
+ function pluckBSONSerializeOptions(options) {
50715
+ const { fieldsAsRaw, useBigInt64, promoteValues, promoteBuffers, promoteLongs, serializeFunctions, ignoreUndefined, bsonRegExp, raw, enableUtf8Validation } = options;
50716
+ return {
50717
+ fieldsAsRaw,
50718
+ useBigInt64,
50719
+ promoteValues,
50720
+ promoteBuffers,
50721
+ promoteLongs,
50722
+ serializeFunctions,
50723
+ ignoreUndefined,
50724
+ bsonRegExp,
50725
+ raw,
50726
+ enableUtf8Validation
50727
+ };
50728
+ }
50729
+ /**
50730
+ * Merge the given BSONSerializeOptions, preferring options over the parent's options, and
50731
+ * substituting defaults for values not set.
50732
+ *
50733
+ * @internal
50734
+ */
50735
+ function resolveBSONOptions(options, parent) {
50736
+ const parentOptions = parent?.bsonOptions;
50737
+ return {
50738
+ raw: options?.raw ?? parentOptions?.raw ?? false,
50739
+ useBigInt64: options?.useBigInt64 ?? parentOptions?.useBigInt64 ?? false,
50740
+ promoteLongs: options?.promoteLongs ?? parentOptions?.promoteLongs ?? true,
50741
+ promoteValues: options?.promoteValues ?? parentOptions?.promoteValues ?? true,
50742
+ promoteBuffers: options?.promoteBuffers ?? parentOptions?.promoteBuffers ?? false,
50743
+ ignoreUndefined: options?.ignoreUndefined ?? parentOptions?.ignoreUndefined ?? false,
50744
+ bsonRegExp: options?.bsonRegExp ?? parentOptions?.bsonRegExp ?? false,
50745
+ serializeFunctions: options?.serializeFunctions ?? parentOptions?.serializeFunctions ?? false,
50746
+ fieldsAsRaw: options?.fieldsAsRaw ?? parentOptions?.fieldsAsRaw ?? {},
50747
+ enableUtf8Validation: options?.enableUtf8Validation ?? parentOptions?.enableUtf8Validation ?? true
50748
+ };
50749
+ }
50750
+ /** @internal */
50751
+ function parseUtf8ValidationOption(options) {
50752
+ const enableUtf8Validation = options?.enableUtf8Validation;
50753
+ if (enableUtf8Validation === false) {
50754
+ return { utf8: false };
50755
+ }
50756
+ return { utf8: { writeErrors: false } };
50757
+ }
50758
+ //# sourceMappingURL=bson.js.map
50759
+
50760
+ /***/ },
50761
+
50762
+ /***/ "./node_modules/mongoose/node_modules/mongodb/lib/bson.js"
50763
+ /*!****************************************************************!*\
50764
+ !*** ./node_modules/mongoose/node_modules/mongodb/lib/bson.js ***!
50765
+ \****************************************************************/
50766
+ (__unused_webpack_module, exports, __webpack_require__) {
50767
+
50768
+ "use strict";
50769
+
50531
50770
  Object.defineProperty(exports, "__esModule", ({ value: true }));
50532
50771
  exports.setUint32LE = exports.readInt32LE = exports.UUID = exports.Timestamp = exports.serialize = exports.ObjectId = exports.NumberUtils = exports.MinKey = exports.MaxKey = exports.Long = exports.Int32 = exports.EJSON = exports.Double = exports.deserialize = exports.Decimal128 = exports.DBRef = exports.Code = exports.calculateObjectSize = exports.ByteUtils = exports.BSONType = exports.BSONSymbol = exports.BSONRegExp = exports.BSONError = exports.BSON = exports.Binary = void 0;
50533
50772
  exports.parseToElementsToArray = parseToElementsToArray;
@@ -51393,7 +51632,7 @@ __webpack_require__.r(__webpack_exports__);
51393
51632
  /* harmony import */ var _vue_runtime_dom__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! @vue/runtime-dom */ "./node_modules/@vue/runtime-core/dist/runtime-core.esm-bundler.js");
51394
51633
  /* harmony import */ var _vue_runtime_dom__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! @vue/runtime-dom */ "./node_modules/@vue/runtime-dom/dist/runtime-dom.esm-bundler.js");
51395
51634
  /**
51396
- * vue v3.5.35
51635
+ * vue v3.5.38
51397
51636
  * (c) 2018-present Yuxi (Evan) You and Vue contributors
51398
51637
  * @license MIT
51399
51638
  **/
@@ -52745,7 +52984,7 @@ module.exports = "<div class=\"dashboard-editor p-4 bg-muted rounded-lg shadow-l
52745
52984
  (module) {
52746
52985
 
52747
52986
  "use strict";
52748
- module.exports = "<div class=\"dashboards bg-page min-h-[calc(100vh-55px)]\">\n <div class=\"max-w-5xl mx-auto px-4 sm:px-6 lg:px-8 py-8\">\n <div class=\"flex items-center justify-between mb-6\">\n <h1 class=\"text-lg font-semibold text-content\">Dashboards</h1>\n <button\n type=\"button\"\n @click=\"showCreateDashboardModal = true\"\n class=\"rounded-md bg-primary px-3 py-2 text-sm font-semibold text-primary-text shadow-sm hover:bg-primary-hover focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary\">\n New Dashboard\n </button>\n </div>\n\n <div v-if=\"status === 'loading'\" class=\"text-center py-12\">\n <svg\n class=\"inline w-8 h-8 animate-spin text-gray-400\"\n xmlns=\"http://www.w3.org/2000/svg\"\n fill=\"none\"\n viewBox=\"0 0 24 24\"\n >\n <circle class=\"opacity-25\" cx=\"12\" cy=\"12\" r=\"10\" stroke=\"currentColor\" stroke-width=\"4\"></circle>\n <path class=\"opacity-75\" fill=\"currentColor\" d=\"M4 12a8 8 0 018-8v4a4 4 0 00-4 4H4z\"></path>\n </svg>\n </div>\n\n <div v-else-if=\"dashboards.length === 0\" class=\"text-center py-12\">\n <h3 class=\"text-sm font-semibold text-content\">No dashboards yet</h3>\n <p class=\"mt-1 text-sm text-content-tertiary\">Get started by creating a new dashboard.</p>\n <div class=\"mt-4\">\n <button\n type=\"button\"\n @click=\"showCreateDashboardModal = true\"\n class=\"inline-flex items-center rounded-md bg-primary px-3 py-2 text-sm font-semibold text-primary-text shadow-sm hover:bg-primary-hover\">\n <svg class=\"-ml-0.5 mr-1.5 h-5 w-5\" viewBox=\"0 0 20 20\" fill=\"currentColor\" aria-hidden=\"true\">\n <path d=\"M10.75 4.75a.75.75 0 00-1.5 0v4.5h-4.5a.75.75 0 000 1.5h4.5v4.5a.75.75 0 001.5 0v-4.5h4.5a.75.75 0 000-1.5h-4.5v-4.5z\" />\n </svg>\n New Dashboard\n </button>\n </div>\n </div>\n\n <div v-else class=\"space-y-3\">\n <div\n v-for=\"dashboard in dashboards\"\n :key=\"dashboard._id\"\n class=\"bg-surface rounded-lg border border-edge px-5 py-4 flex items-center justify-between hover:border-edge-strong transition-colors\"\n >\n <div class=\"min-w-0 flex-1 mr-4\">\n <router-link\n :to=\"'/dashboard/' + dashboard._id\"\n class=\"text-sm font-semibold text-content hover:text-primary block truncate\">\n {{ dashboard.title }}\n </router-link>\n <p v-if=\"dashboard.description\" class=\"mt-0.5 text-sm text-content-tertiary truncate\">{{ dashboard.description }}</p>\n </div>\n <div class=\"flex items-center gap-2 shrink-0\">\n <router-link\n :to=\"'/dashboard/' + dashboard._id\"\n class=\"rounded-md bg-primary px-3 py-1.5 text-sm font-semibold text-primary-text shadow-sm hover:bg-primary-hover\">\n View\n </router-link>\n <div class=\"relative\" v-click-outside=\"() => closeMenu(dashboard._id)\">\n <button\n type=\"button\"\n @click=\"toggleMenu(dashboard._id)\"\n class=\"rounded-md bg-surface px-2 py-1.5 text-content-tertiary ring-1 ring-inset ring-gray-300 hover:bg-page\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 24 24\" stroke-width=\"1.5\" stroke=\"currentColor\" class=\"w-4 h-4\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M6.75 12a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0Zm6 0a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0Zm6 0a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0Z\" />\n </svg>\n </button>\n <div\n v-if=\"openMenuId === dashboard._id\"\n class=\"absolute right-0 mt-1 w-36 origin-top-right rounded-md bg-surface shadow-lg ring-1 ring-black/5 z-10\"\n >\n <div class=\"py-1\">\n <router-link\n :to=\"'/dashboard/' + dashboard._id + '?edit=true'\"\n class=\"block px-4 py-2 text-sm text-content-secondary hover:bg-muted\">\n Edit\n </router-link>\n <button\n @click=\"showDeleteDashboardModal = dashboard; openMenuId = null\"\n type=\"button\"\n class=\"block w-full text-left px-4 py-2 text-sm text-red-600 hover:bg-muted\">\n Delete\n </button>\n </div>\n </div>\n </div>\n </div>\n </div>\n </div>\n </div>\n\n <modal v-if=\"showCreateDashboardModal\">\n <template v-slot:body>\n <div class=\"modal-exit\" @click=\"showCreateDashboardModal = false;\">&times;</div>\n <create-dashboard @close=\"insertNewDashboard\"></create-dashboard>\n </template>\n </modal>\n\n <modal v-if=\"showDeleteDashboardModal\">\n <template v-slot:body>\n <div class=\"modal-exit\" @click=\"showDeleteDashboardModal = null;\">&times;</div>\n <h2>Are you sure you want to delete \"{{showDeleteDashboardModal.title}}\"?</h2>\n <div class=\"flex space-x-2 mt-4\">\n <button class=\"px-4 py-2 bg-red-600 text-white rounded-md hover:bg-red-500 text-sm font-semibold\" @click=\"deleteDashboard(showDeleteDashboardModal)\">Yes, delete</button>\n <button class=\"px-4 py-2 bg-muted text-content-secondary rounded-md hover:bg-muted text-sm font-semibold\" @click=\"showDeleteDashboardModal=null;\">Cancel</button>\n </div>\n </template>\n </modal>\n</div>\n";
52987
+ module.exports = "<div class=\"dashboards bg-page min-h-[calc(100vh-55px)]\">\n <div class=\"max-w-5xl mx-auto px-4 sm:px-6 lg:px-8 py-8\">\n <div class=\"flex items-center justify-between mb-6\">\n <h1 class=\"text-lg font-semibold text-content\">Dashboards</h1>\n <button\n type=\"button\"\n @click=\"showCreateDashboardModal = true\"\n class=\"rounded-md bg-primary px-3 py-2 text-sm font-semibold text-primary-text shadow-sm hover:bg-primary-hover focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary\">\n New Dashboard\n </button>\n </div>\n\n <div v-if=\"status !== 'loading'\" class=\"mb-5\">\n <label for=\"dashboard-search\" class=\"sr-only\">Search dashboards</label>\n <div class=\"relative max-w-lg\">\n <div class=\"pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3 text-content-tertiary\">\n <svg class=\"h-4 w-4\" xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 24 24\" stroke-width=\"1.5\" stroke=\"currentColor\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"m21 21-5.197-5.197m0 0A7.5 7.5 0 1 0 5.196 5.196a7.5 7.5 0 0 0 10.607 10.607Z\" />\n </svg>\n </div>\n <input\n id=\"dashboard-search\"\n v-model=\"searchText\"\n type=\"search\"\n class=\"block w-full rounded-md border-0 bg-surface py-2 pl-9 pr-3 text-sm text-content shadow-sm ring-1 ring-inset ring-edge placeholder:text-content-tertiary focus:ring-2 focus:ring-inset focus:ring-primary\"\n placeholder=\"Search by title or description\">\n </div>\n </div>\n\n <div v-if=\"status === 'loading'\" class=\"text-center py-12\">\n <svg\n class=\"inline w-8 h-8 animate-spin text-gray-400\"\n xmlns=\"http://www.w3.org/2000/svg\"\n fill=\"none\"\n viewBox=\"0 0 24 24\"\n >\n <circle class=\"opacity-25\" cx=\"12\" cy=\"12\" r=\"10\" stroke=\"currentColor\" stroke-width=\"4\"></circle>\n <path class=\"opacity-75\" fill=\"currentColor\" d=\"M4 12a8 8 0 018-8v4a4 4 0 00-4 4H4z\"></path>\n </svg>\n </div>\n\n <div v-else-if=\"dashboards.length === 0\" class=\"text-center py-12\">\n <h3 class=\"text-sm font-semibold text-content\">No dashboards yet</h3>\n <p class=\"mt-1 text-sm text-content-tertiary\">Get started by creating a new dashboard.</p>\n <div class=\"mt-4\">\n <button\n type=\"button\"\n @click=\"showCreateDashboardModal = true\"\n class=\"inline-flex items-center rounded-md bg-primary px-3 py-2 text-sm font-semibold text-primary-text shadow-sm hover:bg-primary-hover\">\n <svg class=\"-ml-0.5 mr-1.5 h-5 w-5\" viewBox=\"0 0 20 20\" fill=\"currentColor\" aria-hidden=\"true\">\n <path d=\"M10.75 4.75a.75.75 0 00-1.5 0v4.5h-4.5a.75.75 0 000 1.5h4.5v4.5a.75.75 0 001.5 0v-4.5h4.5a.75.75 0 000-1.5h-4.5v-4.5z\" />\n </svg>\n New Dashboard\n </button>\n </div>\n </div>\n\n <div v-else-if=\"filteredDashboards.length === 0\" class=\"text-center py-12\">\n <h3 class=\"text-sm font-semibold text-content\">No matching dashboards</h3>\n <p class=\"mt-1 text-sm text-content-tertiary\">Try a different title or description.</p>\n </div>\n\n <div v-else class=\"space-y-3\">\n <div\n v-for=\"dashboard in filteredDashboards\"\n :key=\"dashboard._id\"\n class=\"bg-surface rounded-lg border border-edge px-5 py-4 flex items-start justify-between hover:border-edge-strong transition-colors\"\n >\n <div class=\"min-w-0 flex-1 mr-4\">\n <router-link\n :to=\"'/dashboard/' + dashboard._id\"\n class=\"text-sm font-semibold text-content hover:text-primary block truncate\">\n <span v-html=\"highlightTitle(dashboard.title)\"></span>\n </router-link>\n <p\n v-if=\"dashboard.description\"\n class=\"mt-0.5 text-sm text-content-tertiary truncate\"\n v-html=\"highlightDescription(dashboard.description)\"></p>\n <dl class=\"mt-3 grid grid-cols-1 gap-x-4 gap-y-1 text-xs text-content-tertiary sm:grid-cols-3\">\n <div>\n <dt class=\"font-medium text-content-secondary\">Created At</dt>\n <dd>{{ formatDate(dashboard.createdAt) }}</dd>\n </div>\n <div>\n <dt class=\"font-medium text-content-secondary\">Last Evaluated At</dt>\n <dd>{{ formatDate(dashboard.lastEvaluatedAt) }}</dd>\n </div>\n <div v-show=\"dashboard.createdBy\">\n <dt class=\"font-medium text-content-secondary\">Created By</dt>\n <dd class=\"truncate\">{{ dashboard.createdBy?.name }}</dd>\n </div>\n </dl>\n </div>\n <div class=\"flex items-center gap-2 shrink-0\">\n <button\n type=\"button\"\n @click=\"togglePin(dashboard)\"\n :title=\"dashboard.isPinned ? 'Unpin dashboard' : 'Pin dashboard'\"\n :aria-label=\"dashboard.isPinned ? 'Unpin dashboard' : 'Pin dashboard'\"\n class=\"rounded-md bg-surface px-2 py-1.5 text-content-tertiary ring-1 ring-inset ring-gray-300 hover:bg-page\"\n :class=\"dashboard.isPinned ? 'text-primary ring-primary/40' : ''\">\n <img :src=\"dashboard.isPinned ? 'images/pin-off.svg' : 'images/pin.svg'\" class=\"w-4 h-4\" alt=\"\" />\n </button>\n <router-link\n :to=\"'/dashboard/' + dashboard._id\"\n class=\"rounded-md bg-primary px-3 py-1.5 text-sm font-semibold text-primary-text shadow-sm hover:bg-primary-hover\">\n View\n </router-link>\n <div class=\"relative\" v-click-outside=\"() => closeMenu(dashboard._id)\">\n <button\n type=\"button\"\n @click=\"toggleMenu(dashboard._id)\"\n class=\"rounded-md bg-surface px-2 py-1.5 text-content-tertiary ring-1 ring-inset ring-gray-300 hover:bg-page\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 24 24\" stroke-width=\"1.5\" stroke=\"currentColor\" class=\"w-4 h-4\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M6.75 12a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0Zm6 0a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0Zm6 0a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0Z\" />\n </svg>\n </button>\n <div\n v-if=\"openMenuId === dashboard._id\"\n class=\"absolute right-0 mt-1 w-36 origin-top-right rounded-md bg-surface shadow-lg ring-1 ring-black/5 z-10\"\n >\n <div class=\"py-1\">\n <router-link\n :to=\"'/dashboard/' + dashboard._id + '?edit=true'\"\n class=\"block px-4 py-2 text-sm text-content-secondary hover:bg-muted\">\n Edit\n </router-link>\n <button\n @click=\"showDeleteDashboardModal = dashboard; openMenuId = null\"\n type=\"button\"\n class=\"block w-full text-left px-4 py-2 text-sm text-red-600 hover:bg-muted\">\n Delete\n </button>\n </div>\n </div>\n </div>\n </div>\n </div>\n </div>\n </div>\n\n <modal v-if=\"showCreateDashboardModal\">\n <template v-slot:body>\n <div class=\"modal-exit\" @click=\"showCreateDashboardModal = false;\">&times;</div>\n <create-dashboard @close=\"insertNewDashboard\"></create-dashboard>\n </template>\n </modal>\n\n <modal v-if=\"showDeleteDashboardModal\">\n <template v-slot:body>\n <div class=\"modal-exit\" @click=\"showDeleteDashboardModal = null;\">&times;</div>\n <h2>Are you sure you want to delete \"{{showDeleteDashboardModal.title}}\"?</h2>\n <div class=\"flex space-x-2 mt-4\">\n <button class=\"px-4 py-2 bg-red-600 text-white rounded-md hover:bg-red-500 text-sm font-semibold\" @click=\"deleteDashboard(showDeleteDashboardModal)\">Yes, delete</button>\n <button class=\"px-4 py-2 bg-muted text-content-secondary rounded-md hover:bg-muted text-sm font-semibold\" @click=\"showDeleteDashboardModal=null;\">Cancel</button>\n </div>\n </template>\n </modal>\n</div>\n";
52749
52988
 
52750
52989
  /***/ },
52751
52990
 
@@ -58836,7 +59075,7 @@ exports.validatorErrorSymbol = Symbol('mongoose#validatorError');
58836
59075
 
58837
59076
 
58838
59077
 
58839
- const ObjectId = (__webpack_require__(/*! mongodb/lib/bson */ "./node_modules/mongodb/lib/bson.js").ObjectId);
59078
+ const ObjectId = (__webpack_require__(/*! mongodb/lib/bson */ "./node_modules/mongoose/node_modules/mongodb/lib/bson.js").ObjectId);
58840
59079
  const objectIdSymbol = (__webpack_require__(/*! ../helpers/symbols */ "./node_modules/mongoose/lib/helpers/symbols.js").objectIdSymbol);
58841
59080
 
58842
59081
  /**
@@ -64736,7 +64975,7 @@ var src_default = VueToastificationPlugin;
64736
64975
  (module) {
64737
64976
 
64738
64977
  "use strict";
64739
- module.exports = /*#__PURE__*/JSON.parse('{"name":"@mongoosejs/studio","version":"0.5.5","description":"AI-Powered Data Workspace for MongoDB: Turn your MongoDB data into dashboards, maps, and interactive workflows - powered by your Mongoose models and AI.","homepage":"https://mongoosestudio.app/","repository":{"type":"git","url":"https://github.com/mongoosejs/studio"},"license":"Apache-2.0","dependencies":{"@ai-sdk/anthropic":"2.x","@ai-sdk/google":"2.x","@ai-sdk/openai":"2.x","ace-builds":"^1.43.6","ai":"5.x","archetype":"0.13.1","csv-stringify":"6.3.0","ejson":"^2.2.3","extrovert":"^0.2.0","marked":"15.0.12","mpath":"^0.9.0","node-inspect-extracted":"3.x","regexp.escape":"^2.0.1","tailwindcss":"3.4.0","time-commando":"1.0.1","typescript":"^6.0.2","vue":"3.x","vue-toastification":"^2.0.0-rc.5","webpack":"5.x","xss":"^1.0.15"},"peerDependencies":{"mongoose":"7.x || 8.x || ^9.0.0"},"optionalPeerDependencies":{"@mongoosejs/task":"0.5.x || 0.6.x"},"devDependencies":{"@masteringjs/eslint-config":"0.1.1","axios":"1.2.2","dedent":"^1.6.0","eslint":"9.30.0","express":"4.x","mocha":"10.2.0","mongodb-memory-server":"^11.0.1","mongoose":"9.x","sinon":"^21.0.1"},"scripts":{"lint":"eslint .","seed":"node seed/index.js","start":"node ./local.js","tailwind":"tailwindcss -o ./frontend/public/tw.css","tailwind:watch":"tailwindcss -o ./frontend/public/tw.css --watch","test":"mocha test/*.test.js","test:frontend":"mocha test/frontend/*.test.js","eval:agent":"node eval/agent/run.js"}}');
64978
+ module.exports = /*#__PURE__*/JSON.parse('{"name":"@mongoosejs/studio","version":"0.5.7","description":"AI-Powered Data Workspace for MongoDB: Turn your MongoDB data into dashboards, maps, and interactive workflows - powered by your Mongoose models and AI.","homepage":"https://mongoosestudio.app/","repository":{"type":"git","url":"https://github.com/mongoosejs/studio"},"license":"Apache-2.0","dependencies":{"@ai-sdk/anthropic":"2.x","@ai-sdk/google":"2.x","@ai-sdk/openai":"2.x","ace-builds":"^1.43.6","ai":"5.x","archetype":"0.13.1","csv-stringify":"6.3.0","ejson":"^2.2.3","extrovert":"^0.2.0","marked":"15.0.12","mpath":"^0.9.0","node-inspect-extracted":"3.x","regexp.escape":"^2.0.1","tailwindcss":"3.4.0","time-commando":"1.0.1","typescript":"^6.0.2","vue":"3.x","vue-toastification":"^2.0.0-rc.5","webpack":"5.x","xss":"^1.0.15"},"peerDependencies":{"@nestjs/common":"10.x || 11.x","mongoose":"7.x || 8.x || ^9.0.0"},"peerDependenciesMeta":{"@nestjs/common":{"optional":true}},"optionalPeerDependencies":{"@mongoosejs/task":"0.5.x || 0.6.x"},"devDependencies":{"@masteringjs/eslint-config":"0.1.1","axios":"1.2.2","dedent":"^1.6.0","eslint":"9.30.0","@nestjs/common":"^11.1.9","express":"4.x","mocha":"10.2.0","mongodb-memory-server":"^11.0.1","mongoose":"9.x","sinon":"^21.0.1"},"scripts":{"lint":"eslint .","seed":"node seed/index.js","start":"node ./local.js","tailwind":"tailwindcss -o ./frontend/public/tw.css","tailwind:watch":"tailwindcss -o ./frontend/public/tw.css --watch","test":"mocha test/*.test.js","test:frontend":"mocha test/frontend/*.test.js","eval:agent":"node eval/agent/run.js"}}');
64740
64979
 
64741
64980
  /***/ }
64742
64981
 
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="currentColor"><path d="M680-840v80h-40v327l-80-80v-247H400v87l-87-87-33-33v-47h400ZM480-40l-40-40v-240H240v-80l80-80v-46L56-792l56-56 736 736-58 56-264-264h-6v240l-40 40ZM354-400h92l-44-44-2-2-46 46Zm126-193Zm-78 149Z"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="currentColor"><path d="m640-480 80 80v80H520v240l-40 40-40-40v-240H240v-80l80-80v-280h-40v-80h400v80h-40v280Zm-286 80h252l-46-46v-314H400v314l-46 46Zm126 0Z"/></svg>
@@ -905,6 +905,10 @@ video {
905
905
  margin-bottom: 1rem;
906
906
  }
907
907
 
908
+ .mb-5 {
909
+ margin-bottom: 1.25rem;
910
+ }
911
+
908
912
  .mb-6 {
909
913
  margin-bottom: 1.5rem;
910
914
  }
@@ -1681,6 +1685,10 @@ video {
1681
1685
  column-gap: 1.5rem;
1682
1686
  }
1683
1687
 
1688
+ .gap-y-1 {
1689
+ row-gap: 0.25rem;
1690
+ }
1691
+
1684
1692
  .space-x-2 > :not([hidden]) ~ :not([hidden]) {
1685
1693
  --tw-space-x-reverse: 0;
1686
1694
  margin-right: calc(0.5rem * var(--tw-space-x-reverse));
@@ -2524,6 +2532,10 @@ video {
2524
2532
  padding-left: 2rem;
2525
2533
  }
2526
2534
 
2535
+ .pl-9 {
2536
+ padding-left: 2.25rem;
2537
+ }
2538
+
2527
2539
  .pr-12 {
2528
2540
  padding-right: 3rem;
2529
2541
  }
@@ -2927,6 +2939,10 @@ video {
2927
2939
  text-decoration-line: underline;
2928
2940
  }
2929
2941
 
2942
+ .decoration-primary {
2943
+ text-decoration-color: var(--color-primary);
2944
+ }
2945
+
2930
2946
  .decoration-dotted {
2931
2947
  text-decoration-style: dotted;
2932
2948
  }
@@ -3063,6 +3079,10 @@ video {
3063
3079
  --tw-ring-color: rgb(0 0 0 / 0.05);
3064
3080
  }
3065
3081
 
3082
+ .ring-edge {
3083
+ --tw-ring-color: var(--color-edge);
3084
+ }
3085
+
3066
3086
  .ring-edge-strong {
3067
3087
  --tw-ring-color: var(--color-edge-strong);
3068
3088
  }
@@ -3650,6 +3670,10 @@ video {
3650
3670
  box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000);
3651
3671
  }
3652
3672
 
3673
+ .focus\:ring-inset:focus {
3674
+ --tw-ring-inset: inset;
3675
+ }
3676
+
3653
3677
  .focus\:ring-blue-500:focus {
3654
3678
  --tw-ring-opacity: 1;
3655
3679
  --tw-ring-color: rgb(59 130 246 / var(--tw-ring-opacity));
@@ -3896,6 +3920,10 @@ video {
3896
3920
  flex: none;
3897
3921
  }
3898
3922
 
3923
+ .sm\:grid-cols-3 {
3924
+ grid-template-columns: repeat(3, minmax(0, 1fr));
3925
+ }
3926
+
3899
3927
  .sm\:grid-cols-4 {
3900
3928
  grid-template-columns: repeat(4, minmax(0, 1fr));
3901
3929
  }
@@ -10,6 +10,23 @@
10
10
  </button>
11
11
  </div>
12
12
 
13
+ <div v-if="status !== 'loading'" class="mb-5">
14
+ <label for="dashboard-search" class="sr-only">Search dashboards</label>
15
+ <div class="relative max-w-lg">
16
+ <div class="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3 text-content-tertiary">
17
+ <svg class="h-4 w-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
18
+ <path stroke-linecap="round" stroke-linejoin="round" d="m21 21-5.197-5.197m0 0A7.5 7.5 0 1 0 5.196 5.196a7.5 7.5 0 0 0 10.607 10.607Z" />
19
+ </svg>
20
+ </div>
21
+ <input
22
+ id="dashboard-search"
23
+ v-model="searchText"
24
+ type="search"
25
+ class="block w-full rounded-md border-0 bg-surface py-2 pl-9 pr-3 text-sm text-content shadow-sm ring-1 ring-inset ring-edge placeholder:text-content-tertiary focus:ring-2 focus:ring-inset focus:ring-primary"
26
+ placeholder="Search by title or description">
27
+ </div>
28
+ </div>
29
+
13
30
  <div v-if="status === 'loading'" class="text-center py-12">
14
31
  <svg
15
32
  class="inline w-8 h-8 animate-spin text-gray-400"
@@ -38,21 +55,52 @@
38
55
  </div>
39
56
  </div>
40
57
 
58
+ <div v-else-if="filteredDashboards.length === 0" class="text-center py-12">
59
+ <h3 class="text-sm font-semibold text-content">No matching dashboards</h3>
60
+ <p class="mt-1 text-sm text-content-tertiary">Try a different title or description.</p>
61
+ </div>
62
+
41
63
  <div v-else class="space-y-3">
42
64
  <div
43
- v-for="dashboard in dashboards"
65
+ v-for="dashboard in filteredDashboards"
44
66
  :key="dashboard._id"
45
- class="bg-surface rounded-lg border border-edge px-5 py-4 flex items-center justify-between hover:border-edge-strong transition-colors"
67
+ class="bg-surface rounded-lg border border-edge px-5 py-4 flex items-start justify-between hover:border-edge-strong transition-colors"
46
68
  >
47
69
  <div class="min-w-0 flex-1 mr-4">
48
70
  <router-link
49
71
  :to="'/dashboard/' + dashboard._id"
50
72
  class="text-sm font-semibold text-content hover:text-primary block truncate">
51
- {{ dashboard.title }}
73
+ <span v-html="highlightTitle(dashboard.title)"></span>
52
74
  </router-link>
53
- <p v-if="dashboard.description" class="mt-0.5 text-sm text-content-tertiary truncate">{{ dashboard.description }}</p>
75
+ <p
76
+ v-if="dashboard.description"
77
+ class="mt-0.5 text-sm text-content-tertiary truncate"
78
+ v-html="highlightDescription(dashboard.description)"></p>
79
+ <dl class="mt-3 grid grid-cols-1 gap-x-4 gap-y-1 text-xs text-content-tertiary sm:grid-cols-3">
80
+ <div>
81
+ <dt class="font-medium text-content-secondary">Created At</dt>
82
+ <dd>{{ formatDate(dashboard.createdAt) }}</dd>
83
+ </div>
84
+ <div>
85
+ <dt class="font-medium text-content-secondary">Last Evaluated At</dt>
86
+ <dd>{{ formatDate(dashboard.lastEvaluatedAt) }}</dd>
87
+ </div>
88
+ <div v-show="dashboard.createdBy">
89
+ <dt class="font-medium text-content-secondary">Created By</dt>
90
+ <dd class="truncate">{{ dashboard.createdBy?.name }}</dd>
91
+ </div>
92
+ </dl>
54
93
  </div>
55
94
  <div class="flex items-center gap-2 shrink-0">
95
+ <button
96
+ type="button"
97
+ @click="togglePin(dashboard)"
98
+ :title="dashboard.isPinned ? 'Unpin dashboard' : 'Pin dashboard'"
99
+ :aria-label="dashboard.isPinned ? 'Unpin dashboard' : 'Pin dashboard'"
100
+ class="rounded-md bg-surface px-2 py-1.5 text-content-tertiary ring-1 ring-inset ring-gray-300 hover:bg-page"
101
+ :class="dashboard.isPinned ? 'text-primary ring-primary/40' : ''">
102
+ <img :src="dashboard.isPinned ? 'images/pin-off.svg' : 'images/pin.svg'" class="w-4 h-4" alt="" />
103
+ </button>
56
104
  <router-link
57
105
  :to="'/dashboard/' + dashboard._id"
58
106
  class="rounded-md bg-primary px-3 py-1.5 text-sm font-semibold text-primary-text shadow-sm hover:bg-primary-hover">
@@ -9,11 +9,73 @@ module.exports = app => app.component('dashboards', {
9
9
  data: () => ({
10
10
  status: 'loading',
11
11
  dashboards: [],
12
+ searchText: '',
12
13
  showCreateDashboardModal: false,
13
14
  showDeleteDashboardModal: null,
14
15
  openMenuId: null
15
16
  }),
17
+ computed: {
18
+ filteredDashboards() {
19
+ const searchWords = getSearchWords(this.searchText);
20
+ let dashboards = this.dashboards;
21
+ if (searchWords.length === 0) {
22
+ return sortPinnedFirst(dashboards);
23
+ }
24
+ const searchPatterns = searchWords.map(word => new RegExp(escapeRegExp(word), 'i'));
25
+
26
+ dashboards = dashboards.filter(dashboard => {
27
+ const searchableText = `${dashboard.title || ''} ${dashboard.description || ''}`;
28
+ return searchPatterns.every(pattern => pattern.test(searchableText));
29
+ });
30
+
31
+ return sortPinnedFirst(dashboards);
32
+ }
33
+ },
16
34
  methods: {
35
+ highlightTitle(value) {
36
+ return this.highlightSearchText(value, 'underline decoration-primary underline-offset-2');
37
+ },
38
+ highlightDescription(value) {
39
+ return this.highlightSearchText(value, 'font-semibold text-content-secondary');
40
+ },
41
+ highlightSearchText(value, className) {
42
+ const text = value || '';
43
+ const searchWords = getSearchWords(this.searchText);
44
+ if (searchWords.length === 0) {
45
+ return escapeHtml(text);
46
+ }
47
+ const searchPattern = new RegExp(searchWords.map(escapeRegExp).join('|'), 'ig');
48
+
49
+ let html = '';
50
+ let lastIndex = 0;
51
+ let match = searchPattern.exec(text);
52
+ while (match != null) {
53
+ html += escapeHtml(text.slice(lastIndex, match.index));
54
+ html += `<span class="${className}">${escapeHtml(match[0])}</span>`;
55
+ lastIndex = match.index + match[0].length;
56
+ match = searchPattern.exec(text);
57
+ }
58
+ html += escapeHtml(text.slice(lastIndex));
59
+ return html;
60
+ },
61
+ formatDate(value) {
62
+ if (!value) {
63
+ return '-';
64
+ }
65
+ const date = new Date(value);
66
+ if (Number.isNaN(date.getTime())) {
67
+ return '-';
68
+ }
69
+ return new Intl.DateTimeFormat(undefined, {
70
+ dateStyle: 'medium',
71
+ timeStyle: 'short'
72
+ }).format(date);
73
+ },
74
+ formatUser(dashboard) {
75
+ return dashboard.createdBy?.name ||
76
+ dashboard.createdBy?.email ||
77
+ (dashboard.createdById ? dashboard.createdById.toString() : '-');
78
+ },
17
79
  toggleMenu(id) {
18
80
  this.openMenuId = this.openMenuId === id ? null : id;
19
81
  },
@@ -33,8 +95,19 @@ module.exports = app => app.component('dashboards', {
33
95
  this.$toast.success('Dashboard deleted!');
34
96
  },
35
97
  insertNewDashboard(dashboard) {
36
- this.dashboards.push(dashboard);
98
+ this.dashboards.unshift(dashboard);
37
99
  this.showCreateDashboardModal = false;
100
+ },
101
+ async togglePin(dashboard) {
102
+ if (!dashboard) {
103
+ return;
104
+ }
105
+
106
+ const { doc } = await api.Dashboard.updateDashboard({
107
+ dashboardId: dashboard._id,
108
+ isPinned: !dashboard.isPinned
109
+ });
110
+ dashboard.isPinned = doc.isPinned;
38
111
  }
39
112
  },
40
113
  directives: {
@@ -58,3 +131,25 @@ module.exports = app => app.component('dashboards', {
58
131
  this.status = 'loaded';
59
132
  }
60
133
  });
134
+
135
+ function escapeHtml(value) {
136
+ return value.toString().
137
+ replace(/&/g, '&amp;').
138
+ replace(/</g, '&lt;').
139
+ replace(/>/g, '&gt;').
140
+ replace(/"/g, '&quot;').
141
+ replace(/'/g, '&#39;');
142
+ }
143
+
144
+ function escapeRegExp(value) {
145
+ return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
146
+ }
147
+
148
+ function getSearchWords(searchText) {
149
+ return Array.from(new Set(searchText.trim().split(/\s+/).filter(Boolean))).
150
+ sort((a, b) => b.length - a.length);
151
+ }
152
+
153
+ function sortPinnedFirst(dashboards) {
154
+ return dashboards.slice().sort((a, b) => Number(Boolean(b.isPinned)) - Number(Boolean(a.isPinned)));
155
+ }
package/nest.d.ts ADDED
@@ -0,0 +1,27 @@
1
+ declare module '@mongoosejs/studio/nest' {
2
+ import { DynamicModule, MiddlewareConsumer, NestModule } from '@nestjs/common';
3
+ import { Connection, Mongoose } from 'mongoose';
4
+
5
+ export interface MongooseStudioModuleOptions {
6
+ path?: string;
7
+ apiPath?: string;
8
+ connection?: Connection | Mongoose;
9
+ connectionToken?: string | symbol | Function;
10
+ apiKey?: string;
11
+ bindIp?: string | string[] | null;
12
+ [key: string]: any;
13
+ }
14
+
15
+ export interface MongooseStudioModuleAsyncOptions {
16
+ imports?: any[];
17
+ inject?: any[];
18
+ connectionToken?: string | symbol | Function;
19
+ useFactory: (...args: any[]) => Promise<MongooseStudioModuleOptions> | MongooseStudioModuleOptions;
20
+ }
21
+
22
+ export class MongooseStudioModule implements NestModule {
23
+ configure(consumer: MiddlewareConsumer): void;
24
+ static forRoot(options?: MongooseStudioModuleOptions): DynamicModule;
25
+ static forRootAsync(options: MongooseStudioModuleAsyncOptions): DynamicModule;
26
+ }
27
+ }
package/nest.js ADDED
@@ -0,0 +1,134 @@
1
+ 'use strict';
2
+
3
+ const { Inject, Module, RequestMethod } = require('@nestjs/common');
4
+ const mongoose = require('mongoose');
5
+ const createExpressRouter = require('./express');
6
+
7
+ const MONGOOSE_STUDIO_OPTIONS = Symbol('MONGOOSE_STUDIO_OPTIONS');
8
+ const MONGOOSE_STUDIO_CONNECTION = Symbol('MONGOOSE_STUDIO_CONNECTION');
9
+ const MONGOOSE_STUDIO_EXPRESS_ROUTER = Symbol('MONGOOSE_STUDIO_EXPRESS_ROUTER');
10
+
11
+ class MongooseStudioModule {
12
+ constructor(options, expressRouter) {
13
+ this.options = options || {};
14
+ this.expressRouter = expressRouter;
15
+ }
16
+
17
+ configure(consumer) {
18
+ const mountPath = this.options.path || '/studio';
19
+
20
+ consumer
21
+ .apply((req, res, next) => {
22
+ if (!isMountedStudioRequest(req, mountPath)) {
23
+ return next();
24
+ }
25
+
26
+ const originalUrl = req.url;
27
+ req.url = req.url.slice(normalizeExpressMountPath(mountPath).length) || '/';
28
+ try {
29
+ return this.expressRouter(req, res, err => {
30
+ req.url = originalUrl;
31
+ next(err);
32
+ });
33
+ } finally {
34
+ req.url = originalUrl;
35
+ }
36
+ })
37
+ .forRoutes({
38
+ path: '*path',
39
+ method: RequestMethod.ALL
40
+ });
41
+ }
42
+
43
+ static forRoot(options) {
44
+ options = options || {};
45
+ const providers = [
46
+ { provide: MONGOOSE_STUDIO_OPTIONS, useValue: options },
47
+ createExpressRouterProvider()
48
+ ];
49
+
50
+ if (options.connectionToken) {
51
+ providers.push({ provide: MONGOOSE_STUDIO_CONNECTION, useExisting: options.connectionToken });
52
+ } else if (options.connection) {
53
+ providers.push({ provide: MONGOOSE_STUDIO_CONNECTION, useValue: options.connection });
54
+ } else {
55
+ providers.push({ provide: MONGOOSE_STUDIO_CONNECTION, useValue: mongoose });
56
+ }
57
+
58
+ return {
59
+ module: MongooseStudioModule,
60
+ providers,
61
+ exports: providers
62
+ };
63
+ }
64
+
65
+ static forRootAsync(options) {
66
+ options = options || {};
67
+ const providers = [
68
+ {
69
+ provide: MONGOOSE_STUDIO_OPTIONS,
70
+ useFactory: options.useFactory,
71
+ inject: options.inject || []
72
+ },
73
+ createExpressRouterProvider()
74
+ ];
75
+
76
+ if (options.connectionToken) {
77
+ providers.push({ provide: MONGOOSE_STUDIO_CONNECTION, useExisting: options.connectionToken });
78
+ } else if (options.connection) {
79
+ providers.push({ provide: MONGOOSE_STUDIO_CONNECTION, useValue: options.connection });
80
+ } else {
81
+ providers.push({ provide: MONGOOSE_STUDIO_CONNECTION, useValue: mongoose });
82
+ }
83
+
84
+ return {
85
+ module: MongooseStudioModule,
86
+ imports: options.imports || [],
87
+ providers,
88
+ exports: providers
89
+ };
90
+ }
91
+ }
92
+
93
+ function createExpressRouterProvider() {
94
+ return {
95
+ provide: MONGOOSE_STUDIO_EXPRESS_ROUTER,
96
+ useFactory: async(options, connection) => {
97
+ options = options || {};
98
+ const mountPath = options.path || '/studio';
99
+ const apiPath = options.apiPath || `${mountPath}/api`;
100
+ const studioOptions = { ...options };
101
+ delete studioOptions.path;
102
+ delete studioOptions.apiPath;
103
+ delete studioOptions.connection;
104
+ delete studioOptions.connectionToken;
105
+
106
+ return createExpressRouter(apiPath, options.connection || connection || mongoose, studioOptions);
107
+ },
108
+ inject: [MONGOOSE_STUDIO_OPTIONS, MONGOOSE_STUDIO_CONNECTION]
109
+ };
110
+ }
111
+
112
+ function isMountedStudioRequest(req, mountPath) {
113
+ const normalizedMountPath = normalizeExpressMountPath(mountPath);
114
+ return req.url === normalizedMountPath ||
115
+ req.url.startsWith(`${normalizedMountPath}/`) ||
116
+ req.url.startsWith(`${normalizedMountPath}?`) ||
117
+ req.url.startsWith(`${normalizedMountPath}#`);
118
+ }
119
+
120
+ function normalizeExpressMountPath(path) {
121
+ path = `/${path.replace(/^\/+/, '').replace(/\/+$/, '')}`;
122
+ return path === '/' ? '' : path;
123
+ }
124
+
125
+ Inject(MONGOOSE_STUDIO_OPTIONS)(MongooseStudioModule, undefined, 0);
126
+ Inject(MONGOOSE_STUDIO_EXPRESS_ROUTER)(MongooseStudioModule, undefined, 1);
127
+ Module({})(MongooseStudioModule);
128
+
129
+ module.exports = {
130
+ MONGOOSE_STUDIO_CONNECTION,
131
+ MONGOOSE_STUDIO_EXPRESS_ROUTER,
132
+ MONGOOSE_STUDIO_OPTIONS,
133
+ MongooseStudioModule
134
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mongoosejs/studio",
3
- "version": "0.5.5",
3
+ "version": "0.5.7",
4
4
  "description": "AI-Powered Data Workspace for MongoDB: Turn your MongoDB data into dashboards, maps, and interactive workflows - powered by your Mongoose models and AI.",
5
5
  "homepage": "https://mongoosestudio.app/",
6
6
  "repository": {
@@ -31,8 +31,14 @@
31
31
  "xss": "^1.0.15"
32
32
  },
33
33
  "peerDependencies": {
34
+ "@nestjs/common": "10.x || 11.x",
34
35
  "mongoose": "7.x || 8.x || ^9.0.0"
35
36
  },
37
+ "peerDependenciesMeta": {
38
+ "@nestjs/common": {
39
+ "optional": true
40
+ }
41
+ },
36
42
  "optionalPeerDependencies": {
37
43
  "@mongoosejs/task": "0.5.x || 0.6.x"
38
44
  },
@@ -41,6 +47,7 @@
41
47
  "axios": "1.2.2",
42
48
  "dedent": "^1.6.0",
43
49
  "eslint": "9.30.0",
50
+ "@nestjs/common": "^11.1.9",
44
51
  "express": "4.x",
45
52
  "mocha": "10.2.0",
46
53
  "mongodb-memory-server": "^11.0.1",