@docstack/client 0.0.1 → 0.0.3
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/lib/core/attribute.d.ts +4 -4
- package/lib/core/class.d.ts +13 -12
- package/lib/core/crypto-engine/index.d.ts +5 -5
- package/lib/core/query-engine/executor.d.ts +1 -1
- package/lib/core/query-engine/index.d.ts +3 -3
- package/lib/core/stack.d.ts +6 -6
- package/lib/index.d.ts +1 -1
- package/lib/index.js +7530 -7536
- package/lib/index.js.map +1 -1
- package/lib/index.umd.js +8256 -0
- package/lib/utils/index.d.ts +1 -1
- package/package.json +11 -4
- package/lib/core/attribute.js +0 -296
- package/lib/core/attribute.js.map +0 -1
- package/lib/core/class.js +0 -540
- package/lib/core/class.js.map +0 -1
- package/lib/core/crypto-engine/index.js +0 -130
- package/lib/core/crypto-engine/index.js.map +0 -1
- package/lib/core/crypto-engine/utils.js +0 -76
- package/lib/core/crypto-engine/utils.js.map +0 -1
- package/lib/core/datamodel/index.js +0 -1186
- package/lib/core/datamodel/index.js.map +0 -1
- package/lib/core/domain.js +0 -285
- package/lib/core/domain.js.map +0 -1
- package/lib/core/index.js +0 -379
- package/lib/core/index.js.map +0 -1
- package/lib/core/job-engine/index.js +0 -116
- package/lib/core/job-engine/index.js.map +0 -1
- package/lib/core/policy-engine/index.js +0 -150
- package/lib/core/policy-engine/index.js.map +0 -1
- package/lib/core/query-engine/accumulators.js +0 -258
- package/lib/core/query-engine/accumulators.js.map +0 -1
- package/lib/core/query-engine/evaluator.js +0 -179
- package/lib/core/query-engine/evaluator.js.map +0 -1
- package/lib/core/query-engine/executor.js +0 -405
- package/lib/core/query-engine/executor.js.map +0 -1
- package/lib/core/query-engine/index.js +0 -4
- package/lib/core/query-engine/index.js.map +0 -1
- package/lib/core/query-engine/parser.js +0 -515
- package/lib/core/query-engine/parser.js.map +0 -1
- package/lib/core/query-engine/planner.js +0 -330
- package/lib/core/query-engine/planner.js.map +0 -1
- package/lib/core/stack.js +0 -1507
- package/lib/core/stack.js.map +0 -1
- package/lib/core/test-utils/docstack.js +0 -222
- package/lib/core/test-utils/docstack.js.map +0 -1
- package/lib/core/trigger/index.js +0 -81
- package/lib/core/trigger/index.js.map +0 -1
- package/lib/plugins/pouchdb.js +0 -403
- package/lib/plugins/pouchdb.js.map +0 -1
- package/lib/utils/crypto/index.js +0 -34
- package/lib/utils/crypto/index.js.map +0 -1
- package/lib/utils/index.js +0 -58
- package/lib/utils/index.js.map +0 -1
- package/lib/utils/logger/index.js +0 -20
- package/lib/utils/logger/index.js.map +0 -1
- package/lib/utils/logger/transport.js +0 -28
- package/lib/utils/logger/transport.js.map +0 -1
- package/lib/workers/dataModel.js +0 -48
- package/lib/workers/dataModel.js.map +0 -1
package/lib/core/stack.js
DELETED
|
@@ -1,1507 +0,0 @@
|
|
|
1
|
-
import PouchDB from "pouchdb-browser";
|
|
2
|
-
import createLogger from "../utils/logger/index.js";
|
|
3
|
-
import Class from "./class.js";
|
|
4
|
-
import Domain from "./domain.js";
|
|
5
|
-
import PouchDBFind from 'pouchdb-find';
|
|
6
|
-
import { getSystemPatches } from "./datamodel/index.js";
|
|
7
|
-
import { Stack, isClassModel, } from "@docstack/shared";
|
|
8
|
-
import { StackPlugin } from "../plugins/pouchdb.js";
|
|
9
|
-
import { parse, createPlan, executePlan } from "./query-engine/index.js";
|
|
10
|
-
import { JobEngine } from "./job-engine/index.js";
|
|
11
|
-
import { PolicyEngine } from "./policy-engine/index.js";
|
|
12
|
-
import { CryptoEngine } from "./crypto-engine/index.js";
|
|
13
|
-
import { isEncryptedPayload } from "./crypto-engine/utils.js";
|
|
14
|
-
const logger = createLogger().child({ module: "stack" });
|
|
15
|
-
export const BASE_SCHEMA = {
|
|
16
|
-
"_id": { name: "_id", type: "string", config: { maxLength: 100, primaryKey: true } },
|
|
17
|
-
"~class": { name: "~class", type: "string", config: { maxLength: 100 } },
|
|
18
|
-
"~createTimestamp": { name: "~createTimestamp", type: "integer", config: { min: 0 } },
|
|
19
|
-
"~updateTimestamp": { name: "~updateTimestamp", type: "integer", config: { min: 0 } },
|
|
20
|
-
"description": { name: "description", type: "string", config: { maxLength: 1000 } },
|
|
21
|
-
"active": { name: "active", type: "boolean", config: { defaultValue: true, primaryKey: true } }
|
|
22
|
-
};
|
|
23
|
-
export const CLASS_SCHEMA = Object.assign(Object.assign({}, BASE_SCHEMA), { "~class": { name: "~class", type: "string", config: { defaultValue: "class" } }, "schema": { name: "schema", type: "object", config: { maxLength: 1000, isArray: false } }, "parentClass": { name: "parentClass", type: "foreign_key", config: { isArray: false } } });
|
|
24
|
-
const DOMAIN_SCHEMA = Object.assign(Object.assign({}, BASE_SCHEMA), { "~class": { name: "~class", type: "string", config: { defaultValue: "domain" } }, "schema": {
|
|
25
|
-
name: "schema", type: "object", config: {
|
|
26
|
-
isArray: true,
|
|
27
|
-
defaultValue: {
|
|
28
|
-
"source": {
|
|
29
|
-
name: "source",
|
|
30
|
-
type: "foreign_key",
|
|
31
|
-
config: {
|
|
32
|
-
isArray: false
|
|
33
|
-
}
|
|
34
|
-
},
|
|
35
|
-
"target": {
|
|
36
|
-
name: "target",
|
|
37
|
-
type: "foreign_key",
|
|
38
|
-
config: {
|
|
39
|
-
isArray: false
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
},
|
|
45
|
-
// "parentDomain": { name: "parentDomain", type: "foreign_key", config: { isArray: false } },
|
|
46
|
-
"relation": {
|
|
47
|
-
name: "relation", type: "enum", config: {
|
|
48
|
-
isArray: false, values: [
|
|
49
|
-
{ value: "1:1" }, { value: "1:N" }, { value: "N:1" }, { value: "N:N" }
|
|
50
|
-
]
|
|
51
|
-
}
|
|
52
|
-
}, "sourceClass": { name: "sourceClass", type: "foreign_key", config: { isArray: false } }, "targetClass": { name: "targetClass", type: "foreign_key", config: { isArray: false } } });
|
|
53
|
-
class ClientStack extends Stack {
|
|
54
|
-
constructor() {
|
|
55
|
-
super();
|
|
56
|
-
this.appVersion = "0.0.1";
|
|
57
|
-
this.listeners = [];
|
|
58
|
-
this.modelWorker = null;
|
|
59
|
-
this.dump = async () => {
|
|
60
|
-
const all = await this.db.allDocs({ include_docs: true });
|
|
61
|
-
return all;
|
|
62
|
-
};
|
|
63
|
-
this.applyPatch = async (patch) => {
|
|
64
|
-
const fnLogger = logger.child({ method: "applyPatch", args: { patch } });
|
|
65
|
-
return new Promise(async (resolve, reject) => {
|
|
66
|
-
try {
|
|
67
|
-
fnLogger.info("Attempting to apply patch", { patch });
|
|
68
|
-
fnLogger.info("applyPatch - starting to hydrate patch docs", { docCount: patch.docs.length });
|
|
69
|
-
const hydratedDocs = await Promise.all(patch.docs.map(async (doc) => {
|
|
70
|
-
if (doc._rev === "auto") {
|
|
71
|
-
delete doc._rev;
|
|
72
|
-
const existingDoc = await this.db.get(doc._id);
|
|
73
|
-
if (existingDoc) {
|
|
74
|
-
doc._rev = existingDoc._rev;
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
return doc;
|
|
78
|
-
}));
|
|
79
|
-
fnLogger.info("applyPatch - hydration complete, calling bulkDocs", { docCount: hydratedDocs.length });
|
|
80
|
-
await this.db.bulkDocs(hydratedDocs, { isPatch: true }).then((result) => {
|
|
81
|
-
fnLogger.warn("applyPatch - bulkDocs completed with result", { result });
|
|
82
|
-
fnLogger.warn("Successfully applied patch", { version: patch.version });
|
|
83
|
-
resolve(patch.version);
|
|
84
|
-
}).catch((error) => {
|
|
85
|
-
fnLogger.error("applyPatch - bulkDocs error", { error });
|
|
86
|
-
reject(error);
|
|
87
|
-
});
|
|
88
|
-
}
|
|
89
|
-
catch (e) {
|
|
90
|
-
fnLogger.error("Failed to apply patch", e);
|
|
91
|
-
reject(new Error(e));
|
|
92
|
-
}
|
|
93
|
-
});
|
|
94
|
-
};
|
|
95
|
-
this.setListeners = () => {
|
|
96
|
-
const fnLogger = logger.child({ method: "setListeners" });
|
|
97
|
-
// Listening for class model propagation
|
|
98
|
-
this.addEventListener('class-model-propagation-pending', this.onClassModelPropagationStart);
|
|
99
|
-
this.addEventListener('class-model-propagation-complete', this.onClassModelPropagationComplete);
|
|
100
|
-
// fnLogger.info("Setting up class model worker");
|
|
101
|
-
// this.modelWorker = new Worker(require("../workers/dataModel"), {type: "module"});
|
|
102
|
-
fnLogger.info("Setting up class model changes listener");
|
|
103
|
-
const classModelChanges = this.onClassModelChanges();
|
|
104
|
-
/*
|
|
105
|
-
this.modelWorker.onmessage = (event) => {
|
|
106
|
-
const { status, className, message } = event.data;
|
|
107
|
-
|
|
108
|
-
this.dispatchEvent(new CustomEvent('class-model-propagation-complete', {
|
|
109
|
-
detail: { className: className, success: status === 'success', message }
|
|
110
|
-
}));
|
|
111
|
-
|
|
112
|
-
if (status === 'error') {
|
|
113
|
-
fnLogger.error(`Model worker error for class '${className}': ${message}`);
|
|
114
|
-
}
|
|
115
|
-
};
|
|
116
|
-
*/
|
|
117
|
-
// Store listener for later
|
|
118
|
-
this.listeners.push(classModelChanges);
|
|
119
|
-
};
|
|
120
|
-
/**
|
|
121
|
-
* @description Clears all listeners from the Stack
|
|
122
|
-
*/
|
|
123
|
-
this.removeAllListeners = () => {
|
|
124
|
-
this.removeEventListener('class-model-propagation-pending', this.onClassModelPropagationStart);
|
|
125
|
-
this.removeEventListener('class-model-propagation-complete', this.onClassModelPropagationComplete);
|
|
126
|
-
if (this.listeners.length > 0) {
|
|
127
|
-
for (const listener of this.listeners) {
|
|
128
|
-
if (listener && typeof listener.cancel === 'function') {
|
|
129
|
-
try {
|
|
130
|
-
listener.cancel();
|
|
131
|
-
}
|
|
132
|
-
catch (error) {
|
|
133
|
-
logger.warn('Error while cancelling listener', { error });
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
this.listeners = [];
|
|
138
|
-
}
|
|
139
|
-
};
|
|
140
|
-
/**
|
|
141
|
-
* @description When a class model propagation starts write the ~lock document to the database.
|
|
142
|
-
* It prevents any further modifications on the class data model
|
|
143
|
-
* @param event
|
|
144
|
-
*/
|
|
145
|
-
this.onClassModelPropagationStart = (event) => {
|
|
146
|
-
const className = event.detail.className;
|
|
147
|
-
const fnLogger = logger.child({ method: "onClassModelPropagationStart", className });
|
|
148
|
-
this.addClassLock(className).then(() => {
|
|
149
|
-
fnLogger.info(`Lock created successfully for class: '${className}'`);
|
|
150
|
-
}).catch(error => {
|
|
151
|
-
fnLogger.error(`Error creating lock for '${className}': ${error}`);
|
|
152
|
-
});
|
|
153
|
-
};
|
|
154
|
-
/**
|
|
155
|
-
* @description When a class model propagation comes to completion remove the corresponding
|
|
156
|
-
* ~lock from the database
|
|
157
|
-
* @param event
|
|
158
|
-
*/
|
|
159
|
-
this.onClassModelPropagationComplete = (event) => {
|
|
160
|
-
const fnLogger = logger.child({ method: "onClassModelPropagationComplete", args: { event } });
|
|
161
|
-
const className = event.detail.className;
|
|
162
|
-
this.clearClassLock(className).then(() => {
|
|
163
|
-
fnLogger.info(`Lock removed successfully for class: '${className}'`);
|
|
164
|
-
}).catch(error => {
|
|
165
|
-
fnLogger.error(`Error removing lock for '${className}': ${error}`);
|
|
166
|
-
});
|
|
167
|
-
;
|
|
168
|
-
};
|
|
169
|
-
/**
|
|
170
|
-
* @returns PouchDB.Core.Changes<{}>
|
|
171
|
-
*/
|
|
172
|
-
this.onClassModelChanges = () => {
|
|
173
|
-
const fnLogger = logger.child({ listener: "classModelChanges" });
|
|
174
|
-
const classModelChanges = this.db.changes({
|
|
175
|
-
since: 'now',
|
|
176
|
-
live: true,
|
|
177
|
-
include_docs: true,
|
|
178
|
-
filter: (doc) => {
|
|
179
|
-
return doc["~class"] == "class";
|
|
180
|
-
}
|
|
181
|
-
}).on("change", async (change) => {
|
|
182
|
-
const doc = change.doc;
|
|
183
|
-
if (doc && isClassModel(doc) && doc.active) {
|
|
184
|
-
const className = doc.name;
|
|
185
|
-
// Invalidate cached version if present
|
|
186
|
-
fnLogger.info(`Class model was updated. Clearing '${className}' from cache.`);
|
|
187
|
-
delete this.cache[className];
|
|
188
|
-
fnLogger.info(`Successfully cleared '${className}' from cache.`);
|
|
189
|
-
}
|
|
190
|
-
else if (doc && isClassModel(doc) && !doc.active) {
|
|
191
|
-
const className = doc.name;
|
|
192
|
-
fnLogger.info(`Class was deleted. Removing from '${className} from cache.'`);
|
|
193
|
-
} // else
|
|
194
|
-
});
|
|
195
|
-
return classModelChanges;
|
|
196
|
-
};
|
|
197
|
-
this.onClassLock = (className) => {
|
|
198
|
-
const classLockListener = this.db.changes({
|
|
199
|
-
since: 'now',
|
|
200
|
-
live: true,
|
|
201
|
-
include_docs: true,
|
|
202
|
-
filter: (doc) => {
|
|
203
|
-
return doc["~class"] == "~lock" && doc._id == `~lock-propagation-${className}`;
|
|
204
|
-
}
|
|
205
|
-
});
|
|
206
|
-
this.listeners.push(classLockListener);
|
|
207
|
-
return classLockListener;
|
|
208
|
-
};
|
|
209
|
-
this.addClassLock = async (className) => {
|
|
210
|
-
const fnLogger = logger.child({ method: "addClassLock", args: { className } });
|
|
211
|
-
try {
|
|
212
|
-
const existing = await this.db.get(`~lock-propagation-${className}`);
|
|
213
|
-
let _rev = undefined;
|
|
214
|
-
if (existing) {
|
|
215
|
-
_rev = existing._rev;
|
|
216
|
-
}
|
|
217
|
-
const response = await this.db.put({
|
|
218
|
-
_id: `~lock-propagation-${className}`,
|
|
219
|
-
"~class": `~Lock`,
|
|
220
|
-
_rev
|
|
221
|
-
});
|
|
222
|
-
fnLogger.info(`Adding class lock response`, { response });
|
|
223
|
-
return response.ok;
|
|
224
|
-
}
|
|
225
|
-
catch (e) {
|
|
226
|
-
fnLogger.error(`Error while adding class lock: ${e}`);
|
|
227
|
-
return false;
|
|
228
|
-
}
|
|
229
|
-
};
|
|
230
|
-
this.clearClassLock = async (className) => {
|
|
231
|
-
const fnLogger = logger.child({ method: "clearClassLock", args: { className } });
|
|
232
|
-
try {
|
|
233
|
-
const doc = await this.db.get(`~lock-propagation-${className}`);
|
|
234
|
-
fnLogger.info(`Fetched class lock`, { document: doc });
|
|
235
|
-
const response = await this.db.remove(doc);
|
|
236
|
-
fnLogger.info(`Removing class lock response`, { response });
|
|
237
|
-
return response.ok;
|
|
238
|
-
}
|
|
239
|
-
catch (e) {
|
|
240
|
-
fnLogger.error(`Error while adding class lock: ${e}`);
|
|
241
|
-
return false;
|
|
242
|
-
}
|
|
243
|
-
};
|
|
244
|
-
this.onClassDoc = (className) => {
|
|
245
|
-
const onClassDocListener = this.db.changes({
|
|
246
|
-
since: 'now',
|
|
247
|
-
live: true,
|
|
248
|
-
include_docs: true,
|
|
249
|
-
filter: (doc) => {
|
|
250
|
-
return doc["~class"] == className;
|
|
251
|
-
}
|
|
252
|
-
});
|
|
253
|
-
this.listeners.push(onClassDocListener);
|
|
254
|
-
return onClassDocListener;
|
|
255
|
-
};
|
|
256
|
-
this.close = () => {
|
|
257
|
-
this.removeAllListeners();
|
|
258
|
-
if (this.modelWorker)
|
|
259
|
-
this.modelWorker.terminate();
|
|
260
|
-
};
|
|
261
|
-
// TODO: Make the caching time configurable, and implement regular cleaning of cache
|
|
262
|
-
this.getClass = async (className, fresh = false) => {
|
|
263
|
-
const fnLogger = logger.child({ method: "getClass", args: { className, fresh } });
|
|
264
|
-
if (!fresh) {
|
|
265
|
-
// Check if class is in cache and not expired
|
|
266
|
-
if (this.cache[className] && Date.now() < this.cache[className].ttl) {
|
|
267
|
-
fnLogger.info("Retrieving class from cache", { ttl: this.cache[className].ttl });
|
|
268
|
-
return this.cache[className];
|
|
269
|
-
}
|
|
270
|
-
}
|
|
271
|
-
const classObj = await Class.fetch(this, className);
|
|
272
|
-
if (classObj) {
|
|
273
|
-
classObj.ttl = Date.now() + 60000 * 15; // 15 minutes expiration
|
|
274
|
-
this.cache[className] = classObj;
|
|
275
|
-
}
|
|
276
|
-
return classObj;
|
|
277
|
-
};
|
|
278
|
-
this.getDomain = async (domainName, fresh = false) => {
|
|
279
|
-
const fnLogger = logger.child({ method: "getDomain", args: { domainName, fresh } });
|
|
280
|
-
if (!fresh) {
|
|
281
|
-
if (this.cache[domainName] && Date.now() < this.cache[domainName].ttl) {
|
|
282
|
-
fnLogger.info("Retrieving domain from cache", { ttl: this.cache[domainName].ttl });
|
|
283
|
-
return this.cache[domainName];
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
const domainObj = await Domain.fetch(this, domainName);
|
|
287
|
-
if (domainObj) {
|
|
288
|
-
domainObj.ttl = Date.now() + 60000 * 15; // 15 minutes expiration
|
|
289
|
-
this.cache[domainName] = domainObj;
|
|
290
|
-
}
|
|
291
|
-
return domainObj;
|
|
292
|
-
};
|
|
293
|
-
// Expects a selector like { "~class": { $eq: "class" } }
|
|
294
|
-
this.findDocuments = async (selector, fields, skip, limit) => {
|
|
295
|
-
var _a;
|
|
296
|
-
const fnLogger = logger.child({ method: "findDocuments", args: { selector, fields, skip, limit } });
|
|
297
|
-
// By default request for only active documents
|
|
298
|
-
if (!selector.hasOwnProperty("active")) {
|
|
299
|
-
selector["active"] = true;
|
|
300
|
-
}
|
|
301
|
-
let indexFields = Object.keys(selector);
|
|
302
|
-
fnLogger.info("Produced index fields from selector", { indexFields });
|
|
303
|
-
let result = {
|
|
304
|
-
docs: []
|
|
305
|
-
};
|
|
306
|
-
try {
|
|
307
|
-
// [TODO] This breaks find method and even db!!
|
|
308
|
-
// let indexResult = await this.db.createIndex({
|
|
309
|
-
// index: { fields: indexFields }
|
|
310
|
-
// });
|
|
311
|
-
// fnLogger.info("Index result", indexResult);
|
|
312
|
-
let foundResult = await this.db.find({
|
|
313
|
-
selector: selector,
|
|
314
|
-
fields: fields,
|
|
315
|
-
skip: skip,
|
|
316
|
-
limit: limit
|
|
317
|
-
});
|
|
318
|
-
if (selector.hasOwnProperty("username")) {
|
|
319
|
-
console.log("Found result", { result: foundResult, selector });
|
|
320
|
-
}
|
|
321
|
-
fnLogger.info("Found", {
|
|
322
|
-
result: foundResult,
|
|
323
|
-
selector: selector,
|
|
324
|
-
});
|
|
325
|
-
const readableDocs = [];
|
|
326
|
-
for (const doc of foundResult.docs) {
|
|
327
|
-
const canRead = await this.policyEngine.isReadableDocument(doc);
|
|
328
|
-
if (!canRead) {
|
|
329
|
-
fnLogger.info("Based on policies, document is not readable", { docId: doc._id, docClass: doc["~class"] });
|
|
330
|
-
console.log("Based on policies, document is not readable", { docId: doc._id, docClass: doc["~class"] });
|
|
331
|
-
continue;
|
|
332
|
-
}
|
|
333
|
-
const encryptedKeys = this.cryptoEngine.identifyEncryptedKeys(doc);
|
|
334
|
-
const classObj = encryptedKeys.length || (fields && fields.length)
|
|
335
|
-
? (_a = (await this.getClass(doc["~class"], true))) !== null && _a !== void 0 ? _a : undefined
|
|
336
|
-
: undefined;
|
|
337
|
-
const processedDoc = await this.processReadableDocument(doc, classObj, fields, encryptedKeys);
|
|
338
|
-
if (processedDoc) {
|
|
339
|
-
readableDocs.push(processedDoc);
|
|
340
|
-
}
|
|
341
|
-
}
|
|
342
|
-
result = { docs: readableDocs, selector, skip, limit };
|
|
343
|
-
return result;
|
|
344
|
-
}
|
|
345
|
-
catch (e) {
|
|
346
|
-
fnLogger.error("findDocument - error", e);
|
|
347
|
-
throw e;
|
|
348
|
-
}
|
|
349
|
-
};
|
|
350
|
-
this.getClassModel = async (className) => {
|
|
351
|
-
// TODO: understand whether to use name of _id field
|
|
352
|
-
let selector = {
|
|
353
|
-
$or: [
|
|
354
|
-
{ name: { $eq: className } },
|
|
355
|
-
{ _id: { $eq: className } }
|
|
356
|
-
],
|
|
357
|
-
// _id: { $eq: className },
|
|
358
|
-
"~class": { $in: ["class", "~self"] }
|
|
359
|
-
};
|
|
360
|
-
try {
|
|
361
|
-
let response = await this.db.find({ selector });
|
|
362
|
-
if (response == null)
|
|
363
|
-
return null;
|
|
364
|
-
let result = response.docs[0];
|
|
365
|
-
logger.info("getClassModel - result", { result: result });
|
|
366
|
-
return result;
|
|
367
|
-
}
|
|
368
|
-
catch (e) {
|
|
369
|
-
logger.info("getClassModel - error", e);
|
|
370
|
-
throw new Error(e);
|
|
371
|
-
}
|
|
372
|
-
};
|
|
373
|
-
this.getDomainModel = async (domainName) => {
|
|
374
|
-
let selector = {
|
|
375
|
-
"~class": { $eq: "domain" },
|
|
376
|
-
name: { $eq: domainName }
|
|
377
|
-
};
|
|
378
|
-
try {
|
|
379
|
-
let response = await this.findDocument(selector);
|
|
380
|
-
if (response == null)
|
|
381
|
-
return null;
|
|
382
|
-
let result = response;
|
|
383
|
-
logger.info("getDomainModel - result", { result: result });
|
|
384
|
-
return result;
|
|
385
|
-
}
|
|
386
|
-
catch (e) {
|
|
387
|
-
logger.info("getDomainModel - error", e);
|
|
388
|
-
throw new Error(e);
|
|
389
|
-
}
|
|
390
|
-
};
|
|
391
|
-
// TODO: move listener to stack field, for easier un-registering
|
|
392
|
-
// TODO: Change into getClass("Class").getCards()
|
|
393
|
-
this.getClassModels = async (conf = {}) => {
|
|
394
|
-
const { listen, filter, search } = conf;
|
|
395
|
-
const selector = { "~class": { $eq: "class" } };
|
|
396
|
-
if (Array.isArray(filter) && filter.length > 0) {
|
|
397
|
-
// TODO: Consider checking against name field instead of _id
|
|
398
|
-
selector._id = { $in: filter };
|
|
399
|
-
}
|
|
400
|
-
// Case 2: A search query (partial match)
|
|
401
|
-
else if (search && typeof search === 'string') {
|
|
402
|
-
// Mango doesn’t have full regex support, so we use $regex via the pouchdb-find plugin.
|
|
403
|
-
selector.$or = [
|
|
404
|
-
{ _id: { $regex: RegExp(search, "i") } },
|
|
405
|
-
{ name: { $regex: RegExp(search, "i") } },
|
|
406
|
-
{ description: { $regex: RegExp(search, "i") } }
|
|
407
|
-
];
|
|
408
|
-
}
|
|
409
|
-
const fields = ['_id', 'name', 'description', 'schema', '~class', '_rev'];
|
|
410
|
-
const response = await this.findDocuments(selector, fields);
|
|
411
|
-
const result = response.docs;
|
|
412
|
-
if (!conf.listen) {
|
|
413
|
-
return { list: result };
|
|
414
|
-
}
|
|
415
|
-
// Create a live listener via PouchDB changes feed
|
|
416
|
-
const listener = this.db.changes({
|
|
417
|
-
since: 'now',
|
|
418
|
-
live: true,
|
|
419
|
-
include_docs: true,
|
|
420
|
-
selector
|
|
421
|
-
});
|
|
422
|
-
this.listeners.push(listener);
|
|
423
|
-
return {
|
|
424
|
-
list: result,
|
|
425
|
-
listener
|
|
426
|
-
};
|
|
427
|
-
};
|
|
428
|
-
this.getClasses = async (conf) => {
|
|
429
|
-
const classNames = conf.filter;
|
|
430
|
-
const searchFilter = conf.search;
|
|
431
|
-
const fnLogger = logger.child({ method: "getClasses" });
|
|
432
|
-
fnLogger.info("Requesting");
|
|
433
|
-
const { list: classModels, listener } = await this.getClassModels({
|
|
434
|
-
listen: true, filter: classNames, search: searchFilter
|
|
435
|
-
});
|
|
436
|
-
fnLogger.info("Received class models", { classModels });
|
|
437
|
-
const classList = [];
|
|
438
|
-
// Get current class list
|
|
439
|
-
for (const classModel of classModels) {
|
|
440
|
-
fnLogger.info(`Building class "${classModel.name}"`);
|
|
441
|
-
const classObj = await Class.buildFromModel(this, classModel);
|
|
442
|
-
classList.push(classObj);
|
|
443
|
-
}
|
|
444
|
-
// Queue for occasional addition/deletion
|
|
445
|
-
if (listener) {
|
|
446
|
-
listener.on("change", async (change) => {
|
|
447
|
-
if (!change.deleted) {
|
|
448
|
-
const className = change.id;
|
|
449
|
-
fnLogger.info(`Received class model change with "${className}"`);
|
|
450
|
-
const existingIndex = classList.findIndex(c => c.model._id === className);
|
|
451
|
-
const classObj = await Class.buildFromModel(this, change.doc);
|
|
452
|
-
if (existingIndex === -1) {
|
|
453
|
-
classList.push(classObj);
|
|
454
|
-
}
|
|
455
|
-
else {
|
|
456
|
-
classList[existingIndex] = classObj;
|
|
457
|
-
}
|
|
458
|
-
const evt = new CustomEvent("classListChange", { detail: classList });
|
|
459
|
-
this.dispatchEvent(evt);
|
|
460
|
-
}
|
|
461
|
-
else {
|
|
462
|
-
// remove from classList without altering the array reference
|
|
463
|
-
const idx = classList.findIndex(c => c.model._id === change.id);
|
|
464
|
-
if (idx !== -1) {
|
|
465
|
-
classList.splice(idx, 1);
|
|
466
|
-
const evt = new CustomEvent("classListChange", { detail: classList });
|
|
467
|
-
this.dispatchEvent(evt);
|
|
468
|
-
}
|
|
469
|
-
}
|
|
470
|
-
});
|
|
471
|
-
}
|
|
472
|
-
fnLogger.info("Completed inital classes build");
|
|
473
|
-
return classList;
|
|
474
|
-
};
|
|
475
|
-
this.getDomainModels = async (conf = {}) => {
|
|
476
|
-
const { listen, filter, search } = conf;
|
|
477
|
-
const selector = { "~class": { $eq: "domain" } };
|
|
478
|
-
if (Array.isArray(filter) && filter.length > 0) {
|
|
479
|
-
// TODO: Consider checking against name field instead of _id
|
|
480
|
-
selector._id = { $in: filter };
|
|
481
|
-
}
|
|
482
|
-
// Case 2: A search query (partial match)
|
|
483
|
-
else if (search && typeof search === 'string') {
|
|
484
|
-
// Mango doesn’t have full regex support, so we use $regex via the pouchdb-find plugin.
|
|
485
|
-
selector.$or = [
|
|
486
|
-
{ _id: { $regex: RegExp(search, "i") } },
|
|
487
|
-
{ name: { $regex: RegExp(search, "i") } },
|
|
488
|
-
{ description: { $regex: RegExp(search, "i") } }
|
|
489
|
-
];
|
|
490
|
-
}
|
|
491
|
-
const fields = ['_id', 'name', 'description', 'schema', '~class', '_rev'];
|
|
492
|
-
const response = await this.findDocuments(selector, fields);
|
|
493
|
-
const result = response.docs;
|
|
494
|
-
if (!conf.listen) {
|
|
495
|
-
return { list: result };
|
|
496
|
-
}
|
|
497
|
-
// Create a live listener via PouchDB changes feed
|
|
498
|
-
const listener = this.db.changes({
|
|
499
|
-
since: 'now',
|
|
500
|
-
live: true,
|
|
501
|
-
include_docs: true,
|
|
502
|
-
selector
|
|
503
|
-
});
|
|
504
|
-
return {
|
|
505
|
-
list: result,
|
|
506
|
-
listener
|
|
507
|
-
};
|
|
508
|
-
};
|
|
509
|
-
this.getDomains = async (conf) => {
|
|
510
|
-
const classNames = conf.filter;
|
|
511
|
-
const searchFilter = conf.search;
|
|
512
|
-
const fnLogger = logger.child({ method: "getDomains" });
|
|
513
|
-
fnLogger.info("Requesting");
|
|
514
|
-
const { list: domainModels, listener } = await this.getDomainModels({
|
|
515
|
-
listen: true, filter: classNames, search: searchFilter
|
|
516
|
-
});
|
|
517
|
-
fnLogger.info("Received class models", { domainModels });
|
|
518
|
-
const domainList = [];
|
|
519
|
-
// Get current class list
|
|
520
|
-
for (const domainModel of domainModels) {
|
|
521
|
-
fnLogger.info(`Building class "${domainModel.name}"`);
|
|
522
|
-
const domain = await Domain.buildFromModel(this, domainModel);
|
|
523
|
-
domainList.push(domain);
|
|
524
|
-
}
|
|
525
|
-
// Queue for occasional addition/deletion
|
|
526
|
-
if (listener) {
|
|
527
|
-
listener.on("change", async (change) => {
|
|
528
|
-
if (!change.deleted) {
|
|
529
|
-
const domainName = change.id;
|
|
530
|
-
fnLogger.info(`Received class model change with "${domainName}"`);
|
|
531
|
-
const existingIndex = domainList.findIndex(c => c.model._id === domainName);
|
|
532
|
-
const domain = await Domain.buildFromModel(this, change.doc);
|
|
533
|
-
if (existingIndex === -1) {
|
|
534
|
-
domainList.push(domain);
|
|
535
|
-
}
|
|
536
|
-
else {
|
|
537
|
-
domainList[existingIndex] = domain;
|
|
538
|
-
}
|
|
539
|
-
const evt = new CustomEvent("domainListChange", { detail: domainList });
|
|
540
|
-
this.dispatchEvent(evt);
|
|
541
|
-
}
|
|
542
|
-
else {
|
|
543
|
-
// remove from classList without altering the array reference
|
|
544
|
-
const idx = domainList.findIndex(c => c.model._id === change.id);
|
|
545
|
-
if (idx !== -1) {
|
|
546
|
-
domainList.splice(idx, 1);
|
|
547
|
-
const evt = new CustomEvent("domainListChange", { detail: domainList });
|
|
548
|
-
this.dispatchEvent(evt);
|
|
549
|
-
}
|
|
550
|
-
}
|
|
551
|
-
});
|
|
552
|
-
}
|
|
553
|
-
fnLogger.info("Completed inital domains build");
|
|
554
|
-
return domainList;
|
|
555
|
-
};
|
|
556
|
-
this.addClass = async (classObj) => {
|
|
557
|
-
const fnLogger = logger.child({ method: "addClass", args: { class: classObj.name } });
|
|
558
|
-
const classOrigin = await this.getClass(classObj.type);
|
|
559
|
-
if (classOrigin == null) {
|
|
560
|
-
fnLogger.error("Class originator not found", { classType: classObj.type });
|
|
561
|
-
throw new Error(`Class originator ${classObj.type} not found in stack`);
|
|
562
|
-
}
|
|
563
|
-
let classModel = classObj.getModel();
|
|
564
|
-
fnLogger.info("Got class model", { classModel });
|
|
565
|
-
try {
|
|
566
|
-
const result = await classOrigin.addCard(classModel);
|
|
567
|
-
fnLogger.info("Added class card", { result });
|
|
568
|
-
await this.ensureDefaultPolicyForClass(result);
|
|
569
|
-
return result;
|
|
570
|
-
}
|
|
571
|
-
catch (e) {
|
|
572
|
-
fnLogger.error("Error adding class card", { error: e });
|
|
573
|
-
const message = (e === null || e === void 0 ? void 0 : e.message) || "Failed to add class card";
|
|
574
|
-
throw new Error(message);
|
|
575
|
-
}
|
|
576
|
-
// let existingDoc = await this.getClassModel(classModel.name);
|
|
577
|
-
// if ( existingDoc == null ) {
|
|
578
|
-
// let resultDoc = await this.createDoc(classModel.name, 'class', CLASS_SCHEMA, classModel);
|
|
579
|
-
// fnLogger.info("Result", {result: resultDoc});
|
|
580
|
-
// // TODO: Consider creating a design doc for easier filtering
|
|
581
|
-
// return resultDoc as ClassModel;
|
|
582
|
-
// } else {
|
|
583
|
-
// return existingDoc;
|
|
584
|
-
// }
|
|
585
|
-
};
|
|
586
|
-
this.addDomain = async (domainObj) => {
|
|
587
|
-
const fnLogger = logger.child({ method: "addDomain", args: { domain: domainObj.name } });
|
|
588
|
-
let domainModel = domainObj.getModel();
|
|
589
|
-
fnLogger.info("Got domain model", { domainModel });
|
|
590
|
-
let existingDoc = await this.getDomainModel(domainModel.name);
|
|
591
|
-
if (existingDoc == null) {
|
|
592
|
-
let resultDoc = await this.createDoc(domainModel.name, 'domain', DOMAIN_SCHEMA, domainModel);
|
|
593
|
-
fnLogger.info("Result", { result: resultDoc });
|
|
594
|
-
// TODO: Consider creating a design doc for easier filtering
|
|
595
|
-
return resultDoc;
|
|
596
|
-
}
|
|
597
|
-
else {
|
|
598
|
-
return existingDoc;
|
|
599
|
-
}
|
|
600
|
-
};
|
|
601
|
-
this.updateClass = async (classObj) => {
|
|
602
|
-
const fnLogger = logger.child({ method: "updateClass", args: { class: classObj.name } });
|
|
603
|
-
let result = await this.createDoc(classObj.getId(), 'class', classObj, classObj.getModel());
|
|
604
|
-
fnLogger.info("Result", result);
|
|
605
|
-
return result;
|
|
606
|
-
};
|
|
607
|
-
this.addDesignDocumentPKs = async (className, pKs, temp = false) => {
|
|
608
|
-
const fnLogger = logger.child({ method: 'addDesignDocumentPKs', args: { className, pKs } });
|
|
609
|
-
// Construct the compound key string dynamically
|
|
610
|
-
const keyString = pKs.map(key => `doc.${key}`).join(', ');
|
|
611
|
-
// The 'map' function as a string
|
|
612
|
-
const mapCode = `function (doc) {
|
|
613
|
-
const hasAllKeys = ${pKs.map(key => `doc.${key}`).join(' && ')};
|
|
614
|
-
if (hasAllKeys && doc["~class"] === '${className}') {
|
|
615
|
-
emit([${keyString}], doc._id);
|
|
616
|
-
}
|
|
617
|
-
}`;
|
|
618
|
-
fnLogger.info("Generated map code", { code: mapCode });
|
|
619
|
-
let designDocId = `_design/${className}-group`;
|
|
620
|
-
if (temp)
|
|
621
|
-
designDocId = `_design/${className}-group-temp`;
|
|
622
|
-
const ddoc = {
|
|
623
|
-
_id: designDocId,
|
|
624
|
-
views: {
|
|
625
|
-
'by_pKeys': {
|
|
626
|
-
map: mapCode
|
|
627
|
-
}
|
|
628
|
-
},
|
|
629
|
-
_rev: undefined,
|
|
630
|
-
};
|
|
631
|
-
fnLogger.info("Prepared design document", { ddoc });
|
|
632
|
-
try {
|
|
633
|
-
// Use 'get' to check if the design doc already exists
|
|
634
|
-
const existingDoc = await this.db.get(designDocId);
|
|
635
|
-
ddoc._rev = existingDoc._rev; // Add _rev to update the existing doc
|
|
636
|
-
await this.db.put(ddoc);
|
|
637
|
-
fnLogger.info('Design document updated successfully.');
|
|
638
|
-
}
|
|
639
|
-
catch (err) {
|
|
640
|
-
if (err.name === 'not_found') {
|
|
641
|
-
// Doc doesn't exist, create it
|
|
642
|
-
await this.db.put(ddoc);
|
|
643
|
-
fnLogger.info('Design document created successfully.');
|
|
644
|
-
}
|
|
645
|
-
else {
|
|
646
|
-
fnLogger.error('Error saving design document:', err);
|
|
647
|
-
throw err;
|
|
648
|
-
}
|
|
649
|
-
}
|
|
650
|
-
return designDocId;
|
|
651
|
-
};
|
|
652
|
-
this.createDoc = async (docId, type, classObj, params) => {
|
|
653
|
-
var _a;
|
|
654
|
-
const fnLogger = logger.child({ method: "createDoc", args: { docId, type, params } });
|
|
655
|
-
fnLogger.info("Creating document");
|
|
656
|
-
let schema = {};
|
|
657
|
-
if (classObj instanceof Class) {
|
|
658
|
-
schema = classObj.buildSchema();
|
|
659
|
-
}
|
|
660
|
-
else {
|
|
661
|
-
schema = classObj;
|
|
662
|
-
}
|
|
663
|
-
let db = this.db, doc = null, isNewDoc = false, newDocId = "";
|
|
664
|
-
try {
|
|
665
|
-
if (docId) {
|
|
666
|
-
const existingDoc = await this.getDocument(docId);
|
|
667
|
-
fnLogger.info("Retrieved doc", { existingDoc });
|
|
668
|
-
// console.log("Existing doc", {existingDoc, params})
|
|
669
|
-
if (existingDoc && existingDoc["~class"] === type) {
|
|
670
|
-
fnLogger.info("Assigning existing doc", { doc: existingDoc });
|
|
671
|
-
doc = Object.assign({}, existingDoc);
|
|
672
|
-
}
|
|
673
|
-
else if (existingDoc && existingDoc["~class"] !== type) {
|
|
674
|
-
fnLogger.error("Existing document type differs");
|
|
675
|
-
throw new Error("createDoc - Existing document type differs");
|
|
676
|
-
}
|
|
677
|
-
else {
|
|
678
|
-
isNewDoc = true;
|
|
679
|
-
newDocId = docId;
|
|
680
|
-
doc = this.prepareDoc(newDocId, type, params, "~class");
|
|
681
|
-
}
|
|
682
|
-
}
|
|
683
|
-
else {
|
|
684
|
-
isNewDoc = true;
|
|
685
|
-
newDocId = `${type}-${(this.lastDocId + 1)}`;
|
|
686
|
-
doc = this.prepareDoc(newDocId, type, params, "~class");
|
|
687
|
-
fnLogger.info("Generated docId", { newDocId });
|
|
688
|
-
}
|
|
689
|
-
fnLogger.info("Doc BEFORE elaboration (i.e. merge)", { doc, params });
|
|
690
|
-
let doc_ = Object.assign(Object.assign(Object.assign({}, doc), params), { _rev: doc._rev, "~updateTimestamp": new Date().getTime() });
|
|
691
|
-
if (type === "~User" || type === "User") {
|
|
692
|
-
const groups = doc_.groupId;
|
|
693
|
-
if (!groups || (Array.isArray(groups) && groups.length === 0)) {
|
|
694
|
-
doc_.groupId = ["Group-Default"];
|
|
695
|
-
}
|
|
696
|
-
}
|
|
697
|
-
if (type === "~UserSession" || type === "UserSession") {
|
|
698
|
-
let sessionGroups = doc_.groupId;
|
|
699
|
-
if (!sessionGroups || (Array.isArray(sessionGroups) && sessionGroups.length === 0)) {
|
|
700
|
-
const sessionUserId = doc_.userId;
|
|
701
|
-
if (sessionUserId) {
|
|
702
|
-
const relatedUser = await this.getDocument(sessionUserId).catch(() => null);
|
|
703
|
-
if (relatedUser === null || relatedUser === void 0 ? void 0 : relatedUser.groupId) {
|
|
704
|
-
sessionGroups = relatedUser.groupId;
|
|
705
|
-
}
|
|
706
|
-
}
|
|
707
|
-
if (!sessionGroups || (Array.isArray(sessionGroups) && sessionGroups.length === 0)) {
|
|
708
|
-
sessionGroups = ["Group-Default"];
|
|
709
|
-
}
|
|
710
|
-
doc_.groupId = sessionGroups;
|
|
711
|
-
}
|
|
712
|
-
}
|
|
713
|
-
if ((_a = doc_["~class"]) === null || _a === void 0 ? void 0 : _a.startsWith("Account-")) {
|
|
714
|
-
// console.log("Doc after merge", { doc_ })
|
|
715
|
-
}
|
|
716
|
-
fnLogger.info("Doc AFTER elaboration (i.e. merge)", { doc_ });
|
|
717
|
-
await this.policyEngine.ensureWriteAllowed(type, doc_);
|
|
718
|
-
let response = await db.put(doc_);
|
|
719
|
-
// Find me
|
|
720
|
-
fnLogger.info("Response after put", { "response": response });
|
|
721
|
-
if (response.ok && isNewDoc) {
|
|
722
|
-
await this.incrementLastDocId();
|
|
723
|
-
docId = response.id;
|
|
724
|
-
}
|
|
725
|
-
else if (response.ok) {
|
|
726
|
-
docId = response.id;
|
|
727
|
-
}
|
|
728
|
-
else {
|
|
729
|
-
fnLogger.error("Error, check logs", { "response": response });
|
|
730
|
-
throw new Error("createDoc - Error, check logs");
|
|
731
|
-
}
|
|
732
|
-
}
|
|
733
|
-
catch (e) {
|
|
734
|
-
if (e.name === 'conflict') {
|
|
735
|
-
fnLogger.info("Conflict! Ignoring..");
|
|
736
|
-
// TODO: Handle conflict!
|
|
737
|
-
}
|
|
738
|
-
else {
|
|
739
|
-
fnLogger.info("Problem while putting doc", {
|
|
740
|
-
"error": e,
|
|
741
|
-
"document": doc
|
|
742
|
-
});
|
|
743
|
-
throw new Error("createDoc - Problem while putting doc" + e);
|
|
744
|
-
}
|
|
745
|
-
}
|
|
746
|
-
return doc;
|
|
747
|
-
};
|
|
748
|
-
this.createDocs = async (docs, type, classObj) => {
|
|
749
|
-
const fnLogger = logger.child({ method: "createDocs", args: { docs } });
|
|
750
|
-
let schema = {};
|
|
751
|
-
if (classObj instanceof Class) {
|
|
752
|
-
schema = classObj.buildSchema();
|
|
753
|
-
}
|
|
754
|
-
else {
|
|
755
|
-
schema = classObj;
|
|
756
|
-
}
|
|
757
|
-
fnLogger.info("Determined schema", { schema });
|
|
758
|
-
let db = this.db;
|
|
759
|
-
const documents = [];
|
|
760
|
-
let newDocsIds = [];
|
|
761
|
-
for (const draft of docs) {
|
|
762
|
-
let { docId, params } = draft;
|
|
763
|
-
let doc = null;
|
|
764
|
-
let isNewDoc = false;
|
|
765
|
-
try {
|
|
766
|
-
if (docId) {
|
|
767
|
-
const existingDoc = await this.getDocument(docId);
|
|
768
|
-
fnLogger.info("retrieved doc", { existingDoc });
|
|
769
|
-
if (existingDoc && existingDoc["~class"] === type) {
|
|
770
|
-
fnLogger.info("createDocs - assigning existing doc", { doc: existingDoc });
|
|
771
|
-
doc = Object.assign({}, existingDoc);
|
|
772
|
-
}
|
|
773
|
-
else if (existingDoc && existingDoc["~class"] !== type) {
|
|
774
|
-
throw new Error("createDocs - Existing document type differs");
|
|
775
|
-
}
|
|
776
|
-
else {
|
|
777
|
-
isNewDoc = true;
|
|
778
|
-
doc = this.prepareDoc(docId, type, params, "~class");
|
|
779
|
-
}
|
|
780
|
-
}
|
|
781
|
-
else {
|
|
782
|
-
docId = `${type}-${(this.lastDocId + 1)}`;
|
|
783
|
-
doc = this.prepareDoc(docId, type, params, "~class");
|
|
784
|
-
isNewDoc = true;
|
|
785
|
-
fnLogger.info("Generated docId", docId);
|
|
786
|
-
}
|
|
787
|
-
fnLogger.info("Doc BEFORE elaboration (i.e. merge)", { doc, params });
|
|
788
|
-
const doc_ = Object.assign(Object.assign(Object.assign({}, doc), params), { _id: docId, _rev: doc._rev, "~updateTimestamp": new Date().getTime() });
|
|
789
|
-
fnLogger.info("Doc AFTER elaboration (i.e. merge)", { doc_ });
|
|
790
|
-
await this.policyEngine.ensureWriteAllowed(type, doc_);
|
|
791
|
-
documents.push(doc_);
|
|
792
|
-
if (isNewDoc)
|
|
793
|
-
newDocsIds.push(docId);
|
|
794
|
-
}
|
|
795
|
-
catch (e) {
|
|
796
|
-
fnLogger.error("createDocs - Problem while preparing doc", {
|
|
797
|
-
"error": e,
|
|
798
|
-
"document": doc
|
|
799
|
-
});
|
|
800
|
-
throw new Error("createDocs - Problem while preparing doc" + e);
|
|
801
|
-
}
|
|
802
|
-
}
|
|
803
|
-
try {
|
|
804
|
-
const response = await db.bulkDocs(documents);
|
|
805
|
-
fnLogger.info("Response after bulkDocs", { "response": response });
|
|
806
|
-
// Increment lastDocId based on number of new docs created
|
|
807
|
-
const newDocsCount = response.filter(res => res.id != null && newDocsIds.includes(res.id)).length;
|
|
808
|
-
fnLogger.info(`Successfully created ${newDocsCount} new documents.`);
|
|
809
|
-
for (let i = 0; i < newDocsCount; i++) {
|
|
810
|
-
await this.incrementLastDocId();
|
|
811
|
-
}
|
|
812
|
-
}
|
|
813
|
-
catch (e) {
|
|
814
|
-
fnLogger.error("createDocs - Problem while putting docs", {
|
|
815
|
-
"error": e,
|
|
816
|
-
"documents": documents
|
|
817
|
-
});
|
|
818
|
-
throw new Error("createDocs - Problem while putting docs" + e);
|
|
819
|
-
}
|
|
820
|
-
return documents;
|
|
821
|
-
};
|
|
822
|
-
this.createRelationDoc = async (docId, relationName, domainObj, params) => {
|
|
823
|
-
const fnLogger = logger.child({ method: "createRelationDoc", args: { docId, relationName, params } });
|
|
824
|
-
fnLogger.info("Creating relation document");
|
|
825
|
-
let db = this.db, doc = null, isNewDoc = false;
|
|
826
|
-
try {
|
|
827
|
-
if (docId) {
|
|
828
|
-
const existingDoc = await this.db.get(docId);
|
|
829
|
-
fnLogger.info("retrieved doc", { existingDoc });
|
|
830
|
-
if (existingDoc && existingDoc["~domain"] === domainObj.name) {
|
|
831
|
-
fnLogger.info("Assigning existing doc", { doc: existingDoc });
|
|
832
|
-
doc = Object.assign({}, existingDoc);
|
|
833
|
-
}
|
|
834
|
-
else if (existingDoc && existingDoc["~domain"] !== domainObj.name) {
|
|
835
|
-
fnLogger.error("Existing document type differs");
|
|
836
|
-
throw new Error("createDoc - Existing document type differs");
|
|
837
|
-
}
|
|
838
|
-
else {
|
|
839
|
-
fnLogger.warn("No relation document");
|
|
840
|
-
isNewDoc = true;
|
|
841
|
-
doc = this.prepareDoc(docId, domainObj.name, params, "~domain");
|
|
842
|
-
}
|
|
843
|
-
}
|
|
844
|
-
else {
|
|
845
|
-
docId = `${domainObj.name}-${(this.lastDocId + 1)}`;
|
|
846
|
-
doc = this.prepareDoc(docId, domainObj.name, params, "~domain");
|
|
847
|
-
isNewDoc = true;
|
|
848
|
-
fnLogger.info("Generated docId", docId);
|
|
849
|
-
}
|
|
850
|
-
fnLogger.info("Doc BEFORE elaboration (i.e. merge)", { doc, params });
|
|
851
|
-
const doc_ = Object.assign(Object.assign(Object.assign({}, doc), params), { _id: docId, _rev: doc._rev, "~updateTimestamp": new Date().getTime() });
|
|
852
|
-
fnLogger.info("Doc AFTER elaboration (i.e. merge)", { doc_ });
|
|
853
|
-
let response = await db.put(doc_);
|
|
854
|
-
fnLogger.info("Response after put", { "response": response });
|
|
855
|
-
if (response.ok && isNewDoc) {
|
|
856
|
-
await this.incrementLastDocId();
|
|
857
|
-
docId = response.id;
|
|
858
|
-
}
|
|
859
|
-
else if (response.ok) {
|
|
860
|
-
docId = response.id;
|
|
861
|
-
}
|
|
862
|
-
else {
|
|
863
|
-
fnLogger.error("Error, check logs", { "response": response });
|
|
864
|
-
throw new Error("createDoc - Error, check logs");
|
|
865
|
-
}
|
|
866
|
-
}
|
|
867
|
-
catch (e) {
|
|
868
|
-
fnLogger.error("Error while creating relation document", { error: e });
|
|
869
|
-
}
|
|
870
|
-
return doc;
|
|
871
|
-
};
|
|
872
|
-
this.createRelationDocs = async (docs, relationName, domainObj) => {
|
|
873
|
-
const fnLogger = logger.child({ method: "createRelationDocs", args: { docs, relationName } });
|
|
874
|
-
let db = this.db;
|
|
875
|
-
const documents = [];
|
|
876
|
-
let newDocsIds = [];
|
|
877
|
-
for (const draft of docs) {
|
|
878
|
-
let { docId, params } = draft;
|
|
879
|
-
let doc = null;
|
|
880
|
-
let isNewDoc = false;
|
|
881
|
-
try {
|
|
882
|
-
if (docId) {
|
|
883
|
-
const existingDoc = await db.get(docId);
|
|
884
|
-
fnLogger.info("retrieved doc", { existingDoc });
|
|
885
|
-
if (existingDoc && existingDoc["~domain"] === domainObj.name) {
|
|
886
|
-
fnLogger.info("createRelationDocs - assigning existing doc", { doc: existingDoc });
|
|
887
|
-
doc = Object.assign({}, existingDoc);
|
|
888
|
-
}
|
|
889
|
-
else if (existingDoc && existingDoc["~domain"] !== domainObj.name) {
|
|
890
|
-
throw new Error("createRelationDocs - Existing document type differs");
|
|
891
|
-
}
|
|
892
|
-
else {
|
|
893
|
-
isNewDoc = true;
|
|
894
|
-
doc = this.prepareDoc(docId, domainObj.name, params, "~domain");
|
|
895
|
-
}
|
|
896
|
-
}
|
|
897
|
-
else {
|
|
898
|
-
docId = `${domainObj.name}-${(this.lastDocId + 1)}`;
|
|
899
|
-
doc = this.prepareDoc(docId, domainObj.name, params, "~domain");
|
|
900
|
-
isNewDoc = true;
|
|
901
|
-
fnLogger.info("Generated docId", docId);
|
|
902
|
-
}
|
|
903
|
-
fnLogger.info("Doc BEFORE elaboration (i.e. merge)", { doc, params });
|
|
904
|
-
const doc_ = Object.assign(Object.assign(Object.assign({}, doc), params), { _id: docId, _rev: doc._rev, "~updateTimestamp": new Date().getTime() });
|
|
905
|
-
fnLogger.info("Doc AFTER elaboration (i.e. merge)", { doc_ });
|
|
906
|
-
documents.push(doc_);
|
|
907
|
-
if (isNewDoc)
|
|
908
|
-
newDocsIds.push(docId);
|
|
909
|
-
}
|
|
910
|
-
catch (e) {
|
|
911
|
-
fnLogger.error("createRelationDocs - Problem while preparing doc", {
|
|
912
|
-
"error": e,
|
|
913
|
-
"document": doc
|
|
914
|
-
});
|
|
915
|
-
throw new Error("createRelationDocs - Problem while preparing doc" + e);
|
|
916
|
-
}
|
|
917
|
-
}
|
|
918
|
-
try {
|
|
919
|
-
// console.log("Documents to be created", {documents});
|
|
920
|
-
const response = await db.bulkDocs(documents);
|
|
921
|
-
fnLogger.info("Response after bulkDocs", { "response": response });
|
|
922
|
-
// Increment lastDocId based on number of new docs created
|
|
923
|
-
const newDocsCount = response.filter(res => res.id != null && newDocsIds.includes(res.id)).length;
|
|
924
|
-
fnLogger.info(`Successfully created ${newDocsCount} new documents.`);
|
|
925
|
-
for (let i = 0; i < newDocsCount; i++) {
|
|
926
|
-
await this.incrementLastDocId();
|
|
927
|
-
}
|
|
928
|
-
}
|
|
929
|
-
catch (e) {
|
|
930
|
-
fnLogger.error("createRelationDocs - Problem while putting docs", {
|
|
931
|
-
"error": e,
|
|
932
|
-
"documents": documents
|
|
933
|
-
});
|
|
934
|
-
throw new Error("createRelationDocs - Problem while putting docs" + e);
|
|
935
|
-
}
|
|
936
|
-
return documents;
|
|
937
|
-
};
|
|
938
|
-
/**
|
|
939
|
-
* Sets the active param of a document to false
|
|
940
|
-
* @param _id
|
|
941
|
-
* @returns Promise<boolean>
|
|
942
|
-
*/
|
|
943
|
-
this.deleteDocument = async (_id) => {
|
|
944
|
-
const fnLogger = logger.child({ method: "deleteDocument", args: { _id } });
|
|
945
|
-
const doc = await this.db.get(_id);
|
|
946
|
-
if (doc) {
|
|
947
|
-
try {
|
|
948
|
-
const targetClass = doc["~class"];
|
|
949
|
-
await this.policyEngine.ensureWriteAllowed(targetClass, doc);
|
|
950
|
-
await this.db.put(Object.assign(Object.assign({}, doc), { active: false }));
|
|
951
|
-
return true;
|
|
952
|
-
}
|
|
953
|
-
catch (e) {
|
|
954
|
-
fnLogger.error(`Error while deleting document: ${e}`, { document: doc });
|
|
955
|
-
return false;
|
|
956
|
-
}
|
|
957
|
-
}
|
|
958
|
-
else {
|
|
959
|
-
fnLogger.error("Found no document with given id");
|
|
960
|
-
return false;
|
|
961
|
-
}
|
|
962
|
-
};
|
|
963
|
-
this.query = async (sql, ...params) => {
|
|
964
|
-
const fnLogger = logger.child({ method: "query", args: { sql, params } });
|
|
965
|
-
fnLogger.info("Executing query");
|
|
966
|
-
let astList = [];
|
|
967
|
-
try {
|
|
968
|
-
astList = parse(sql);
|
|
969
|
-
fnLogger.info("Produced AST", { astList });
|
|
970
|
-
}
|
|
971
|
-
catch (error) {
|
|
972
|
-
error.ast = astList.length > 0 ? astList[0] : null;
|
|
973
|
-
throw error;
|
|
974
|
-
}
|
|
975
|
-
// A UNION query is treated as a single execution, not a loop over ASTs.
|
|
976
|
-
if (astList.length > 0) {
|
|
977
|
-
try {
|
|
978
|
-
const plan = createPlan(astList);
|
|
979
|
-
const rows = await executePlan(this, plan, params);
|
|
980
|
-
// The AST for the whole query (including unions) is the list
|
|
981
|
-
fnLogger.info("Query executed successfully", { rows, astList });
|
|
982
|
-
return { rows, ast: astList };
|
|
983
|
-
}
|
|
984
|
-
catch (error) {
|
|
985
|
-
error.ast = astList; // Attach full AST list to error for debugging
|
|
986
|
-
throw error;
|
|
987
|
-
}
|
|
988
|
-
}
|
|
989
|
-
// Handle case where query is empty or only comments
|
|
990
|
-
return { rows: [], ast: null };
|
|
991
|
-
};
|
|
992
|
-
// Private constructor to prevent direct instantiation
|
|
993
|
-
this.cache = {};
|
|
994
|
-
}
|
|
995
|
-
async initialize(conn, options) {
|
|
996
|
-
// Store the connection string and options
|
|
997
|
-
this.connection = conn;
|
|
998
|
-
this.options = options;
|
|
999
|
-
this.cryptoEngineDisabled = Boolean(options === null || options === void 0 ? void 0 : options.disableCryptoEngine);
|
|
1000
|
-
if (options === null || options === void 0 ? void 0 : options.name) {
|
|
1001
|
-
this.name = options === null || options === void 0 ? void 0 : options.name;
|
|
1002
|
-
}
|
|
1003
|
-
const connRegExp = /(?<=db-).*/;
|
|
1004
|
-
const match = conn.match(connRegExp);
|
|
1005
|
-
if (match) {
|
|
1006
|
-
this.name = match[0];
|
|
1007
|
-
}
|
|
1008
|
-
else {
|
|
1009
|
-
this.name = conn;
|
|
1010
|
-
}
|
|
1011
|
-
// PouchDB.plugin((await import('pouchdb-adapter-node-websql')).default);
|
|
1012
|
-
// PouchDB.plugin((await import('pouchdb-adapter-websql')).default);
|
|
1013
|
-
// Load default plugins
|
|
1014
|
-
PouchDB.plugin(PouchDBFind);
|
|
1015
|
-
PouchDB.plugin(StackPlugin(PouchDB, this, conn));
|
|
1016
|
-
// Validation plugin
|
|
1017
|
-
if (options === null || options === void 0 ? void 0 : options.plugins) {
|
|
1018
|
-
for (let plugin of options.plugins) {
|
|
1019
|
-
PouchDB.plugin(plugin);
|
|
1020
|
-
}
|
|
1021
|
-
}
|
|
1022
|
-
this.db = new PouchDB(conn);
|
|
1023
|
-
this.cache = {
|
|
1024
|
-
// empty at init
|
|
1025
|
-
};
|
|
1026
|
-
this.jobEngine = new JobEngine(this);
|
|
1027
|
-
this.policyEngine = new PolicyEngine(this);
|
|
1028
|
-
this.cryptoEngine = new CryptoEngine(this);
|
|
1029
|
-
}
|
|
1030
|
-
getDb() {
|
|
1031
|
-
return this.db;
|
|
1032
|
-
}
|
|
1033
|
-
async getDbInfo() {
|
|
1034
|
-
return this.db.info();
|
|
1035
|
-
}
|
|
1036
|
-
getDbName() {
|
|
1037
|
-
return this.db.name;
|
|
1038
|
-
}
|
|
1039
|
-
isCryptoEngineDisabled() {
|
|
1040
|
-
return this.cryptoEngineDisabled;
|
|
1041
|
-
}
|
|
1042
|
-
setAuthSession(proof) {
|
|
1043
|
-
this.authSession = proof;
|
|
1044
|
-
}
|
|
1045
|
-
clearAuthSession() {
|
|
1046
|
-
this.authSession = undefined;
|
|
1047
|
-
this.cryptoEngine.setDocumentKey(null);
|
|
1048
|
-
}
|
|
1049
|
-
async ensureDefaultPolicyForClass(targetClass) {
|
|
1050
|
-
const fnLogger = logger.child({ method: "ensureDefaultPolicyForClass", targetClass: targetClass._id });
|
|
1051
|
-
const existingPolicy = await this.findDocument({
|
|
1052
|
-
"~class": { $eq: "~Policy" },
|
|
1053
|
-
targetClass: { $elemMatch: { $eq: targetClass._id } }
|
|
1054
|
-
});
|
|
1055
|
-
if (existingPolicy) {
|
|
1056
|
-
return;
|
|
1057
|
-
}
|
|
1058
|
-
const policyDoc = {
|
|
1059
|
-
_id: `Policy-${targetClass._id}`,
|
|
1060
|
-
"~class": "~Policy",
|
|
1061
|
-
rule: "return session && session.sessionStatus === 'active';",
|
|
1062
|
-
description: `Default policy for ${targetClass.name || targetClass._id}`,
|
|
1063
|
-
targetClass: [targetClass._id],
|
|
1064
|
-
};
|
|
1065
|
-
fnLogger.info("Creating default policy", { policyDoc });
|
|
1066
|
-
try {
|
|
1067
|
-
await this.db.bulkDocs([policyDoc]);
|
|
1068
|
-
}
|
|
1069
|
-
catch (error) {
|
|
1070
|
-
throw new Error(`Failed to create default policy for ${targetClass._id}: ${(error === null || error === void 0 ? void 0 : error.message) || error}`);
|
|
1071
|
-
}
|
|
1072
|
-
}
|
|
1073
|
-
// asynchronous factory method
|
|
1074
|
-
static async create(conn, options) {
|
|
1075
|
-
const stack = new ClientStack();
|
|
1076
|
-
await stack.initialize(conn, options);
|
|
1077
|
-
await stack.initdb();
|
|
1078
|
-
if ((options === null || options === void 0 ? void 0 : options.patches) && options.patches.length) {
|
|
1079
|
-
for (const patch of options.patches) {
|
|
1080
|
-
await stack.applyPatch(patch);
|
|
1081
|
-
// ClientStack.logger.info(`Applied patch '${patch._id}' to stack '${stack.name}'`);
|
|
1082
|
-
}
|
|
1083
|
-
}
|
|
1084
|
-
if (options === null || options === void 0 ? void 0 : options.credentials) {
|
|
1085
|
-
await stack.authenticate(options.credentials);
|
|
1086
|
-
}
|
|
1087
|
-
return stack;
|
|
1088
|
-
}
|
|
1089
|
-
async authenticate(credentials) {
|
|
1090
|
-
var _a, _b, _c;
|
|
1091
|
-
const { username, password } = credentials;
|
|
1092
|
-
const userQuery = await this.db.find({
|
|
1093
|
-
selector: {
|
|
1094
|
-
"~class": { $eq: "~User" },
|
|
1095
|
-
username: { $eq: username },
|
|
1096
|
-
active: { $eq: true }
|
|
1097
|
-
}
|
|
1098
|
-
});
|
|
1099
|
-
const user = userQuery.docs.length ? userQuery.docs[0] : null;
|
|
1100
|
-
if (!user) {
|
|
1101
|
-
throw new Error(`User '${username}' not found`);
|
|
1102
|
-
}
|
|
1103
|
-
const authModuleId = user.authMethod || "AuthMod-Classic";
|
|
1104
|
-
const authModule = await this.db.get(authModuleId);
|
|
1105
|
-
const jobId = authModule.jobId;
|
|
1106
|
-
const run = await this.jobEngine.executeJob(jobId, {
|
|
1107
|
-
password,
|
|
1108
|
-
salt: user.keyDerivationSalt,
|
|
1109
|
-
keyDerivationSalt: user.keyDerivationSalt,
|
|
1110
|
-
});
|
|
1111
|
-
const derivedKey = (_b = (_a = run.finalMetadata) === null || _a === void 0 ? void 0 : _a.derivedKey) !== null && _b !== void 0 ? _b : (_c = run.initialMetadata) === null || _c === void 0 ? void 0 : _c.derivedKey;
|
|
1112
|
-
const userGroups = Array.isArray(user.groupId)
|
|
1113
|
-
? user.groupId
|
|
1114
|
-
: user.groupId
|
|
1115
|
-
? [user.groupId]
|
|
1116
|
-
: ["Group-Default"];
|
|
1117
|
-
const randomBytes = new Uint8Array(8);
|
|
1118
|
-
globalThis.crypto.getRandomValues(randomBytes);
|
|
1119
|
-
const hexId = Array.from(randomBytes).map(b => b.toString(16).padStart(2, '0')).join('');
|
|
1120
|
-
const sessionId = `session-${globalThis.crypto.randomUUID ? globalThis.crypto.randomUUID() : hexId}`;
|
|
1121
|
-
const sessionDoc = {
|
|
1122
|
-
_id: sessionId,
|
|
1123
|
-
"~class": "~UserSession",
|
|
1124
|
-
userId: user._id || user.username,
|
|
1125
|
-
groupId: userGroups,
|
|
1126
|
-
username: user.username,
|
|
1127
|
-
sessionId,
|
|
1128
|
-
sessionStart: new Date().toISOString(),
|
|
1129
|
-
sessionStatus: "active",
|
|
1130
|
-
};
|
|
1131
|
-
const sessionClassModel = (await this.getClassModel("~UserSession")) || (await this.getClassModel("UserSession"));
|
|
1132
|
-
const sessionSchema = (sessionClassModel === null || sessionClassModel === void 0 ? void 0 : sessionClassModel.schema) || {};
|
|
1133
|
-
await this.createDoc(sessionDoc._id, sessionDoc["~class"], sessionSchema, sessionDoc);
|
|
1134
|
-
const documentKey = await this.cryptoEngine.unwrapAndStoreDocumentKey(user.wrappedDocumentKey, derivedKey);
|
|
1135
|
-
const proof = { session: sessionDoc, derivedKey, documentKey: documentKey !== null && documentKey !== void 0 ? documentKey : undefined };
|
|
1136
|
-
this.setAuthSession(proof);
|
|
1137
|
-
await this.ensureCryptoMarkerEncryption();
|
|
1138
|
-
return proof;
|
|
1139
|
-
}
|
|
1140
|
-
async getLastDocId() {
|
|
1141
|
-
let lastDocId = 0;
|
|
1142
|
-
try {
|
|
1143
|
-
let doc = await this.db.get("lastDocId");
|
|
1144
|
-
lastDocId = doc.value;
|
|
1145
|
-
}
|
|
1146
|
-
catch (e) {
|
|
1147
|
-
if (e.name === 'not_found') {
|
|
1148
|
-
logger.info("getLastDocId - not found. Must be first initialization.");
|
|
1149
|
-
return lastDocId;
|
|
1150
|
-
}
|
|
1151
|
-
logger.error("checkdb - something went wrong", { "error": e });
|
|
1152
|
-
}
|
|
1153
|
-
return lastDocId;
|
|
1154
|
-
}
|
|
1155
|
-
async getSystem() {
|
|
1156
|
-
try {
|
|
1157
|
-
let doc = await this.db.get("~system");
|
|
1158
|
-
return doc;
|
|
1159
|
-
}
|
|
1160
|
-
catch (e) {
|
|
1161
|
-
if (e.name === 'not_found') {
|
|
1162
|
-
logger.info("get System - not found", e);
|
|
1163
|
-
return null;
|
|
1164
|
-
}
|
|
1165
|
-
logger.error("getSystem - something went wrong", { "error": e });
|
|
1166
|
-
throw new Error(e);
|
|
1167
|
-
}
|
|
1168
|
-
}
|
|
1169
|
-
async loadPatches(schemaVersion) {
|
|
1170
|
-
const fnLogger = logger.child({ method: "loadPatches" });
|
|
1171
|
-
try {
|
|
1172
|
-
fnLogger.info("loadPatches - loading patches");
|
|
1173
|
-
const patches = await getSystemPatches(schemaVersion || "0.0.0");
|
|
1174
|
-
fnLogger.warn(`loadPatches - loaded ${patches.length} patches`);
|
|
1175
|
-
return patches;
|
|
1176
|
-
}
|
|
1177
|
-
catch (e) {
|
|
1178
|
-
fnLogger.error("loadPatches - something went wrong", e);
|
|
1179
|
-
throw new Error(e);
|
|
1180
|
-
}
|
|
1181
|
-
}
|
|
1182
|
-
async applyPatches(schemaVersion) {
|
|
1183
|
-
const fnLogger = logger.child({ method: "applyPatches", args: { schemaVersion } });
|
|
1184
|
-
let _schemaVersion = schemaVersion;
|
|
1185
|
-
try {
|
|
1186
|
-
const patches = await this.loadPatches(_schemaVersion);
|
|
1187
|
-
for (let patch of patches) {
|
|
1188
|
-
_schemaVersion = await this.applyPatch(patch);
|
|
1189
|
-
}
|
|
1190
|
-
if (_schemaVersion) {
|
|
1191
|
-
fnLogger.warn("Successfully applied patches till version", { version: _schemaVersion });
|
|
1192
|
-
this.schemaVersion = _schemaVersion;
|
|
1193
|
-
return _schemaVersion;
|
|
1194
|
-
}
|
|
1195
|
-
else {
|
|
1196
|
-
fnLogger.info("No patches were provided or applied");
|
|
1197
|
-
throw new Error("applyPatches - No patches were provided or applied");
|
|
1198
|
-
}
|
|
1199
|
-
}
|
|
1200
|
-
catch (e) {
|
|
1201
|
-
fnLogger.error("Something went wrong", e);
|
|
1202
|
-
throw new Error(e);
|
|
1203
|
-
}
|
|
1204
|
-
}
|
|
1205
|
-
// Method that verifies wether the system information are updated
|
|
1206
|
-
// applies patches too
|
|
1207
|
-
// TODO: Test if works corrrectly with multiple patch files
|
|
1208
|
-
async checkSystem() {
|
|
1209
|
-
let systemDoc = await this.getSystem();
|
|
1210
|
-
let _systemDoc;
|
|
1211
|
-
const dbInfo = await this.getDbInfo();
|
|
1212
|
-
logger.info("checkSystem - current system doc", { system: systemDoc });
|
|
1213
|
-
if (!systemDoc) {
|
|
1214
|
-
_systemDoc = {
|
|
1215
|
-
_id: "~system",
|
|
1216
|
-
appVersion: this.appVersion,
|
|
1217
|
-
dbInfo: dbInfo,
|
|
1218
|
-
schemaVersion: undefined,
|
|
1219
|
-
startupTime: (new Date()).valueOf()
|
|
1220
|
-
};
|
|
1221
|
-
// schemaVersion will be added after applying patches
|
|
1222
|
-
let schemaVersion = await this.applyPatches(_systemDoc.schemaVersion);
|
|
1223
|
-
console.log("Applied patches, new schema version:", schemaVersion);
|
|
1224
|
-
_systemDoc.schemaVersion = schemaVersion;
|
|
1225
|
-
}
|
|
1226
|
-
else {
|
|
1227
|
-
logger.info("checkSystem - system doc already exists. Checking for updates", systemDoc);
|
|
1228
|
-
// apply patches if needed
|
|
1229
|
-
let schemaVersion = await this.applyPatches(systemDoc.schemaVersion);
|
|
1230
|
-
_systemDoc = Object.assign(Object.assign({}, systemDoc), { appVersion: this.appVersion, dbInfo: dbInfo, schemaVersion: schemaVersion, startupTime: (new Date()).valueOf() });
|
|
1231
|
-
}
|
|
1232
|
-
// Update systemDoc
|
|
1233
|
-
try {
|
|
1234
|
-
await this.db.put(_systemDoc);
|
|
1235
|
-
}
|
|
1236
|
-
catch (e) {
|
|
1237
|
-
logger.error("checkSystem - There was a problem while updating system", { error: e });
|
|
1238
|
-
throw new Error(e);
|
|
1239
|
-
}
|
|
1240
|
-
logger.info("checkSystem - updated system", { system: _systemDoc });
|
|
1241
|
-
}
|
|
1242
|
-
// Database initialization should be about making sure that all the documents
|
|
1243
|
-
// representing the base data model for this framework are present
|
|
1244
|
-
// perform tasks like applying patches, creating indexes, etc.
|
|
1245
|
-
async initdb() {
|
|
1246
|
-
logger.warn("initdb - starting initialization", { "stackName": this.name });
|
|
1247
|
-
await this.ensureCryptoConfigDocument();
|
|
1248
|
-
logger.warn("initdb - crypto config ensured", { "stackName": this.name });
|
|
1249
|
-
await this.initIndex();
|
|
1250
|
-
logger.warn("initdb - index initialized", { "stackName": this.name });
|
|
1251
|
-
await this.checkSystem();
|
|
1252
|
-
logger.warn("initdb - system checked", { "stackName": this.name });
|
|
1253
|
-
this.setListeners();
|
|
1254
|
-
logger.warn("initdb - listeners set, initialization complete", { "stackName": this.name });
|
|
1255
|
-
return this;
|
|
1256
|
-
}
|
|
1257
|
-
async ensureCryptoConfigDocument() {
|
|
1258
|
-
const markerId = ClientStack.CRYPTO_CONFIG_DOC_ID;
|
|
1259
|
-
const existing = await this.db.get(markerId).catch((error) => {
|
|
1260
|
-
if ((error === null || error === void 0 ? void 0 : error.name) === "not_found" || (error === null || error === void 0 ? void 0 : error.status) === 404)
|
|
1261
|
-
return null;
|
|
1262
|
-
throw error;
|
|
1263
|
-
});
|
|
1264
|
-
if (existing) {
|
|
1265
|
-
this.validateCryptoConfig(existing);
|
|
1266
|
-
return existing;
|
|
1267
|
-
}
|
|
1268
|
-
const markerDoc = {
|
|
1269
|
-
_id: markerId,
|
|
1270
|
-
cryptoEngineDisabled: this.cryptoEngineDisabled,
|
|
1271
|
-
createdAt: new Date().toISOString(),
|
|
1272
|
-
};
|
|
1273
|
-
if (!this.cryptoEngineDisabled) {
|
|
1274
|
-
const randomBytes = new Uint8Array(12);
|
|
1275
|
-
globalThis.crypto.getRandomValues(randomBytes);
|
|
1276
|
-
const encryptedMarker = await this.cryptoEngine.encryptValueForMarker({
|
|
1277
|
-
nonce: Array.from(randomBytes).map(b => b.toString(16).padStart(2, '0')).join(''),
|
|
1278
|
-
});
|
|
1279
|
-
if (encryptedMarker) {
|
|
1280
|
-
markerDoc.encryptedMarker = encryptedMarker;
|
|
1281
|
-
}
|
|
1282
|
-
}
|
|
1283
|
-
try {
|
|
1284
|
-
await this.db.put(markerDoc);
|
|
1285
|
-
return markerDoc;
|
|
1286
|
-
}
|
|
1287
|
-
catch (error) {
|
|
1288
|
-
if ((error === null || error === void 0 ? void 0 : error.status) === 409 || (error === null || error === void 0 ? void 0 : error.name) === "conflict") {
|
|
1289
|
-
const current = await this.db.get(markerId);
|
|
1290
|
-
this.validateCryptoConfig(current);
|
|
1291
|
-
return current;
|
|
1292
|
-
}
|
|
1293
|
-
throw error;
|
|
1294
|
-
}
|
|
1295
|
-
}
|
|
1296
|
-
async ensureCryptoMarkerEncryption() {
|
|
1297
|
-
if (this.cryptoEngineDisabled || !this.cryptoEngine.isEnabled())
|
|
1298
|
-
return;
|
|
1299
|
-
const markerId = ClientStack.CRYPTO_CONFIG_DOC_ID;
|
|
1300
|
-
const markerDoc = await this.db.get(markerId).catch((error) => {
|
|
1301
|
-
if ((error === null || error === void 0 ? void 0 : error.name) === "not_found" || (error === null || error === void 0 ? void 0 : error.status) === 404)
|
|
1302
|
-
return null;
|
|
1303
|
-
throw error;
|
|
1304
|
-
});
|
|
1305
|
-
if (!markerDoc || isEncryptedPayload(markerDoc.encryptedMarker))
|
|
1306
|
-
return;
|
|
1307
|
-
const randomBytes = new Uint8Array(12);
|
|
1308
|
-
globalThis.crypto.getRandomValues(randomBytes);
|
|
1309
|
-
const encryptedMarker = await this.cryptoEngine.encryptValueForMarker({
|
|
1310
|
-
nonce: Array.from(randomBytes).map(b => b.toString(16).padStart(2, '0')).join(''),
|
|
1311
|
-
});
|
|
1312
|
-
if (!encryptedMarker)
|
|
1313
|
-
return;
|
|
1314
|
-
markerDoc.encryptedMarker = encryptedMarker;
|
|
1315
|
-
await this.db.put(markerDoc);
|
|
1316
|
-
}
|
|
1317
|
-
validateCryptoConfig(existing) {
|
|
1318
|
-
const storedDisabled = Boolean(existing.cryptoEngineDisabled);
|
|
1319
|
-
if (storedDisabled !== this.cryptoEngineDisabled) {
|
|
1320
|
-
throw new Error(storedDisabled
|
|
1321
|
-
? "Stack was initialized with crypto engine disabled; re-open it with disableCryptoEngine set to true."
|
|
1322
|
-
: "Stack requires the crypto engine; remove disableCryptoEngine to continue.");
|
|
1323
|
-
}
|
|
1324
|
-
if (!storedDisabled && isEncryptedPayload(existing.encryptedMarker) && !this.cryptoEngine.isEnabled()) {
|
|
1325
|
-
throw new Error("Crypto engine must be enabled to access this stack because it contains encrypted data.");
|
|
1326
|
-
}
|
|
1327
|
-
}
|
|
1328
|
-
async initIndex() {
|
|
1329
|
-
try {
|
|
1330
|
-
let lastDocId = await this.getLastDocId();
|
|
1331
|
-
// logger.info("initdb - res", res)
|
|
1332
|
-
if (!lastDocId) {
|
|
1333
|
-
lastDocId = Number(lastDocId);
|
|
1334
|
-
// logger.info("initdb - initializing db")
|
|
1335
|
-
try {
|
|
1336
|
-
let response = await this.db.put({
|
|
1337
|
-
_id: "lastDocId",
|
|
1338
|
-
value: ++lastDocId
|
|
1339
|
-
});
|
|
1340
|
-
if (response.ok)
|
|
1341
|
-
this.lastDocId = lastDocId;
|
|
1342
|
-
else
|
|
1343
|
-
throw new Error("Got problem while putting doc" + response);
|
|
1344
|
-
}
|
|
1345
|
-
catch (error) {
|
|
1346
|
-
if ((error === null || error === void 0 ? void 0 : error.status) === 409 || (error === null || error === void 0 ? void 0 : error.name) === "conflict") {
|
|
1347
|
-
const existing = await this.db.get("lastDocId");
|
|
1348
|
-
this.lastDocId = Number(existing.value);
|
|
1349
|
-
return;
|
|
1350
|
-
}
|
|
1351
|
-
throw error;
|
|
1352
|
-
}
|
|
1353
|
-
}
|
|
1354
|
-
else {
|
|
1355
|
-
logger.info("initdb - db already initialized, consider purge");
|
|
1356
|
-
}
|
|
1357
|
-
this.lastDocId = Number(lastDocId);
|
|
1358
|
-
}
|
|
1359
|
-
catch (e) {
|
|
1360
|
-
logger.error("initdb - something went wrong", e);
|
|
1361
|
-
throw new Error(e);
|
|
1362
|
-
}
|
|
1363
|
-
}
|
|
1364
|
-
// static async build( that: ClientStack ) {
|
|
1365
|
-
// let result = await that.initdb();
|
|
1366
|
-
// return result;
|
|
1367
|
-
// }
|
|
1368
|
-
// TODO: Consider filtering returned properties
|
|
1369
|
-
async getDocument(docId) {
|
|
1370
|
-
let doc = undefined;
|
|
1371
|
-
try {
|
|
1372
|
-
doc = await this.db.get(docId);
|
|
1373
|
-
}
|
|
1374
|
-
catch (e) {
|
|
1375
|
-
if (e.name === 'not_found') {
|
|
1376
|
-
logger.info("getDocument - not found", e);
|
|
1377
|
-
return null;
|
|
1378
|
-
}
|
|
1379
|
-
logger.info("getDocument - error", e);
|
|
1380
|
-
throw new Error(e);
|
|
1381
|
-
}
|
|
1382
|
-
return doc;
|
|
1383
|
-
}
|
|
1384
|
-
async getDocRevision(docId) {
|
|
1385
|
-
let _rev = null;
|
|
1386
|
-
try {
|
|
1387
|
-
let doc = await this.getDocument(docId);
|
|
1388
|
-
if (doc)
|
|
1389
|
-
_rev = doc._rev;
|
|
1390
|
-
}
|
|
1391
|
-
catch (e) {
|
|
1392
|
-
logger.info("getDocRevision - error", e);
|
|
1393
|
-
throw new Error(e);
|
|
1394
|
-
}
|
|
1395
|
-
return _rev;
|
|
1396
|
-
}
|
|
1397
|
-
async processReadableDocument(doc, classObj, fields, precomputedEncryptedKeys) {
|
|
1398
|
-
if (!this.cryptoEngine.isEnabled()) {
|
|
1399
|
-
return doc;
|
|
1400
|
-
}
|
|
1401
|
-
const encryptedKeys = precomputedEncryptedKeys !== null && precomputedEncryptedKeys !== void 0 ? precomputedEncryptedKeys : this.cryptoEngine.identifyEncryptedKeys(doc, classObj);
|
|
1402
|
-
if (!encryptedKeys.length && (!fields || !fields.length)) {
|
|
1403
|
-
return doc;
|
|
1404
|
-
}
|
|
1405
|
-
const clone = Object.assign({}, doc);
|
|
1406
|
-
const hasDocumentKey = Boolean(this.cryptoEngine.getDocumentKey());
|
|
1407
|
-
if (hasDocumentKey && encryptedKeys.length) {
|
|
1408
|
-
await this.cryptoEngine.decryptDocument(clone, classObj, encryptedKeys);
|
|
1409
|
-
}
|
|
1410
|
-
else if (encryptedKeys.length) {
|
|
1411
|
-
for (const key of encryptedKeys) {
|
|
1412
|
-
if (clone[key] !== undefined) {
|
|
1413
|
-
clone[key] = null;
|
|
1414
|
-
}
|
|
1415
|
-
}
|
|
1416
|
-
}
|
|
1417
|
-
const encryptedKeySet = new Set(encryptedKeys);
|
|
1418
|
-
const visibleKeys = Object.keys(clone).filter((key) => {
|
|
1419
|
-
if (key === "_id" || key === "_rev" || key === "~rev" || key === "~class" || key === "active" || key === "~createTimestamp" || key === "~updateTimestamp" || key === "description") {
|
|
1420
|
-
return false;
|
|
1421
|
-
}
|
|
1422
|
-
if (fields && fields.length) {
|
|
1423
|
-
return fields.includes(key) && clone[key] !== undefined;
|
|
1424
|
-
}
|
|
1425
|
-
return clone[key] !== undefined;
|
|
1426
|
-
});
|
|
1427
|
-
if (!hasDocumentKey && encryptedKeySet.size) {
|
|
1428
|
-
const nonEncryptedVisible = visibleKeys.filter((key) => !encryptedKeySet.has(key));
|
|
1429
|
-
if (!nonEncryptedVisible.length) {
|
|
1430
|
-
return null;
|
|
1431
|
-
}
|
|
1432
|
-
}
|
|
1433
|
-
if (!visibleKeys.length) {
|
|
1434
|
-
return null;
|
|
1435
|
-
}
|
|
1436
|
-
return clone;
|
|
1437
|
-
}
|
|
1438
|
-
async findDocument(selector, fields = undefined, skip = undefined, limit = undefined) {
|
|
1439
|
-
let result = await this.findDocuments(selector, fields, skip, limit);
|
|
1440
|
-
return result.docs.length > 0 ? result.docs[0] : null;
|
|
1441
|
-
}
|
|
1442
|
-
async incrementLastDocId() {
|
|
1443
|
-
let docId = "lastDocId", _rev = await this.getDocRevision(docId);
|
|
1444
|
-
if (_rev) {
|
|
1445
|
-
await this.db.put({
|
|
1446
|
-
_id: "lastDocId",
|
|
1447
|
-
_rev: _rev,
|
|
1448
|
-
value: ++this.lastDocId
|
|
1449
|
-
});
|
|
1450
|
-
return this.lastDocId;
|
|
1451
|
-
}
|
|
1452
|
-
// throw new Error
|
|
1453
|
-
}
|
|
1454
|
-
// The idea of this method is to be called from within the server (like CLI command)
|
|
1455
|
-
//
|
|
1456
|
-
async reset() {
|
|
1457
|
-
await this.destroyDb();
|
|
1458
|
-
// wait a few seconds
|
|
1459
|
-
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
1460
|
-
await this.initialize(this.connection, this.options);
|
|
1461
|
-
await this.initdb();
|
|
1462
|
-
return this;
|
|
1463
|
-
}
|
|
1464
|
-
async destroyDb() {
|
|
1465
|
-
const fnLogger = logger.child({ method: "destroyDb" });
|
|
1466
|
-
try {
|
|
1467
|
-
this.db.destroy(null, () => {
|
|
1468
|
-
fnLogger.info("Destroyed db");
|
|
1469
|
-
return true;
|
|
1470
|
-
});
|
|
1471
|
-
}
|
|
1472
|
-
catch (e) {
|
|
1473
|
-
fnLogger.error(`Error while destroying db: ${e}`);
|
|
1474
|
-
return false;
|
|
1475
|
-
}
|
|
1476
|
-
}
|
|
1477
|
-
// This method is similar to destroyDb, but intended to be called from the client (not to destroy the main db)
|
|
1478
|
-
// TODO: Right now this allows to clear any db
|
|
1479
|
-
// there should be more restrictions
|
|
1480
|
-
static async clear(conn) {
|
|
1481
|
-
return new Promise((resolve, reject) => {
|
|
1482
|
-
try {
|
|
1483
|
-
let db = new PouchDB(conn);
|
|
1484
|
-
db.destroy(null, () => {
|
|
1485
|
-
logger.info("clear - Destroyed db");
|
|
1486
|
-
resolve(true);
|
|
1487
|
-
});
|
|
1488
|
-
}
|
|
1489
|
-
catch (e) {
|
|
1490
|
-
logger.error("clear - Error while destroying db" + e);
|
|
1491
|
-
reject(false);
|
|
1492
|
-
}
|
|
1493
|
-
});
|
|
1494
|
-
}
|
|
1495
|
-
prepareDoc(_id, type, params, metaKey = "~class") {
|
|
1496
|
-
logger.info("prepareDoc - given args", { _id: _id, type: type, params: params });
|
|
1497
|
-
params["_id"] = _id;
|
|
1498
|
-
params[metaKey] = type;
|
|
1499
|
-
params["~createTimestamp"] = new Date().getTime();
|
|
1500
|
-
params["active"] = true;
|
|
1501
|
-
logger.info("prepareDoc - after elaborations", { params });
|
|
1502
|
-
return params;
|
|
1503
|
-
}
|
|
1504
|
-
}
|
|
1505
|
-
ClientStack.CRYPTO_CONFIG_DOC_ID = "~crypto-engine-config";
|
|
1506
|
-
export default ClientStack;
|
|
1507
|
-
//# sourceMappingURL=stack.js.map
|