@acodeninja/persist 2.2.1 → 2.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,10 @@
1
+ version = 1
2
+
3
+ [[analyzers]]
4
+ name = "javascript"
5
+
6
+ [analyzers.meta]
7
+ environment = [
8
+ "nodejs",
9
+ "browser"
10
+ ]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@acodeninja/persist",
3
- "version": "2.2.1",
3
+ "version": "2.2.3",
4
4
  "description": "A JSON based data modelling and persistence module with alternate storage mechanisms.",
5
5
  "type": "module",
6
6
  "scripts": {
package/src/Persist.js CHANGED
@@ -4,7 +4,7 @@ import enableTransactions from './Transactions.js';
4
4
  /**
5
5
  * @class Persist
6
6
  */
7
- export default class Persist {
7
+ class Persist {
8
8
  static _engine = {};
9
9
  /**
10
10
  * @memberof Persist
@@ -42,3 +42,5 @@ export default class Persist {
42
42
  engine.configure(configuration);
43
43
  }
44
44
  }
45
+
46
+ export default Persist;
package/src/Query.js CHANGED
@@ -1,61 +1,117 @@
1
1
  /**
2
- * persist query language features:
3
- * - value match {title: 'test'} or {title: {$is: 'test'}}
4
- * - contains match {list: {$contains: 'test'}} or {string: {$contains: 'es'}}
5
- * - nested query {list: {$contains: {slug: 'test'}}}
6
- * - deep nesting queries {list: {$contains: {string: {$contains: 'test'}}}}
7
- */
8
-
9
- /**
10
- * @class Query
2
+ * The `Query` class is responsible for executing searches on an indexed dataset
3
+ * based on a structured query. It supports various query types including value matches,
4
+ * contains matches, and nested queries.
5
+ *
6
+ * @example
7
+ * // The object has the property `title` witch exactly equals `test`.
8
+ * const query = new Query({title: 'test'});
9
+ * const query = new Query({title: {$is: 'test'}});
10
+ *
11
+ * // The object has the property `list` witch contains the string `test`.
12
+ * const query = new Query({list: {$contains: 'test'}});
13
+ *
14
+ * // The object has the property `string` witch contains the string `es`.
15
+ * const query = new Query({string: {$contains: 'es'}});
16
+ *
17
+ * // The object has the property `list` contains an object
18
+ * // with a property `string` that contains the string `test`.
19
+ * const query = new Query({
20
+ * list: {
21
+ * $contains: {
22
+ * string: {
23
+ * $contains: 'test'
24
+ * }
25
+ * }
26
+ * }
27
+ * });
11
28
  */
12
29
  class Query {
30
+ /**
31
+ * The query object that defines the search criteria.
32
+ * @type {Object}
33
+ */
13
34
  query;
14
35
 
15
36
  /**
37
+ * Constructs a new `Query` instance with the provided query object.
16
38
  *
17
- * @param {object} query
39
+ * @param {Object} query - The structured query object defining the search criteria.
18
40
  */
19
41
  constructor(query) {
20
42
  this.query = query;
21
43
  }
22
44
 
23
45
  /**
24
- * Using the input query, find records in an index that match
46
+ * Executes the query against a model's index and returns the matching results.
25
47
  *
26
- * @param {typeof Model} model
27
- * @param {object} index
48
+ * @param {Model.constructor} model - The model class that contains the `fromData` method for constructing models from data.
49
+ * @param {Object<string, Model>} index - The index dataset to search through.
50
+ * @returns {Array<Model>} The models that match the query.
28
51
  */
29
52
  execute(model, index) {
30
- const matchIs = (query) => !!query?.$is;
31
- const matchPrimitive = (query) => ['string', 'number', 'boolean'].includes(typeof query);
32
- const matchContains = (query) => !!query?.$contains;
33
-
34
- const matchesQuery = (subject, inputQuery = this.query) => {
35
- if (!subject || !inputQuery) return false;
53
+ return Object.values(index)
54
+ .filter(m =>
55
+ this._splitQuery(this.query)
56
+ .map(query => Boolean(this._matchesQuery(m, query)))
57
+ .every(c => c),
58
+ )
59
+ .map(m => model.fromData(m));
60
+ }
36
61
 
37
- if (matchPrimitive(inputQuery)) return subject === inputQuery;
62
+ /**
63
+ * Recursively checks if a subject matches a given query.
64
+ *
65
+ * This function supports matching:
66
+ * - Primitive values directly (`string`, `number`, `boolean`)
67
+ * - The `$is` property for exact matches
68
+ * - The `$contains` property for substring or array element matches
69
+ *
70
+ * @private
71
+ * @param {*} subject - The subject to be matched.
72
+ * @param {Object} [inputQuery=this.query] - The query to match against. Defaults to `this.query` if not provided.
73
+ * @returns {boolean} True if the subject matches the query, otherwise false.
74
+ */
75
+ _matchesQuery(subject, inputQuery = this.query) {
76
+ if (['string', 'number', 'boolean'].includes(typeof inputQuery)) return subject === inputQuery;
38
77
 
39
- if (matchIs(inputQuery))
40
- if (subject === inputQuery.$is) return true;
78
+ if (inputQuery?.$is !== undefined && subject === inputQuery.$is) return true;
41
79
 
42
- if (matchContains(inputQuery)) {
43
- if (subject.includes?.(inputQuery.$contains)) return true;
80
+ if (inputQuery?.$contains !== undefined) {
81
+ if (subject.includes?.(inputQuery.$contains)) return true;
44
82
 
45
- for (const value of subject) {
46
- if (matchesQuery(value, inputQuery.$contains)) return true;
47
- }
83
+ for (const value of subject) {
84
+ if (this._matchesQuery(value, inputQuery.$contains)) return true;
48
85
  }
86
+ }
49
87
 
50
- for (const key of Object.keys(inputQuery)) {
51
- if (!['$is', '$contains'].includes(key))
52
- if (matchesQuery(subject[key], inputQuery[key])) return true;
53
- }
54
- };
88
+ for (const key of Object.keys(inputQuery)) {
89
+ if (!['$is', '$contains'].includes(key))
90
+ if (this._matchesQuery(subject[key], inputQuery[key])) return true;
91
+ }
55
92
 
56
- return Object.values(index)
57
- .filter(m => matchesQuery(m))
58
- .map(m => model.fromData(m));
93
+ return false;
94
+ };
95
+
96
+ /**
97
+ * Recursively splits an object into an array of objects,
98
+ * where each key-value pair from the input query becomes a separate object.
99
+ *
100
+ * If the value of a key is a nested object (and not an array),
101
+ * the function recursively splits it, preserving the parent key.
102
+ *
103
+ * @private
104
+ * @param {Object} query - The input object to be split into individual key-value pairs.
105
+ * @returns {Array<Object>} An array of objects, where each object contains a single key-value pair
106
+ * from the original query or its nested objects.
107
+ */
108
+ _splitQuery(query) {
109
+ return Object.entries(query)
110
+ .flatMap(([key, value]) =>
111
+ typeof value === 'object' && value !== null && !Array.isArray(value)
112
+ ? this._splitQuery(value).map(nestedObj => ({[key]: nestedObj}))
113
+ : {[key]: value},
114
+ );
59
115
  }
60
116
  }
61
117
 
@@ -4,13 +4,21 @@ import ajvErrors from 'ajv-errors';
4
4
  import ajvFormats from 'ajv-formats';
5
5
 
6
6
  /**
7
- * @class SchemaCompiler
7
+ * A class responsible for compiling raw schema definitions into a format that can be validated using the AJV (Another JSON Validator) library.
8
8
  */
9
- export default class SchemaCompiler {
9
+ class SchemaCompiler {
10
10
  /**
11
- * @method compile
12
- * @param {Model|object} rawSchema
13
- * @return {CompiledSchema}
11
+ * Compiles a raw schema into a validation-ready schema, and returns a class that extends `CompiledSchema`.
12
+ *
13
+ * This method converts a given schema into a JSON schema-like format, setting up properties, types, formats, and validation rules.
14
+ * It uses AJV for the validation process and integrates with model types and their specific validation rules.
15
+ *
16
+ * @param {Object|Model} rawSchema - The raw schema or model definition to be compiled.
17
+ * @returns {CompiledSchema} - A class that extends `CompiledSchema`, with the compiled schema and validator attached.
18
+ *
19
+ * @example
20
+ * const schemaClass = SchemaCompiler.compile(MyModelSchema);
21
+ * const isValid = schemaClass.validate(data); // Throws ValidationError if data is invalid.
14
22
  */
15
23
  static compile(rawSchema) {
16
24
  const validation = new ajv({allErrors: true});
@@ -27,7 +35,7 @@ export default class SchemaCompiler {
27
35
 
28
36
  if (Type.Model.isModel(rawSchema)) {
29
37
  schema.required.push('id');
30
- schema.properties['id'] = {type: 'string'};
38
+ schema.properties.id = {type: 'string'};
31
39
  }
32
40
 
33
41
  for (const [name, type] of Object.entries(rawSchema)) {
@@ -88,7 +96,20 @@ export default class SchemaCompiler {
88
96
  }
89
97
 
90
98
  class Schema extends CompiledSchema {
99
+ /**
100
+ * The compiled schema definition.
101
+ * @type {Object}
102
+ * @static
103
+ * @private
104
+ */
91
105
  static _schema = schema;
106
+
107
+ /**
108
+ * The AJV validator function compiled from the schema.
109
+ * @type {Function}
110
+ * @static
111
+ * @private
112
+ */
92
113
  static _validator = validation.compile(schema);
93
114
  }
94
115
 
@@ -96,20 +117,36 @@ export default class SchemaCompiler {
96
117
  }
97
118
  }
98
119
 
120
+
99
121
  /**
100
- * @class CompiledSchema
101
- * @property {object} _schema
102
- * @property {Function} _validator
122
+ * Represents a compiled schema used for validating data models.
123
+ * This class provides a mechanism to validate data using a precompiled schema and a validator function.
103
124
  */
104
125
  export class CompiledSchema {
126
+ /**
127
+ * The schema definition for validation, typically a precompiled JSON schema or similar.
128
+ * @type {?Object}
129
+ * @static
130
+ * @private
131
+ */
105
132
  static _schema = null;
133
+
134
+ /**
135
+ * The validator function used to validate data against the schema.
136
+ * @type {?Function}
137
+ * @static
138
+ * @private
139
+ */
106
140
  static _validator = null;
107
141
 
108
142
  /**
109
- * @method validate
110
- * @param data
111
- * @return {boolean}
112
- * @throws {ValidationError}
143
+ * Validates the given data against the compiled schema.
144
+ *
145
+ * If the data is an instance of a model, it will be converted to a plain object via `toData()` before validation.
146
+ *
147
+ * @param {Object|Model} data - The data or model instance to be validated.
148
+ * @returns {boolean} - Returns `true` if the data is valid according to the schema.
149
+ * @throws {ValidationError} - Throws a `ValidationError` if the data is invalid.
113
150
  */
114
151
  static validate(data) {
115
152
  let inputData = Object.assign({}, data);
@@ -127,15 +164,29 @@ export class CompiledSchema {
127
164
  }
128
165
 
129
166
  /**
130
- * @class ValidationError
131
- * @extends Error
132
- * @property {object[]} errors
133
- * @property {object} data
167
+ * Represents a validation error that occurs when a model or data fails validation.
168
+ * Extends the built-in JavaScript `Error` class.
134
169
  */
135
170
  export class ValidationError extends Error {
171
+ /**
172
+ * Creates an instance of `ValidationError`.
173
+ *
174
+ * @param {Object} data - The data that failed validation.
175
+ * @param {Array<Object>} errors - A list of validation errors, each typically containing details about what failed.
176
+ */
136
177
  constructor(data, errors) {
137
178
  super('Validation failed');
179
+ /**
180
+ * An array of validation errors, containing details about each failed validation.
181
+ * @type {Array<Object>}
182
+ */
138
183
  this.errors = errors;
184
+ /**
185
+ * The data that caused the validation error.
186
+ * @type {Object}
187
+ */
139
188
  this.data = data;
140
189
  }
141
190
  }
191
+
192
+ export default SchemaCompiler;
@@ -1,32 +1,110 @@
1
+ /**
2
+ * Class representing a transaction-related error.
3
+ *
4
+ * @class TransactionError
5
+ * @extends Error
6
+ */
1
7
  class TransactionError extends Error {
2
8
  }
3
9
 
10
+ /**
11
+ * Error thrown when a transaction is already committed.
12
+ *
13
+ * @class TransactionCommittedError
14
+ * @extends TransactionError
15
+ */
4
16
  export class TransactionCommittedError extends TransactionError {
17
+ /**
18
+ * Creates an instance of TransactionCommittedError.
19
+ * This error is thrown when attempting to commit an already committed transaction.
20
+ * @property {string} message - The error message.
21
+ */
5
22
  message = 'Transaction was already committed.';
6
23
  }
7
24
 
25
+ /**
26
+ * Enables transaction support for the provided engine.
27
+ *
28
+ * This function enhances an engine class with transaction capabilities, allowing multiple
29
+ * changes to be grouped into a single transaction that can be committed or rolled back.
30
+ *
31
+ * @param {Engine.constructor} engine - The base engine class to be enhanced with transaction support.
32
+ * @returns {TransactionalEngine.constructor} TransactionalEngine - The enhanced engine class with transaction functionality.
33
+ */
8
34
  export default function enableTransactions(engine) {
35
+ /**
36
+ * A class representing an engine with transaction capabilities.
37
+ * @class TransactionalEngine
38
+ * @extends {engine}
39
+ */
9
40
  class TransactionalEngine extends engine {
10
41
  }
11
42
 
43
+ /**
44
+ * Starts a transaction on the engine. Returns a Transaction class that can handle
45
+ * put, commit, and rollback actions for the transaction.
46
+ *
47
+ * @returns {Transaction.constructor} Transaction - A class that manages the transaction's operations.
48
+ */
12
49
  TransactionalEngine.start = () => {
50
+ /**
51
+ * A class representing an active transaction on the engine.
52
+ * Contains methods to put changes, commit the transaction, or roll back in case of failure.
53
+ *
54
+ * @class Transaction
55
+ */
13
56
  class Transaction extends TransactionalEngine {
57
+ /**
58
+ * @property {Array<Object>} transactions - An array storing all the operations within the transaction.
59
+ * @static
60
+ */
14
61
  static transactions = [];
62
+
63
+ /**
64
+ * @property {boolean} committed - Indicates if the transaction has been committed.
65
+ * @static
66
+ */
15
67
  static committed = false;
68
+
69
+ /**
70
+ * @property {boolean} failed - Indicates if the transaction has failed.
71
+ * @static
72
+ */
16
73
  static failed = false;
17
74
 
18
- static async put(model) {
75
+ /**
76
+ * Adds a model to the transaction queue.
77
+ *
78
+ * @param {Object} model - The model to be added to the transaction.
79
+ * @returns {Promise<void>} A promise that resolves once the model is added.
80
+ */
81
+ static put(model) {
19
82
  this.transactions.push({
20
83
  hasRun: false,
21
84
  hasRolledBack: false,
22
85
  model,
23
86
  });
87
+
88
+ return Promise.resolve();
24
89
  }
25
90
 
91
+ /**
92
+ * Checks if the transaction has already been committed. If true, throws a TransactionCommittedError.
93
+ *
94
+ * @throws {TransactionCommittedError} If the transaction has already been committed.
95
+ * @private
96
+ */
26
97
  static _checkCommitted() {
27
98
  if (this.committed) throw new TransactionCommittedError();
28
99
  }
29
100
 
101
+ /**
102
+ * Commits the transaction, applying all the changes to the engine.
103
+ * Rolls back if any part of the transaction fails.
104
+ *
105
+ * @returns {Promise<void>} A promise that resolves once the transaction is committed, or rejects if an error occurs.
106
+ * @throws {Error} If any operation in the transaction fails.
107
+ */
30
108
  static async commit() {
31
109
  this._checkCommitted();
32
110
 
@@ -34,7 +112,7 @@ export default function enableTransactions(engine) {
34
112
  for (const [index, {model}] of this.transactions.entries()) {
35
113
  try {
36
114
  this.transactions[index].original = await engine.get(model.constructor, model.id);
37
- } catch (_) {
115
+ } catch (_error) {
38
116
  this.transactions[index].original = null;
39
117
  }
40
118