@docstack/client 0.0.1 → 0.0.2

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.
Files changed (60) hide show
  1. package/lib/core/attribute.d.ts +4 -4
  2. package/lib/core/class.d.ts +3 -3
  3. package/lib/core/crypto-engine/index.d.ts +5 -5
  4. package/lib/core/query-engine/executor.d.ts +1 -1
  5. package/lib/core/query-engine/index.d.ts +3 -3
  6. package/lib/core/stack.d.ts +6 -6
  7. package/lib/index.d.ts +1 -1
  8. package/lib/index.js +2 -8237
  9. package/lib/index.js.map +1 -1
  10. package/lib/utils/index.d.ts +1 -1
  11. package/package.json +11 -3
  12. package/webpack.config.js +62 -0
  13. package/lib/core/attribute.js +0 -296
  14. package/lib/core/attribute.js.map +0 -1
  15. package/lib/core/class.js +0 -540
  16. package/lib/core/class.js.map +0 -1
  17. package/lib/core/crypto-engine/index.js +0 -130
  18. package/lib/core/crypto-engine/index.js.map +0 -1
  19. package/lib/core/crypto-engine/utils.js +0 -76
  20. package/lib/core/crypto-engine/utils.js.map +0 -1
  21. package/lib/core/datamodel/index.js +0 -1186
  22. package/lib/core/datamodel/index.js.map +0 -1
  23. package/lib/core/domain.js +0 -285
  24. package/lib/core/domain.js.map +0 -1
  25. package/lib/core/index.js +0 -379
  26. package/lib/core/index.js.map +0 -1
  27. package/lib/core/job-engine/index.js +0 -116
  28. package/lib/core/job-engine/index.js.map +0 -1
  29. package/lib/core/policy-engine/index.js +0 -150
  30. package/lib/core/policy-engine/index.js.map +0 -1
  31. package/lib/core/query-engine/accumulators.js +0 -258
  32. package/lib/core/query-engine/accumulators.js.map +0 -1
  33. package/lib/core/query-engine/evaluator.js +0 -179
  34. package/lib/core/query-engine/evaluator.js.map +0 -1
  35. package/lib/core/query-engine/executor.js +0 -405
  36. package/lib/core/query-engine/executor.js.map +0 -1
  37. package/lib/core/query-engine/index.js +0 -4
  38. package/lib/core/query-engine/index.js.map +0 -1
  39. package/lib/core/query-engine/parser.js +0 -515
  40. package/lib/core/query-engine/parser.js.map +0 -1
  41. package/lib/core/query-engine/planner.js +0 -330
  42. package/lib/core/query-engine/planner.js.map +0 -1
  43. package/lib/core/stack.js +0 -1507
  44. package/lib/core/stack.js.map +0 -1
  45. package/lib/core/test-utils/docstack.js +0 -222
  46. package/lib/core/test-utils/docstack.js.map +0 -1
  47. package/lib/core/trigger/index.js +0 -81
  48. package/lib/core/trigger/index.js.map +0 -1
  49. package/lib/plugins/pouchdb.js +0 -403
  50. package/lib/plugins/pouchdb.js.map +0 -1
  51. package/lib/utils/crypto/index.js +0 -34
  52. package/lib/utils/crypto/index.js.map +0 -1
  53. package/lib/utils/index.js +0 -58
  54. package/lib/utils/index.js.map +0 -1
  55. package/lib/utils/logger/index.js +0 -20
  56. package/lib/utils/logger/index.js.map +0 -1
  57. package/lib/utils/logger/transport.js +0 -28
  58. package/lib/utils/logger/transport.js.map +0 -1
  59. package/lib/workers/dataModel.js +0 -48
  60. 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