@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.
- package/backend/actions/Dashboard/getDashboard.js +1 -1
- package/backend/actions/Model/index.js +1 -0
- package/backend/actions/Model/listModels.js +36 -1
- package/backend/actions/Model/streamDocumentChanges.js +123 -0
- package/backend/authorize.js +1 -0
- package/backend/index.js +7 -1
- package/backend/next.js +63 -3
- package/eslint.config.js +3 -1
- package/express.js +3 -2
- package/frontend/public/app.js +282 -39
- package/frontend/public/tw.css +46 -0
- package/frontend/src/api.js +60 -0
- package/frontend/src/dashboard-result/dashboard-document/dashboard-document.html +4 -5
- package/frontend/src/dashboard-result/dashboard-document/dashboard-document.js +13 -14
- package/frontend/src/document/document.html +58 -0
- package/frontend/src/document/document.js +194 -19
- package/frontend/src/index.js +12 -3
- package/package.json +1 -1
|
@@ -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
|
|
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
|
+
};
|
package/backend/authorize.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
9
|
-
|
|
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 =>
|
|
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
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
|
|
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
|
|
35
|
+
const backend = Backend(conn, options.studioConnection, options);
|
|
35
36
|
|
|
36
37
|
router.use(
|
|
37
38
|
'/api',
|