@mongoosejs/studio 0.2.8 → 0.2.10

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.
@@ -66,7 +66,7 @@ module.exports = ({ db }) => async function getDashboard(params) {
66
66
  );
67
67
  });
68
68
 
69
- return { dashboard, dashboardResult };
69
+ return { dashboard, dashboardResult, result };
70
70
  } catch (error) {
71
71
  return { dashboard, error: { message: error.message } };
72
72
  }
@@ -13,6 +13,7 @@ exports.getDocumentsStream = require('./getDocumentsStream');
13
13
  exports.getCollectionInfo = require('./getCollectionInfo');
14
14
  exports.getIndexes = require('./getIndexes');
15
15
  exports.listModels = require('./listModels');
16
+ exports.streamDocumentChanges = require('./streamDocumentChanges');
16
17
  exports.streamChatMessage = require('./streamChatMessage');
17
18
  exports.updateDocument = require('./updateDocument');
18
19
  exports.updateDocuments = require('./updateDocuments');
@@ -2,6 +2,8 @@
2
2
 
3
3
  const Archetype = require('archetype');
4
4
  const authorize = require('../../authorize');
5
+ const getRefFromSchemaType = require('../../helpers/getRefFromSchemaType');
6
+ const removeSpecifiedPaths = require('../../helpers/removeSpecifiedPaths');
5
7
 
6
8
  const ListModelsParams = new Archetype({
7
9
  roles: {
@@ -15,8 +17,41 @@ module.exports = ({ db }) => async function listModels(params) {
15
17
 
16
18
  const readyState = db.connection?.readyState ?? db.readyState;
17
19
 
20
+ const models = Object.keys(db.models).filter(key => !key.startsWith('__Studio_')).sort();
21
+
22
+ const modelSchemaPaths = {};
23
+ for (const modelName of models) {
24
+ const Model = db.models[modelName];
25
+ const schemaPaths = {};
26
+ modelSchemaPaths[modelName] = schemaPaths;
27
+ for (const path of Object.keys(Model.schema.paths)) {
28
+ const schemaType = Model.schema.paths[path];
29
+ schemaPaths[path] = {
30
+ instance: schemaType.instance,
31
+ path,
32
+ ref: getRefFromSchemaType(schemaType),
33
+ required: schemaType.options?.required,
34
+ enum: schemaType.options?.enum
35
+ };
36
+ if (schemaType.schema) {
37
+ schemaPaths[path].schema = {};
38
+ for (const subpath of Object.keys(schemaType.schema.paths)) {
39
+ schemaPaths[path].schema[subpath] = {
40
+ instance: schemaType.schema.paths[subpath].instance,
41
+ path: subpath,
42
+ ref: getRefFromSchemaType(schemaType.schema.paths[subpath]),
43
+ required: schemaType.schema.paths[subpath].options?.required,
44
+ enum: schemaType.schema.paths[subpath].options?.enum
45
+ };
46
+ }
47
+ }
48
+ }
49
+ removeSpecifiedPaths(schemaPaths, '.$*');
50
+ }
51
+
18
52
  return {
19
- models: Object.keys(db.models).filter(key => !key.startsWith('__Studio_')).sort(),
53
+ models,
54
+ modelSchemaPaths,
20
55
  readyState
21
56
  };
22
57
  };
@@ -0,0 +1,123 @@
1
+ 'use strict';
2
+
3
+ const Archetype = require('archetype');
4
+ const authorize = require('../../authorize');
5
+
6
+ const StreamDocumentChangesParams = new Archetype({
7
+ model: {
8
+ $type: 'string',
9
+ $required: true
10
+ },
11
+ documentId: {
12
+ $type: 'string',
13
+ $required: true
14
+ },
15
+ roles: {
16
+ $type: ['string']
17
+ }
18
+ }).compile('StreamDocumentChangesParams');
19
+
20
+ module.exports = ({ db, changeStream }) => async function* streamDocumentChanges(params) {
21
+ const { model, documentId, roles } = new StreamDocumentChangesParams(params);
22
+
23
+ await authorize('Model.streamDocumentChanges', roles);
24
+
25
+ const Model = db.models[model];
26
+ if (Model == null) {
27
+ throw new Error(`Model ${model} not found`);
28
+ }
29
+
30
+ if (!changeStream) {
31
+ throw new Error('Change streams are not enabled');
32
+ }
33
+
34
+ const collectionName = Model.collection.name;
35
+ const targetId = String(documentId);
36
+
37
+ const queue = [];
38
+ let resolveQueue = null;
39
+ let streamError = null;
40
+ let streamEnded = false;
41
+
42
+ function enqueue(payload) {
43
+ queue.push(payload);
44
+ if (resolveQueue) {
45
+ const resolve = resolveQueue;
46
+ resolveQueue = null;
47
+ resolve();
48
+ }
49
+ }
50
+
51
+ function handleChange(change) {
52
+ if (!change || change.ns?.coll !== collectionName) {
53
+ return;
54
+ }
55
+ if (!change.documentKey || change.documentKey._id == null) {
56
+ return;
57
+ }
58
+ if (String(change.documentKey._id) !== targetId) {
59
+ return;
60
+ }
61
+
62
+ enqueue({
63
+ type: 'change',
64
+ operationType: change.operationType,
65
+ documentKey: change.documentKey,
66
+ ns: change.ns,
67
+ updateDescription: change.updateDescription,
68
+ clusterTime: change.clusterTime
69
+ });
70
+ }
71
+
72
+ function handleError(err) {
73
+ streamError = err || new Error('Change stream error');
74
+ enqueue({ type: 'error', message: streamError.message });
75
+ }
76
+
77
+ function handleEnd() {
78
+ streamEnded = true;
79
+ enqueue({ type: 'end' });
80
+ }
81
+
82
+ changeStream.on('change', handleChange);
83
+ changeStream.on('error', handleError);
84
+ changeStream.on('end', handleEnd);
85
+
86
+ try {
87
+ while (true) {
88
+ if (streamError) {
89
+ throw streamError;
90
+ }
91
+
92
+ if (queue.length === 0) {
93
+ await new Promise(resolve => {
94
+ resolveQueue = resolve;
95
+ });
96
+ }
97
+
98
+ if (streamError) {
99
+ throw streamError;
100
+ }
101
+
102
+ while (queue.length > 0) {
103
+ const payload = queue.shift();
104
+ if (payload?.type === 'end') {
105
+ return;
106
+ }
107
+ yield payload;
108
+ }
109
+
110
+ if (streamEnded) {
111
+ return;
112
+ }
113
+ }
114
+ } finally {
115
+ changeStream.off('change', handleChange);
116
+ changeStream.off('error', handleError);
117
+ changeStream.off('end', handleEnd);
118
+ if (resolveQueue) {
119
+ resolveQueue();
120
+ resolveQueue = null;
121
+ }
122
+ }
123
+ };
@@ -23,6 +23,7 @@ const actionsToRequiredRoles = {
23
23
  'Model.getDocumentsStream': ['owner', 'admin', 'member', 'readonly'],
24
24
  'Model.getIndexes': ['owner', 'admin', 'member', 'readonly'],
25
25
  'Model.listModels': ['owner', 'admin', 'member', 'readonly'],
26
+ 'Model.streamDocumentChanges': ['owner', 'admin', 'member', 'readonly'],
26
27
  'Model.streamChatMessage': ['owner', 'admin', 'member', 'readonly'],
27
28
  'Model.updateDocuments': ['owner', 'admin', 'member']
28
29
  };
package/backend/index.js CHANGED
@@ -16,6 +16,12 @@ module.exports = function backend(db, studioConnection, options) {
16
16
  const ChatMessage = studioConnection.model('__Studio_ChatMessage', chatMessageSchema, 'studio__chatMessages');
17
17
  const ChatThread = studioConnection.model('__Studio_ChatThread', chatThreadSchema, 'studio__chatThreads');
18
18
 
19
- const actions = applySpec(Actions, { db, studioConnection, options });
19
+ let changeStream = null;
20
+ if (options?.changeStream) {
21
+ changeStream = db.watch();
22
+ }
23
+
24
+ const actions = applySpec(Actions, { db, studioConnection, options, changeStream });
25
+ actions.services = { changeStream };
20
26
  return actions;
21
27
  };
package/backend/next.js CHANGED
@@ -5,9 +5,66 @@ const Backend = require('./');
5
5
  module.exports = function next(conn, options) {
6
6
  const backend = Backend(conn, options?.studioConnection, options);
7
7
 
8
- return function wrappedNextJSFunction(req, res) {
9
- const params = { ...req.query, ...req.body, ...req.params };
8
+ const mothershipUrl = options?._mothershipUrl || 'https://mongoose-js.netlify.app/.netlify/functions';
9
+ let workspace = null;
10
+
11
+ return async function wrappedNextJSFunction(req, res) {
12
+ const params = { ...req.query, ...req.body, ...req.params, authorization: req.headers.authorization };
10
13
  const actionName = params?.action;
14
+
15
+ const authorization = params?.authorization;
16
+ if (options?.apiKey) {
17
+ if (!authorization) {
18
+ throw new Error('Not authorized');
19
+ }
20
+
21
+ if (workspace == null) {
22
+ ({ workspace } = await fetch(`${mothershipUrl}/getWorkspace`, {
23
+ method: 'POST',
24
+ body: JSON.stringify({ apiKey: options.apiKey }),
25
+ headers: {
26
+ Authorization: `Bearer ${options.apiKey}`,
27
+ 'Content-Type': 'application/json'
28
+ }
29
+ })
30
+ .then(response => {
31
+ if (response.status < 200 || response.status >= 400) {
32
+ return response.json().then(data => {
33
+ throw new Error(`Error getting workspace ${response.status}: ${require('util').inspect(data)}`);
34
+ });
35
+ }
36
+ return response;
37
+ })
38
+ .then(res => res.json()));
39
+ }
40
+
41
+ const { user, roles } = await fetch(`${mothershipUrl}/me?`, {
42
+ method: 'POST',
43
+ body: JSON.stringify({ workspaceId: workspace._id }),
44
+ headers: {
45
+ Authorization: authorization,
46
+ 'Content-Type': 'application/json'
47
+ }
48
+ })
49
+ .then(response => {
50
+ if (response.status < 200 || response.status >= 400) {
51
+ return response.json().then(data => {
52
+ throw new Error(`Mongoose Studio API Key Error ${response.status}: ${require('util').inspect(data)}`);
53
+ });
54
+ }
55
+ return response;
56
+ })
57
+ .then(res => res.json());
58
+ if (!user || !roles) {
59
+ throw new Error('Not authorized');
60
+ }
61
+
62
+ params.$workspaceId = workspace._id;
63
+ params.roles = roles;
64
+ params.userId = user._id;
65
+ params.initiatedById = user._id;
66
+ }
67
+
11
68
  if (typeof actionName !== 'string') {
12
69
  throw new Error('No action specified');
13
70
  }
@@ -24,7 +81,10 @@ module.exports = function next(conn, options) {
24
81
  }
25
82
 
26
83
  return actionFn(params)
27
- .then(result => res.status(200).json(result))
84
+ .then(result => {
85
+ res.status(200).json(result);
86
+ return result;
87
+ })
28
88
  .catch(error => res.status(500).json({ message: error.message }));
29
89
  };
30
90
  };
package/eslint.config.js CHANGED
@@ -39,7 +39,9 @@ module.exports = defineConfig([
39
39
  process: true,
40
40
  setTimeout: true,
41
41
  navigator: true,
42
- TextDecoder: true
42
+ TextDecoder: true,
43
+ AbortController: true,
44
+ clearTimeout: true
43
45
  },
44
46
  sourceType: 'commonjs'
45
47
  },
package/express.js CHANGED
@@ -7,8 +7,9 @@ const { toRoute, objectRouter } = require('extrovert');
7
7
 
8
8
  module.exports = async function mongooseStudioExpressApp(apiUrl, conn, options) {
9
9
  const router = express.Router();
10
+ options = options ? { changeStream: true, ...options } : { changeStream: true };
10
11
 
11
- const mothershipUrl = options?._mothershipUrl || 'https://mongoose-js.netlify.app/.netlify/functions';
12
+ const mothershipUrl = options._mothershipUrl || 'https://mongoose-js.netlify.app/.netlify/functions';
12
13
  let workspace = null;
13
14
  if (options?.apiKey) {
14
15
  ({ workspace } = await fetch(`${mothershipUrl}/getWorkspace`, {
@@ -31,7 +32,7 @@ module.exports = async function mongooseStudioExpressApp(apiUrl, conn, options)
31
32
  }
32
33
 
33
34
  apiUrl = apiUrl || 'api';
34
- const backend = Backend(conn, options?.studioConnection, options);
35
+ const backend = Backend(conn, options.studioConnection, options);
35
36
 
36
37
  router.use(
37
38
  '/api',