@docstack/client 0.0.4 → 0.0.6
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.js +406 -0
- package/lib/core/attribute.js.map +1 -0
- package/lib/core/class.d.ts +15 -16
- package/lib/core/class.js +761 -0
- package/lib/core/class.js.map +1 -0
- package/lib/core/crypto-engine/index.js +229 -0
- package/lib/core/crypto-engine/index.js.map +1 -0
- package/lib/core/crypto-engine/utils.js +88 -0
- package/lib/core/crypto-engine/utils.js.map +1 -0
- package/lib/core/datamodel/index.js +1308 -0
- package/lib/core/datamodel/index.js.map +1 -0
- package/lib/core/domain.d.ts +7 -8
- package/lib/core/domain.js +423 -0
- package/lib/core/domain.js.map +1 -0
- package/lib/core/index.js +520 -0
- package/lib/core/index.js.map +1 -0
- package/lib/core/job-engine/index.js +220 -0
- package/lib/core/job-engine/index.js.map +1 -0
- package/lib/core/policy-engine/index.js +232 -0
- package/lib/core/policy-engine/index.js.map +1 -0
- package/lib/core/query-engine/accumulators.js +258 -0
- package/lib/core/query-engine/accumulators.js.map +1 -0
- package/lib/core/query-engine/evaluator.js +179 -0
- package/lib/core/query-engine/evaluator.js.map +1 -0
- package/lib/core/query-engine/executor.js +405 -0
- package/lib/core/query-engine/executor.js.map +1 -0
- package/lib/core/query-engine/index.js +4 -0
- package/lib/core/query-engine/index.js.map +1 -0
- package/lib/core/query-engine/parser.js +515 -0
- package/lib/core/query-engine/parser.js.map +1 -0
- package/lib/core/query-engine/planner.js +330 -0
- package/lib/core/query-engine/planner.js.map +1 -0
- package/lib/core/stack.js +1817 -0
- package/lib/core/stack.js.map +1 -0
- package/lib/core/test-utils/docstack.js +222 -0
- package/lib/core/test-utils/docstack.js.map +1 -0
- package/lib/core/trigger/index.js +81 -0
- package/lib/core/trigger/index.js.map +1 -0
- package/lib/index.js +4 -8231
- package/lib/index.js.map +1 -1
- package/lib/index.umd.js +10 -4
- package/lib/plugins/pouchdb.js +368 -0
- package/lib/plugins/pouchdb.js.map +1 -0
- package/lib/utils/crypto/index.js +34 -0
- package/lib/utils/crypto/index.js.map +1 -0
- package/lib/utils/index.js +58 -0
- package/lib/utils/index.js.map +1 -0
- package/lib/utils/logger/index.js +20 -0
- package/lib/utils/logger/index.js.map +1 -0
- package/lib/utils/logger/transport.js +28 -0
- package/lib/utils/logger/transport.js.map +1 -0
- package/lib/workers/dataModel.js +48 -0
- package/lib/workers/dataModel.js.map +1 -0
- package/package.json +2 -2
|
@@ -0,0 +1,761 @@
|
|
|
1
|
+
var _a;
|
|
2
|
+
import { Class as Class_, isAttributeModel } from "@docstack/shared";
|
|
3
|
+
import createLogger from "../utils/logger/index";
|
|
4
|
+
import Attribute from "./attribute";
|
|
5
|
+
import { Trigger } from "./trigger/index";
|
|
6
|
+
import { z } from "zod";
|
|
7
|
+
import clientLogger from "../utils/logger/index";
|
|
8
|
+
/**
|
|
9
|
+
* Represents a data class (schema definition) in the DocStack database.
|
|
10
|
+
*
|
|
11
|
+
* A Class defines the structure of documents, including their attributes,
|
|
12
|
+
* validation rules (via Zod), and triggers that execute during document operations.
|
|
13
|
+
*
|
|
14
|
+
* Use the static factory methods ({@link Class.create}, {@link Class.fetch}) to
|
|
15
|
+
* instantiate classes - the constructor is private.
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* ```typescript
|
|
19
|
+
* // Create a new class with schema
|
|
20
|
+
* const taskClass = await Class.create(stack, 'Task', 'class', 'User Tasks');
|
|
21
|
+
*
|
|
22
|
+
* // Add attributes to define the schema
|
|
23
|
+
* await Attribute.create(taskClass, 'title', 'string', 'Task Title', { mandatory: true });
|
|
24
|
+
* await Attribute.create(taskClass, 'isComplete', 'boolean', 'Done?', { defaultValue: false });
|
|
25
|
+
*
|
|
26
|
+
* // Create documents (cards) of this class
|
|
27
|
+
* const task = await taskClass.add({ title: 'My Task', isComplete: false });
|
|
28
|
+
* ```
|
|
29
|
+
*
|
|
30
|
+
* @extends Class_
|
|
31
|
+
*/
|
|
32
|
+
class Class extends Class_ {
|
|
33
|
+
constructor() {
|
|
34
|
+
super();
|
|
35
|
+
/** Map of attribute names to Attribute instances defining the schema. */
|
|
36
|
+
this.attributes = {};
|
|
37
|
+
/** The raw schema definition from the ClassModel. */
|
|
38
|
+
this.schema = {};
|
|
39
|
+
/** Zod schema for runtime validation of document data. */
|
|
40
|
+
this.schemaZOD = z.object({});
|
|
41
|
+
/** Current state indicating if the class is processing an operation. */
|
|
42
|
+
this.state = "idle";
|
|
43
|
+
/** Array of triggers that execute before/after document operations. */
|
|
44
|
+
this.triggers = [];
|
|
45
|
+
// TODO: Test
|
|
46
|
+
/*
|
|
47
|
+
inheritAttributes( parentClass: Class ) {
|
|
48
|
+
let parentAttributes = parentClass.getAttributes();
|
|
49
|
+
for ( let attribute of parentAttributes ) {
|
|
50
|
+
this.addAttribute(attribute);
|
|
51
|
+
}
|
|
52
|
+
} */
|
|
53
|
+
this.build = () => {
|
|
54
|
+
return new Promise(async (resolve, reject) => {
|
|
55
|
+
let stack = this.getStack();
|
|
56
|
+
if (stack) {
|
|
57
|
+
// if (parentClassName) this.setParentClass(parentClassName);
|
|
58
|
+
let classModel = await stack.addClass(this);
|
|
59
|
+
// Hydrate model
|
|
60
|
+
if (classModel) {
|
|
61
|
+
this.setModel(classModel);
|
|
62
|
+
_a.logger.info("build - classModel", { classModel: classModel });
|
|
63
|
+
this.setId(classModel._id);
|
|
64
|
+
resolve(this);
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
reject("unable to get classModel. Check logs");
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
reject("Missing stack assignment");
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
};
|
|
75
|
+
this.init = (stack, id, name, type, description, schema = {}
|
|
76
|
+
// parentClass: Class | null
|
|
77
|
+
) => {
|
|
78
|
+
// this.parentClass = parentClass;
|
|
79
|
+
if (stack) {
|
|
80
|
+
this.stack = stack;
|
|
81
|
+
}
|
|
82
|
+
this.name = name;
|
|
83
|
+
this.id = id;
|
|
84
|
+
this.description = description;
|
|
85
|
+
this.type = type;
|
|
86
|
+
// this.attributes = [];
|
|
87
|
+
// this.stack = null;
|
|
88
|
+
// this.id = null;
|
|
89
|
+
// if (schema) {
|
|
90
|
+
// this.schema = schema;
|
|
91
|
+
// }
|
|
92
|
+
this.setModel({
|
|
93
|
+
"~class": type, _id: id, active: true,
|
|
94
|
+
name, description,
|
|
95
|
+
schema, triggers: [],
|
|
96
|
+
});
|
|
97
|
+
this.logger = clientLogger(stack).child({ module: "class", className: this.name });
|
|
98
|
+
// TODO: Waiting for test of method
|
|
99
|
+
// if (parentClass) this.inheritAttributes(parentClass);
|
|
100
|
+
};
|
|
101
|
+
this.uniqueCheck = async (doc) => {
|
|
102
|
+
const fnLogger = this.logger.child({ method: "uniqueCheck", args: { doc } });
|
|
103
|
+
const duplicate = await this.getByPrimaryKeys(doc);
|
|
104
|
+
if (duplicate == null || duplicate._id == doc._id) {
|
|
105
|
+
fnLogger.info("No duplicate found for doc");
|
|
106
|
+
return true;
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
fnLogger.info("Duplicate found for doc", { duplicate });
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
this.bulkUniqueCheck = async (pKs) => {
|
|
114
|
+
var _b;
|
|
115
|
+
const fnLogger = this.logger.child({ method: "bulkUniqueCheck", args: { pKs } });
|
|
116
|
+
const ddocId = await ((_b = this.stack) === null || _b === void 0 ? void 0 : _b.addDesignDocumentPKs(this.name, pKs, true));
|
|
117
|
+
fnLogger.info(`Created temporary design document '${ddocId}'`);
|
|
118
|
+
if (this.stack && ddocId) {
|
|
119
|
+
try {
|
|
120
|
+
const result = await this.stack.db.query(`${ddocId}/by_pKeys`, {
|
|
121
|
+
group: true,
|
|
122
|
+
reduce: '_count'
|
|
123
|
+
});
|
|
124
|
+
const hasDuplicates = result.rows.some(row => row.value > 1);
|
|
125
|
+
if (hasDuplicates) {
|
|
126
|
+
// 3a. Rollback: new schema is invalid
|
|
127
|
+
fnLogger.error('Schema change invalid: new duplicates found.');
|
|
128
|
+
const finalTempDoc = await this.stack.db.get(ddocId);
|
|
129
|
+
await this.stack.db.remove(finalTempDoc);
|
|
130
|
+
return false; // Indicate failure
|
|
131
|
+
}
|
|
132
|
+
else {
|
|
133
|
+
// 3b. Execute: new schema is valid. Replace the live document.
|
|
134
|
+
fnLogger.info('Bulk unique check completed: no new duplicates found');
|
|
135
|
+
const finalTempDoc = await this.stack.db.get(ddocId);
|
|
136
|
+
// Clean up the temporary document
|
|
137
|
+
await this.stack.db.remove(finalTempDoc);
|
|
138
|
+
return true; // Indicate success
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
catch (err) {
|
|
142
|
+
console.error('Error during schema validation:', err);
|
|
143
|
+
// Ensure the temporary document is removed on error
|
|
144
|
+
try {
|
|
145
|
+
const finalTempDoc = await this.stack.db.get(ddocId);
|
|
146
|
+
await this.stack.db.remove(finalTempDoc);
|
|
147
|
+
}
|
|
148
|
+
catch (e) { /* ignore */ }
|
|
149
|
+
return false;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
else {
|
|
153
|
+
fnLogger.error(`Was unable to create temporary design document to group by`);
|
|
154
|
+
return false;
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
/**
|
|
158
|
+
* Validates document data against the class schema using Zod.
|
|
159
|
+
*
|
|
160
|
+
* @param data - The document data to validate
|
|
161
|
+
* @returns `true` if validation passes, `false` otherwise
|
|
162
|
+
*/
|
|
163
|
+
this.validate = async (data) => {
|
|
164
|
+
const fnLogger = this.logger.child({ method: "validate" });
|
|
165
|
+
const result = await this.schemaZOD.safeParseAsync(data);
|
|
166
|
+
fnLogger.debug("Got result", { result });
|
|
167
|
+
if (result.success) {
|
|
168
|
+
return true;
|
|
169
|
+
}
|
|
170
|
+
else {
|
|
171
|
+
return false;
|
|
172
|
+
}
|
|
173
|
+
};
|
|
174
|
+
// TODO Turn into method (after factory method instantiation refactory is done)
|
|
175
|
+
this.setId = (id) => {
|
|
176
|
+
this.id = id;
|
|
177
|
+
};
|
|
178
|
+
this.getName = () => {
|
|
179
|
+
return this.name;
|
|
180
|
+
};
|
|
181
|
+
this.getStack = () => {
|
|
182
|
+
return this.stack;
|
|
183
|
+
};
|
|
184
|
+
this.getDescription = () => {
|
|
185
|
+
return this.description;
|
|
186
|
+
};
|
|
187
|
+
this.getType = () => {
|
|
188
|
+
return this.type;
|
|
189
|
+
};
|
|
190
|
+
this.getId = () => {
|
|
191
|
+
return this.id;
|
|
192
|
+
};
|
|
193
|
+
/**
|
|
194
|
+
* Builds the schema object from the current attributes.
|
|
195
|
+
* @returns The schema definition object
|
|
196
|
+
*/
|
|
197
|
+
this.buildSchema = () => {
|
|
198
|
+
let schema = {};
|
|
199
|
+
Object.entries(this.attributes).forEach(t => {
|
|
200
|
+
schema[t[0]] = t[1].model;
|
|
201
|
+
});
|
|
202
|
+
return schema;
|
|
203
|
+
};
|
|
204
|
+
/**
|
|
205
|
+
* Returns the current ClassModel representation of this class.
|
|
206
|
+
* @returns The ClassModel document
|
|
207
|
+
*/
|
|
208
|
+
this.getModel = () => {
|
|
209
|
+
let triggers = [];
|
|
210
|
+
for (const trigger of this.triggers) {
|
|
211
|
+
triggers.push(trigger.model);
|
|
212
|
+
}
|
|
213
|
+
let model = {
|
|
214
|
+
_id: this.id,
|
|
215
|
+
name: this.getName(),
|
|
216
|
+
description: this.getDescription(),
|
|
217
|
+
"~class": this.getType(),
|
|
218
|
+
schema: this.buildSchema(),
|
|
219
|
+
triggers: triggers,
|
|
220
|
+
active: true,
|
|
221
|
+
_rev: this.model ? this.model._rev : "",
|
|
222
|
+
"~createTimestamp": this.model ? this.model["~createTimestamp"] : undefined,
|
|
223
|
+
};
|
|
224
|
+
return model;
|
|
225
|
+
};
|
|
226
|
+
// [TODO] Change into buildFromModel
|
|
227
|
+
/**
|
|
228
|
+
* It hydrates attributes and triggers from given model
|
|
229
|
+
* @param model
|
|
230
|
+
*/
|
|
231
|
+
this.setModel = (model) => {
|
|
232
|
+
_a.logger.info("setModel - got incoming model", { model: model });
|
|
233
|
+
// Retreive current class model
|
|
234
|
+
let currentModel = this.getModel();
|
|
235
|
+
// Set model arg to the overwrite of the current model with the given one
|
|
236
|
+
model = Object.assign(currentModel, model);
|
|
237
|
+
if (model.schema) {
|
|
238
|
+
// model.schema = {...this.model.schema, ...model.schema};
|
|
239
|
+
this.attributes = {};
|
|
240
|
+
this.schemaZOD = z.object({});
|
|
241
|
+
for (const [key, attrModel] of Object.entries(model.schema)) {
|
|
242
|
+
let attribute = new Attribute(this, attrModel.name, attrModel.type, attrModel.description, attrModel.config);
|
|
243
|
+
this.attributes[attrModel.name] = attribute;
|
|
244
|
+
this.schemaZOD = this.schemaZOD.extend({
|
|
245
|
+
[attrModel.name]: attribute.field
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
if (model.triggers) {
|
|
250
|
+
this.triggers = [];
|
|
251
|
+
for (const trigger of model.triggers) {
|
|
252
|
+
let trigger_ = new Trigger(trigger, this);
|
|
253
|
+
this.triggers.push(trigger_);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
this.name = model.name;
|
|
257
|
+
this.description = model.description;
|
|
258
|
+
this.type = model["~class"];
|
|
259
|
+
this.model = model;
|
|
260
|
+
_a.logger.info("setModel - model after processing", { model: model });
|
|
261
|
+
};
|
|
262
|
+
/**
|
|
263
|
+
* Returns the primary key attribute names for this class.
|
|
264
|
+
* @returns Array of attribute names marked as primary keys
|
|
265
|
+
*/
|
|
266
|
+
this.getPrimaryKeys = () => {
|
|
267
|
+
return Object.values(this.attributes).filter(attr => attr.isPrimaryKey())
|
|
268
|
+
.map(attr => attr.getName());
|
|
269
|
+
};
|
|
270
|
+
this.getAttributes = (...names) => {
|
|
271
|
+
let attributes = {};
|
|
272
|
+
for (const attribute of Object.values(this.attributes)) {
|
|
273
|
+
if (names.length > 0) {
|
|
274
|
+
// filter with given names
|
|
275
|
+
for (let name of names) {
|
|
276
|
+
// match?
|
|
277
|
+
if (name != null && attribute.getName() == name) {
|
|
278
|
+
attributes[attribute.name] = attribute;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
else {
|
|
283
|
+
// no filter provided add all
|
|
284
|
+
attributes[attribute.name] = attribute;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
return attributes;
|
|
288
|
+
};
|
|
289
|
+
this.hasAllAttributes = (...names) => {
|
|
290
|
+
let result = false;
|
|
291
|
+
let attributes = this.getAttributes(...names);
|
|
292
|
+
for (let attribute of Object.values(attributes)) {
|
|
293
|
+
result = names.includes(attribute.getName());
|
|
294
|
+
if (!result)
|
|
295
|
+
break;
|
|
296
|
+
}
|
|
297
|
+
return result;
|
|
298
|
+
};
|
|
299
|
+
this.hasAnyAttributes = (...names) => {
|
|
300
|
+
let result = false;
|
|
301
|
+
let attributes = this.getAttributes(...names);
|
|
302
|
+
for (let attribute of Object.values(attributes)) {
|
|
303
|
+
result = names.includes(attribute.getName());
|
|
304
|
+
if (result)
|
|
305
|
+
break;
|
|
306
|
+
}
|
|
307
|
+
return result;
|
|
308
|
+
};
|
|
309
|
+
this.getEncryptedAttributes = () => {
|
|
310
|
+
return Object.values(this.attributes).filter((attribute) => {
|
|
311
|
+
const config = attribute.model.config;
|
|
312
|
+
return (config === null || config === void 0 ? void 0 : config.encrypted) === true && (config === null || config === void 0 ? void 0 : config.primaryKey) !== true;
|
|
313
|
+
});
|
|
314
|
+
};
|
|
315
|
+
// interface of hasAnyAttributes
|
|
316
|
+
this.hasAttribute = (name) => {
|
|
317
|
+
return this.hasAnyAttributes(name);
|
|
318
|
+
};
|
|
319
|
+
/**
|
|
320
|
+
* Adds a new attribute to the class schema.
|
|
321
|
+
* Persists the change to the database.
|
|
322
|
+
*
|
|
323
|
+
* @param attribute - The Attribute instance or AttributeModel to add
|
|
324
|
+
* @returns This Class instance for chaining
|
|
325
|
+
*
|
|
326
|
+
* @example
|
|
327
|
+
* ```typescript
|
|
328
|
+
* await taskClass.addAttribute(new Attribute(taskClass, 'dueDate', 'date', 'Due Date'));
|
|
329
|
+
* // Or use Attribute.create() for a simpler API
|
|
330
|
+
* ```
|
|
331
|
+
*/
|
|
332
|
+
this.addAttribute = async (attribute) => {
|
|
333
|
+
var _b;
|
|
334
|
+
const fnLogger = this.logger.child({ method: "addAttribute", args: { attribute: attribute.name } });
|
|
335
|
+
const attribute_ = isAttributeModel(attribute)
|
|
336
|
+
? new Attribute(this, attribute.name, attribute.type, attribute.description, attribute.config) : attribute;
|
|
337
|
+
try {
|
|
338
|
+
let name = attribute_.getName();
|
|
339
|
+
// console.log("Adding attribute", {className: this.name, attribute: name})
|
|
340
|
+
if (!this.hasAttribute(name)) {
|
|
341
|
+
fnLogger.info("Adding attribute", { name: name, type: attribute_.getModel() });
|
|
342
|
+
this.attributes[name] = attribute_;
|
|
343
|
+
let attributeModel = attribute_.getModel();
|
|
344
|
+
fnLogger.info("Adding attribute to schema", { attributeModel: attributeModel });
|
|
345
|
+
const currentSchema = (_b = this.model.schema) !== null && _b !== void 0 ? _b : {};
|
|
346
|
+
this.model.schema = Object.assign(Object.assign({}, currentSchema), { [name]: attributeModel });
|
|
347
|
+
this.schemaZOD = this.schemaZOD.extend({
|
|
348
|
+
[name]: attribute_.field
|
|
349
|
+
});
|
|
350
|
+
// TODO:
|
|
351
|
+
// this.schema[name] = attributeModel; // sometimes getting schema undefined
|
|
352
|
+
// update class on db
|
|
353
|
+
fnLogger.info("Checking for requirements before updating class on db", { stack: (this.stack != null), id: this.id });
|
|
354
|
+
if (this.stack && this.id) {
|
|
355
|
+
// debugger;
|
|
356
|
+
fnLogger.info("Updating class on db");
|
|
357
|
+
let res = await this.stack.updateClass(this);
|
|
358
|
+
return this;
|
|
359
|
+
// TODO: Check if this class has subclasses
|
|
360
|
+
}
|
|
361
|
+
else {
|
|
362
|
+
fnLogger.error("Class not updated on db because of missing stack or id");
|
|
363
|
+
return this;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
else {
|
|
367
|
+
fnLogger.error("Attribute with name " + name + " already exists within this Class");
|
|
368
|
+
return this;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
catch (e) {
|
|
372
|
+
fnLogger.error("Falied adding attribute because: ", e);
|
|
373
|
+
return this;
|
|
374
|
+
}
|
|
375
|
+
};
|
|
376
|
+
/**
|
|
377
|
+
* Modifies an existing attribute in the class schema.
|
|
378
|
+
*
|
|
379
|
+
* @param name - The name of the attribute to modify
|
|
380
|
+
* @param attribute - The new Attribute or AttributeModel definition
|
|
381
|
+
* @returns This Class instance for chaining
|
|
382
|
+
*/
|
|
383
|
+
this.modifyAttribute = async (name, attribute) => {
|
|
384
|
+
const fnLogger = this.logger.child({ method: "modifyAttribute", args: { name } });
|
|
385
|
+
const originSchema = Object.assign({}, this.model.schema[name]), originAttr = this.attributes[name];
|
|
386
|
+
const attribute_ = isAttributeModel(attribute)
|
|
387
|
+
? new Attribute(this, attribute.name, attribute.type, attribute.description, attribute.config) : attribute;
|
|
388
|
+
try {
|
|
389
|
+
fnLogger.info(`Attempting to change attribute definition.`);
|
|
390
|
+
delete this.model.schema[name];
|
|
391
|
+
delete this.attributes[name];
|
|
392
|
+
this.schemaZOD = this.schemaZOD.omit({ [name]: true });
|
|
393
|
+
return this.addAttribute(attribute_);
|
|
394
|
+
}
|
|
395
|
+
catch (e) {
|
|
396
|
+
// Revert
|
|
397
|
+
this.model.schema[name] = originSchema;
|
|
398
|
+
this.attributes[name] = originAttr;
|
|
399
|
+
fnLogger.error(`Failed at removing attribute from class.'`);
|
|
400
|
+
}
|
|
401
|
+
return this;
|
|
402
|
+
};
|
|
403
|
+
/**
|
|
404
|
+
* Removes an attribute from the class schema.
|
|
405
|
+
*
|
|
406
|
+
* @param name - The name of the attribute to remove
|
|
407
|
+
* @returns This Class instance for chaining
|
|
408
|
+
*/
|
|
409
|
+
this.removeAttribute = async (name) => {
|
|
410
|
+
const fnLogger = this.logger.child({ method: "removeAttribute", args: { name } });
|
|
411
|
+
const originSchema = Object.assign({}, this.model.schema[name]), originAttr = this.attributes[name];
|
|
412
|
+
try {
|
|
413
|
+
fnLogger.info(`Attempting to remove attribute from class.`);
|
|
414
|
+
delete this.model.schema[name];
|
|
415
|
+
delete this.attributes[name];
|
|
416
|
+
this.schemaZOD = this.schemaZOD.omit({ [name]: true });
|
|
417
|
+
if (this.stack) {
|
|
418
|
+
this.stack.updateClass(this);
|
|
419
|
+
}
|
|
420
|
+
else
|
|
421
|
+
throw new Error("Missing stack, cannot perform updates.");
|
|
422
|
+
}
|
|
423
|
+
catch (e) {
|
|
424
|
+
// Revert
|
|
425
|
+
this.model.schema[name] = originSchema;
|
|
426
|
+
this.attributes[name] = originAttr;
|
|
427
|
+
fnLogger.error(`Failed at removing attribute from class.'`);
|
|
428
|
+
}
|
|
429
|
+
return this;
|
|
430
|
+
};
|
|
431
|
+
/**
|
|
432
|
+
* Creates a new document (card) of this class type.
|
|
433
|
+
*
|
|
434
|
+
* @param params - The document data
|
|
435
|
+
* @returns The created document, or `null` if stack is not defined
|
|
436
|
+
*
|
|
437
|
+
* @example
|
|
438
|
+
* ```typescript
|
|
439
|
+
* const task = await taskClass.addCard({
|
|
440
|
+
* title: 'My Task',
|
|
441
|
+
* isComplete: false
|
|
442
|
+
* });
|
|
443
|
+
* ```
|
|
444
|
+
*/
|
|
445
|
+
this.addCard = async (params) => {
|
|
446
|
+
const fnLogger = this.logger.child({ method: "addCard", args: { params } });
|
|
447
|
+
if (!this.stack) {
|
|
448
|
+
fnLogger.error("Stack is not defined");
|
|
449
|
+
return null;
|
|
450
|
+
}
|
|
451
|
+
return await this.stack.createDoc(null, this.getName(), this, params);
|
|
452
|
+
};
|
|
453
|
+
/**
|
|
454
|
+
* Creates multiple documents (cards) of this class type in a batch.
|
|
455
|
+
*
|
|
456
|
+
* @param paramsArray - Array of document data objects
|
|
457
|
+
* @returns Array of created documents
|
|
458
|
+
*/
|
|
459
|
+
this.addCards = async (paramsArray) => {
|
|
460
|
+
const fnLogger = this.logger.child({ method: "addCards", args: { paramsArray } });
|
|
461
|
+
if (!this.stack) {
|
|
462
|
+
fnLogger.error("Stack is not defined");
|
|
463
|
+
return [];
|
|
464
|
+
}
|
|
465
|
+
let addedCards = [];
|
|
466
|
+
addedCards = await this.stack.createDocs(paramsArray.map(params => ({ docId: null, params })), this.getName(), this);
|
|
467
|
+
return addedCards;
|
|
468
|
+
};
|
|
469
|
+
this.getByPrimaryKeys = async (params) => {
|
|
470
|
+
const fnLogger = this.logger.child({ method: "getByPrimaryKeys" });
|
|
471
|
+
// attempt to retrieve card by primary key
|
|
472
|
+
let filter = {};
|
|
473
|
+
let primaryKeys = this.getPrimaryKeys();
|
|
474
|
+
fnLogger.info("Got primary keys", { primaryKeys });
|
|
475
|
+
if (primaryKeys.length) {
|
|
476
|
+
// executes a reducer function on each element of the primaryKeys array
|
|
477
|
+
// that sets each primary key prop to the corresponding param value
|
|
478
|
+
primaryKeys.reduce((accumulator, currentValue) => accumulator[currentValue] = params[currentValue], filter);
|
|
479
|
+
fnLogger.info("Defined filter", { filter });
|
|
480
|
+
let cards = await this.getCards(filter, undefined, 0, 1);
|
|
481
|
+
if (cards.length > 0) {
|
|
482
|
+
return cards[0];
|
|
483
|
+
}
|
|
484
|
+
else {
|
|
485
|
+
fnLogger.info("Did not find any documents with given primary key", { filter });
|
|
486
|
+
return null;
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
else {
|
|
490
|
+
fnLogger.info("Class has no field specified as primary key");
|
|
491
|
+
return null;
|
|
492
|
+
}
|
|
493
|
+
};
|
|
494
|
+
this.addOrUpdateCard = async (params, cardId) => {
|
|
495
|
+
const fnLogger = this.logger.child({ method: "addOrUpdateCard", args: { params, cardId } });
|
|
496
|
+
return new Promise(async (resolve, reject) => {
|
|
497
|
+
if (cardId) {
|
|
498
|
+
fnLogger.info("Provided document's id, performing an update");
|
|
499
|
+
const res = await this.updateCard(cardId, params);
|
|
500
|
+
resolve(res);
|
|
501
|
+
}
|
|
502
|
+
else {
|
|
503
|
+
fnLogger.info("No document id provided, checking for PKs");
|
|
504
|
+
const card = await this.getByPrimaryKeys(params);
|
|
505
|
+
if (card == null) {
|
|
506
|
+
const res = await this.addCard(params);
|
|
507
|
+
resolve(res);
|
|
508
|
+
}
|
|
509
|
+
else {
|
|
510
|
+
fnLogger.error("Duplicate card by keys");
|
|
511
|
+
reject("Duplicate card by keys");
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
});
|
|
515
|
+
};
|
|
516
|
+
/**
|
|
517
|
+
* Updates an existing document (card) of this class.
|
|
518
|
+
*
|
|
519
|
+
* @param cardId - The document ID to update
|
|
520
|
+
* @param params - The updated document data
|
|
521
|
+
* @returns The updated document, or `null` if stack is not defined
|
|
522
|
+
*/
|
|
523
|
+
this.updateCard = async (cardId, params) => {
|
|
524
|
+
return new Promise(async (resolve, reject) => {
|
|
525
|
+
if (this.stack) {
|
|
526
|
+
const res = await this.stack.createDoc(cardId, this.getName(), this, params);
|
|
527
|
+
resolve(res);
|
|
528
|
+
}
|
|
529
|
+
else {
|
|
530
|
+
_a.logger.info("no stack defined");
|
|
531
|
+
resolve(null);
|
|
532
|
+
}
|
|
533
|
+
});
|
|
534
|
+
};
|
|
535
|
+
/**
|
|
536
|
+
* Soft-deletes a document by setting its `active` flag to `false`.
|
|
537
|
+
*
|
|
538
|
+
* @param cardId - The document ID to delete
|
|
539
|
+
* @returns `true` if successful, `false` otherwise
|
|
540
|
+
*/
|
|
541
|
+
this.deleteCard = async (cardId) => {
|
|
542
|
+
const fnLogger = this.logger.child({ method: "deleteCard", args: { cardId } });
|
|
543
|
+
if (this.stack) {
|
|
544
|
+
const res = await this.stack.deleteDocument(cardId);
|
|
545
|
+
return res;
|
|
546
|
+
}
|
|
547
|
+
else {
|
|
548
|
+
fnLogger.error("Stack is not defined");
|
|
549
|
+
return false;
|
|
550
|
+
}
|
|
551
|
+
};
|
|
552
|
+
/**
|
|
553
|
+
* Retrieves documents (cards) of this class type.
|
|
554
|
+
*
|
|
555
|
+
* @param selector - Optional PouchDB/Mango selector for filtering
|
|
556
|
+
* @param fields - Optional list of fields to return
|
|
557
|
+
* @param skip - Number of documents to skip
|
|
558
|
+
* @param limit - Maximum number of documents to return
|
|
559
|
+
* @returns Array of matching documents
|
|
560
|
+
*
|
|
561
|
+
* @example
|
|
562
|
+
* ```typescript
|
|
563
|
+
* // Get all tasks
|
|
564
|
+
* const allTasks = await taskClass.getCards();
|
|
565
|
+
*
|
|
566
|
+
* // Get incomplete tasks
|
|
567
|
+
* const incomplete = await taskClass.getCards({ isComplete: { $eq: false } });
|
|
568
|
+
* ```
|
|
569
|
+
*/
|
|
570
|
+
this.getCards = async (selector, fields, skip, limit) => {
|
|
571
|
+
const _selector = Object.assign(Object.assign({}, (selector || {})), { "~class": { $eq: this.name } });
|
|
572
|
+
this.logger.info("getCards - selector", { selector: _selector, fields, skip, limit });
|
|
573
|
+
let docs = (await this.stack.findDocuments(_selector, fields, skip, limit)).docs;
|
|
574
|
+
return docs;
|
|
575
|
+
};
|
|
576
|
+
/**
|
|
577
|
+
* Adds a trigger to this class.
|
|
578
|
+
* Triggers execute before or after document operations.
|
|
579
|
+
*
|
|
580
|
+
* @param name - The trigger name
|
|
581
|
+
* @param model - The trigger model containing the execution logic
|
|
582
|
+
* @returns This Class instance for chaining
|
|
583
|
+
*
|
|
584
|
+
* @example
|
|
585
|
+
* ```typescript
|
|
586
|
+
* await taskClass.addTrigger('generate-slug', {
|
|
587
|
+
* name: 'generate-slug',
|
|
588
|
+
* order: 'before',
|
|
589
|
+
* run: `document.slug = document.title.toLowerCase().replace(/\\s+/g, '-'); return document;`
|
|
590
|
+
* });
|
|
591
|
+
* ```
|
|
592
|
+
*/
|
|
593
|
+
this.addTrigger = async (name, model) => {
|
|
594
|
+
const fnLogger = this.logger.child({ method: "addTrigger" });
|
|
595
|
+
try {
|
|
596
|
+
const trigger = new Trigger(model, this);
|
|
597
|
+
this.triggers.push(trigger);
|
|
598
|
+
if (this.stack) {
|
|
599
|
+
this.setModel();
|
|
600
|
+
let res = await this.stack.updateClass(this);
|
|
601
|
+
}
|
|
602
|
+
else {
|
|
603
|
+
throw new Error(`Stack is not defined. Can't update class`);
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
catch (e) {
|
|
607
|
+
fnLogger.error(e);
|
|
608
|
+
}
|
|
609
|
+
return this;
|
|
610
|
+
};
|
|
611
|
+
/**
|
|
612
|
+
* Removes a trigger from this class by name.
|
|
613
|
+
*
|
|
614
|
+
* @param name - The name of the trigger to remove
|
|
615
|
+
* @returns This Class instance for chaining
|
|
616
|
+
*/
|
|
617
|
+
this.removeTrigger = async (name) => {
|
|
618
|
+
this.triggers = this.triggers.filter(t => t.name != name);
|
|
619
|
+
return this;
|
|
620
|
+
};
|
|
621
|
+
// Private constructor to prevent direct instantiation
|
|
622
|
+
/* Populated on async build */
|
|
623
|
+
// this.id = null;
|
|
624
|
+
}
|
|
625
|
+
async add(...paramsArray) {
|
|
626
|
+
const fnLogger = this.logger.child({ method: "add", args: { paramsArray } });
|
|
627
|
+
const addedCards = await this.addCards(paramsArray);
|
|
628
|
+
fnLogger.info("Added cards", { addedCards });
|
|
629
|
+
if (paramsArray.length === 1)
|
|
630
|
+
return addedCards[0] || null;
|
|
631
|
+
return addedCards;
|
|
632
|
+
}
|
|
633
|
+
async get(...cardId) {
|
|
634
|
+
const fnLogger = this.logger.child({ method: "get", args: { cardId } });
|
|
635
|
+
if (typeof cardId === "string") {
|
|
636
|
+
let docs = await this.getCards({ _id: { $eq: cardId } });
|
|
637
|
+
return docs[0] || null;
|
|
638
|
+
}
|
|
639
|
+
else {
|
|
640
|
+
let docs = await this.getCards({ _id: { $in: cardId } });
|
|
641
|
+
fnLogger.info("Fetched documents", { docs });
|
|
642
|
+
return docs;
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
_a = Class;
|
|
647
|
+
Class.logger = createLogger().child({ module: "class" });
|
|
648
|
+
/**
|
|
649
|
+
* Gets a Class instance without persisting it to the database.
|
|
650
|
+
* Use this for working with existing class models or for testing.
|
|
651
|
+
* Sets up a document change listener for real-time updates.
|
|
652
|
+
*
|
|
653
|
+
* @param stack - The parent stack instance
|
|
654
|
+
* @param id - The class ID
|
|
655
|
+
* @param name - The class name
|
|
656
|
+
* @param type - The class type
|
|
657
|
+
* @param description - Optional description
|
|
658
|
+
* @param schema - Initial schema definition
|
|
659
|
+
* @returns A new Class instance (not persisted)
|
|
660
|
+
*/
|
|
661
|
+
Class.get = (stack, id, name, type, description, schema = {}) => {
|
|
662
|
+
const class_ = new _a();
|
|
663
|
+
_a.logger.info("Received schema", { schema });
|
|
664
|
+
class_.init(stack, id, name, type, description, schema);
|
|
665
|
+
// Add listener for new documents of this class type
|
|
666
|
+
class_.stack.onClassDoc(name)
|
|
667
|
+
.on("change", (change) => {
|
|
668
|
+
const evt = new CustomEvent("doc", {
|
|
669
|
+
detail: change
|
|
670
|
+
});
|
|
671
|
+
class_.dispatchEvent(evt);
|
|
672
|
+
});
|
|
673
|
+
return class_;
|
|
674
|
+
};
|
|
675
|
+
/**
|
|
676
|
+
* Creates a new class and persists it to the database.
|
|
677
|
+
* This is the primary factory method for creating new classes.
|
|
678
|
+
*
|
|
679
|
+
* @param stack - The parent stack instance
|
|
680
|
+
* @param name - The name for the new class
|
|
681
|
+
* @param type - The class type (typically 'class')
|
|
682
|
+
* @param description - Optional description of the class
|
|
683
|
+
* @param schema - Initial schema definition
|
|
684
|
+
* @returns The persisted Class instance
|
|
685
|
+
*
|
|
686
|
+
* @example
|
|
687
|
+
* ```typescript
|
|
688
|
+
* const userClass = await Class.create(stack, 'User', 'class', 'Application users');
|
|
689
|
+
* ```
|
|
690
|
+
*/
|
|
691
|
+
Class.create = async (stack, name, type, description, schema = {}) => {
|
|
692
|
+
const class_ = _a.get(stack, name, name, type, description, schema);
|
|
693
|
+
await class_.build();
|
|
694
|
+
return class_;
|
|
695
|
+
};
|
|
696
|
+
/**
|
|
697
|
+
* Builds a Class instance from an existing ClassModel document.
|
|
698
|
+
* Hydrates attributes and triggers from the model.
|
|
699
|
+
*
|
|
700
|
+
* @param stack - The parent stack instance
|
|
701
|
+
* @param classModel - The ClassModel document from the database
|
|
702
|
+
* @returns The hydrated Class instance
|
|
703
|
+
*/
|
|
704
|
+
Class.buildFromModel = async (stack, classModel) => {
|
|
705
|
+
_a.logger.info("buildFromModel - Instantiate from model", { classModel });
|
|
706
|
+
if (classModel._rev) {
|
|
707
|
+
let classObj = _a.get(stack, classModel._id, classModel.name, classModel["~class"], classModel.description, classModel.schema);
|
|
708
|
+
classObj.setModel(classModel);
|
|
709
|
+
return classObj;
|
|
710
|
+
}
|
|
711
|
+
else {
|
|
712
|
+
let classObj = await _a.create(stack, classModel.name, classModel["~class"], classModel["~class"], classModel.schema);
|
|
713
|
+
classObj.setModel(classModel);
|
|
714
|
+
return classObj;
|
|
715
|
+
}
|
|
716
|
+
};
|
|
717
|
+
/**
|
|
718
|
+
* Fetches a class by its document ID.
|
|
719
|
+
*
|
|
720
|
+
* @param stack - The parent stack instance
|
|
721
|
+
* @param classId - The class document ID
|
|
722
|
+
* @returns The Class instance
|
|
723
|
+
* @throws Error if the class is not found
|
|
724
|
+
*/
|
|
725
|
+
Class.fetchById = async (stack, classId) => {
|
|
726
|
+
try {
|
|
727
|
+
let classModel = await stack.db.get(classId);
|
|
728
|
+
const classObj = await _a.buildFromModel(stack, classModel);
|
|
729
|
+
return classObj;
|
|
730
|
+
}
|
|
731
|
+
catch (error) {
|
|
732
|
+
throw new Error(`Class not found: ${classId}`);
|
|
733
|
+
}
|
|
734
|
+
};
|
|
735
|
+
/**
|
|
736
|
+
* Fetches a class by its name.
|
|
737
|
+
* This is the most common way to retrieve an existing class.
|
|
738
|
+
*
|
|
739
|
+
* @param stack - The parent stack instance
|
|
740
|
+
* @param className - The class name to fetch
|
|
741
|
+
* @returns The Class instance, or `null` if not found
|
|
742
|
+
*
|
|
743
|
+
* @example
|
|
744
|
+
* ```typescript
|
|
745
|
+
* const taskClass = await Class.fetch(stack, 'Task');
|
|
746
|
+
* if (taskClass) {
|
|
747
|
+
* const tasks = await taskClass.getCards();
|
|
748
|
+
* }
|
|
749
|
+
* ```
|
|
750
|
+
*/
|
|
751
|
+
Class.fetch = async (stack, className) => {
|
|
752
|
+
let classModel = await stack.getClassModel(className);
|
|
753
|
+
if (classModel) {
|
|
754
|
+
return _a.buildFromModel(stack, classModel);
|
|
755
|
+
}
|
|
756
|
+
else {
|
|
757
|
+
return null;
|
|
758
|
+
}
|
|
759
|
+
};
|
|
760
|
+
export default Class;
|
|
761
|
+
//# sourceMappingURL=class.js.map
|