@onehat/data 1.22.26 → 1.22.28

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,105 @@
1
+ import PropertyTypes from '../../../src/Property/index.js';
2
+ import _ from 'lodash';
3
+
4
+ describe('MixedProperty', function() {
5
+
6
+ beforeEach(function() {
7
+ const
8
+ definition = {
9
+ type: 'mixed',
10
+ types: [
11
+ 'int',
12
+ 'string',
13
+ ],
14
+ },
15
+ Property = PropertyTypes[definition.type];
16
+ this.property = new Property(definition);
17
+ });
18
+
19
+ it('className', function() {
20
+ const className = this.property.getClassName();
21
+ expect(className).to.be.eq('Mixed');
22
+ });
23
+
24
+ it('events are handled properly', function() {
25
+ this.property.on('change', (property, oldValue, newValue) => {
26
+ if (property.getCurrentType() === 'int') {
27
+ expect(oldValue).to.be.eq(null);
28
+ expect(newValue).to.be.eq(42);
29
+ } else {
30
+ expect(oldValue).to.be.eq(null);
31
+ expect(newValue).to.be.eq('here');
32
+ }
33
+ });
34
+
35
+ this.property.setValue(42);
36
+ this.property.setValue('here');
37
+ });
38
+
39
+ describe('direct methods', function() {
40
+
41
+ it('getCurrentType', function() {
42
+ const currentType = this.property.getCurrentType();
43
+ expect(currentType).to.be.eq('int');
44
+
45
+ // switch type
46
+ this.property.setValue('Hello');
47
+ const newCurrentType = this.property.getCurrentType();
48
+ expect(newCurrentType).to.be.eq('string');
49
+ });
50
+
51
+ it('getInternalProperty', function() {
52
+ const intProperty = this.property.getInternalProperty('int');
53
+ expect(intProperty).to.be.not.undefined;
54
+ expect(intProperty.constructor.type).to.be.eq('int');
55
+
56
+ // switch type
57
+ this.property.setValue('Hello');
58
+ const stringProperty = this.property.getInternalProperty('string');
59
+ expect(stringProperty).to.be.not.undefined;
60
+ expect(stringProperty.constructor.type).to.be.eq('string');
61
+ });
62
+
63
+ it('destroy', function() {
64
+ this.property.destroy();
65
+ expect(this.property.internalProperties).to.be.empty;
66
+ });
67
+
68
+ });
69
+
70
+ it('passes methods to currentProperty', function() {
71
+
72
+ this.property.setValue(42);
73
+ expect(this.property.getSubmitValue()).to.be.eq(42);
74
+ expect(this.property.getCurrentType()).to.be.eq('int');
75
+
76
+ this.property.setValue('here');
77
+ expect(this.property.getSubmitValue()).to.be.eq('here');
78
+ expect(this.property.getCurrentType()).to.be.eq('string');
79
+
80
+ });
81
+
82
+ it('passes configs to internal properties', function() {
83
+ const
84
+ definition = {
85
+ type: 'mixed',
86
+ types: [
87
+ {
88
+ type: 'date',
89
+ readFormat: 'YYYY-MM-DD',
90
+ displayFormat: 'YYYY',
91
+ submitFormat: 'YYYY-MM',
92
+ },
93
+ 'string',
94
+ ],
95
+ },
96
+ Property = PropertyTypes[definition.type],
97
+ property = new Property(definition);
98
+
99
+ property.setValue('2025-10-17');
100
+ expect(property.getSubmitValue()).to.be.eq('2025-10');
101
+ expect(property.getDisplayValue()).to.be.eq('2025');
102
+ expect(property.getCurrentType()).to.be.eq('date');
103
+ });
104
+
105
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onehat/data",
3
- "version": "1.22.26",
3
+ "version": "1.22.28",
4
4
  "description": "JS data modeling package with adapters for many storage mediums.",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
@@ -38,11 +38,11 @@
38
38
  "dependencies": {
39
39
  "@onehat/events": "^1.6.6",
40
40
  "accounting-js": "^2.0.3",
41
- "async-wait-until": "^2.0.27",
42
- "axios": "^1.11.0",
43
- "chrono-node": "^2.8.3",
41
+ "async-wait-until": "^2.0.31",
42
+ "axios": "^1.13.0",
43
+ "chrono-node": "^2.9.0",
44
44
  "he": "^1.2.0",
45
- "js-base64": "^3.7.7",
45
+ "js-base64": "^3.7.8",
46
46
  "lodash": "^4.17.21",
47
47
  "moment": "^2.30.1",
48
48
  "natsort": "^2.0.3",
@@ -50,7 +50,7 @@
50
50
  "object-hash": "^3.0.0",
51
51
  "qs": "^6.14.0",
52
52
  "relative-time-parser": "^1.0.15",
53
- "uuid": "^11.1.0"
53
+ "uuid": "^13.0.0"
54
54
  },
55
55
  "peerDependencies": {
56
56
  "fast-xml-parser": "^4.4.1",
@@ -0,0 +1,408 @@
1
+ /** @module Property */
2
+
3
+ import EventEmitter from '@onehat/events';
4
+ import Base64Property from './Base64.js';
5
+ import BooleanProperty from './Boolean.js';
6
+ import CurrencyProperty from './Currency.js';
7
+ import DateProperty from './Date.js';
8
+ import DateTimeProperty from './DateTime.js';
9
+ import FileProperty from './File.js';
10
+ import FloatProperty from './Float.js';
11
+ import IntegerProperty from './Integer.js';
12
+ import JsonProperty, { TagProperty } from './Json.js';
13
+ import PercentProperty from './Percent.js';
14
+ import PercentIntProperty from './PercentInt.js';
15
+ import StringProperty from './String.js';
16
+ import TimeProperty from './Time.js';
17
+ import UuidProperty from './Uuid.js';
18
+ import _ from 'lodash';
19
+
20
+ /**
21
+ * Class represents a Property that can store values of multiple types.
22
+ * The actual type is determined dynamically based on the value being set.
23
+ *
24
+ * Usage: { name: 'date', title: 'Date', types: ['date', 'string'], },
25
+ * This is primarily used to allow a field that's normally one PropertyType
26
+ * to also accept string values (e.g. values like '2025-01-01' or 'N/A').
27
+ *
28
+ * @extends Property
29
+ */
30
+ export default class MixedProperty extends EventEmitter {
31
+
32
+ constructor(config = {}, entity) {
33
+ config = _.merge({}, MixedProperty.defaults, config);
34
+
35
+ if (!config.types || !Array.isArray(config.types) || config.types.length < 2) {
36
+ throw Error('MixedProperty requires a types array with at least two types in its configuration.');
37
+ }
38
+
39
+ super(config, entity);
40
+
41
+ this.registerEvents([
42
+ 'change',
43
+ 'changeValidity',
44
+ 'destroy',
45
+ ]);
46
+
47
+ this.types = config.types;
48
+ this.currentType = this.types[0].type || this.types[0];
49
+ this.internalProperties = new Map();
50
+
51
+ this._createInternalProperties();
52
+
53
+ this.currentProperty = this.internalProperties.get(this.currentType);
54
+
55
+ this._proxy = new Proxy(this, {
56
+ get(mixedProperty, prop, receiver) {
57
+ if (prop in mixedProperty) {
58
+ const value = mixedProperty[prop];
59
+ if (typeof value === 'function') {
60
+ return value.bind(mixedProperty);
61
+ }
62
+ return value;
63
+ }
64
+ if (mixedProperty.currentProperty && prop in mixedProperty.currentProperty) {
65
+ const value = mixedProperty.currentProperty[prop];
66
+ if (typeof value === 'function') {
67
+ return value.bind(mixedProperty.currentProperty);
68
+ }
69
+ return value;
70
+ }
71
+ return undefined;
72
+ },
73
+ has(mixedProperty, prop) {
74
+ if (prop in mixedProperty) {
75
+ return true;
76
+ }
77
+ return mixedProperty.currentProperty ? prop in mixedProperty.currentProperty : false;
78
+ }
79
+ });
80
+
81
+ return this._proxy;
82
+ }
83
+
84
+ /**
85
+ * Creates internal Property instances for each configured type
86
+ * @private
87
+ */
88
+ _createInternalProperties() {
89
+ _.each(this.types, (typeConfig) => {
90
+ let typeName,
91
+ propertyConfig;
92
+ if (typeof typeConfig === 'string') {
93
+ typeName = typeConfig;
94
+ propertyConfig = {};
95
+ } else {
96
+ typeName = typeConfig.type;
97
+ propertyConfig = _.omit(typeConfig, 'type');
98
+ }
99
+
100
+ const
101
+ PropertyClass = this._getPropertyClass(typeName),
102
+
103
+ // Create a clean config for this internal property
104
+ baseConfig = _.omit(this.config, ['types', 'defaultType']), // Start with base config but exclude Mixed-specific settings
105
+ mergedConfig = _.merge({}, baseConfig, propertyConfig, { // Merge with type-specific config, giving precedence to type-specific settings
106
+ name: this.name,
107
+ }),
108
+ property = new PropertyClass(mergedConfig, this.entity);
109
+
110
+ this._setupEventForwarding(property);
111
+
112
+ this.internalProperties.set(typeName, property);
113
+ });
114
+ }
115
+
116
+ /**
117
+ * Gets the Property class for a given type name
118
+ * @param {string} typeName - Name of the property type
119
+ * @returns {Class} Property class
120
+ * @private
121
+ */
122
+ _getPropertyClass(typeName) {
123
+ const
124
+ typeMap = {
125
+ base64: Base64Property,
126
+ bool: BooleanProperty,
127
+ currency: CurrencyProperty,
128
+ date: DateProperty,
129
+ datetime: DateTimeProperty,
130
+ file: FileProperty,
131
+ float: FloatProperty,
132
+ int: IntegerProperty,
133
+ json: JsonProperty,
134
+ percent: PercentProperty,
135
+ percentint: PercentIntProperty,
136
+ string: StringProperty,
137
+ tag: TagProperty,
138
+ time: TimeProperty,
139
+ uuid: UuidProperty,
140
+ },
141
+ PropertyClass = typeMap[typeName.toLowerCase()];
142
+
143
+ if (!PropertyClass) {
144
+ const availableTypes = Object.keys(typeMap).join(', ');
145
+ throw new Error(`Unknown property type: '${typeName}'. Available types are: ${availableTypes}`);
146
+ }
147
+
148
+ return typeMap[typeName.toLowerCase()];
149
+ }
150
+
151
+ /**
152
+ * Sets up event forwarding from an internal property to this MixedProperty
153
+ * @param {Property} property - Internal property to forward events from
154
+ * @private
155
+ */
156
+ _setupEventForwarding(property) {
157
+ // Forward all events from internal property to MixedProperty
158
+ const forwardEvent = (eventName, ...args) => {
159
+ if (property === this.currentProperty) {
160
+ // Replace the property reference in args with this MixedProperty
161
+ const modifiedArgs = args.map(arg => arg === property ? this : arg);
162
+ this.emit(eventName, ...modifiedArgs);
163
+ }
164
+ };
165
+
166
+ // Listen to all registered events on the internal property
167
+ property.getRegisteredEvents().forEach((eventName) => {
168
+ property.on(eventName, (...args) => forwardEvent(eventName, ...args));
169
+ });
170
+ }
171
+
172
+ /**
173
+ * Detects the appropriate property type for a given value
174
+ * @param {any} value - Value to analyze
175
+ * @returns {string} Detected type name
176
+ * @private
177
+ */
178
+ _detectType(value) {
179
+ if (_.isNil(value)) {
180
+ return this.currentType || this.defaultType || (typeof this.types[0] === 'string' ? this.types[0] : this.types[0].type);
181
+ }
182
+
183
+ // Get available type names from config
184
+ const availableTypes = this.types.map(typeConfig =>
185
+ typeof typeConfig === 'string' ? typeConfig : typeConfig.type
186
+ );
187
+
188
+ // Define precedence order - more specific types first
189
+ const precedenceOrder = [
190
+ 'bool', // Most specific - only true/false/1/0
191
+ 'int', // Integers
192
+ 'float', // Floating point numbers
193
+ 'currency', // Currency values
194
+ 'percent', // Percentage values
195
+ 'percentint',// Integer percentages
196
+ 'date', // Date values
197
+ 'datetime', // DateTime values
198
+ 'time', // Time values
199
+ 'uuid', // UUID format
200
+ 'base64', // Base64 encoded data
201
+ 'json', // JSON objects/arrays
202
+ 'tag', // Tag format
203
+ 'file', // File data
204
+ 'string' // Most general - accepts anything
205
+ ];
206
+
207
+ // Try types in precedence order, but only if they're in the available types
208
+ let typeName;
209
+ for(typeName of precedenceOrder) {
210
+ if (availableTypes.includes(typeName)) {
211
+ const property = this.internalProperties.get(typeName);
212
+ if (property && this._canParseValue(property, value)) {
213
+ return typeName;
214
+ }
215
+ }
216
+ }
217
+
218
+ throw new Error(`No configured property type could handle value: ${value}. Available types: ${availableTypes.join(', ')}`);
219
+ }
220
+
221
+ /**
222
+ * Tests if a property can successfully parse a value
223
+ * @param {Property} property - Property to test
224
+ * @param {any} value - Value to test
225
+ * @returns {boolean} Whether the property can parse the value
226
+ * @private
227
+ */
228
+ _canParseValue(property, value) {
229
+ try {
230
+ switch(property.constructor.type) {
231
+ case 'bool':
232
+ return this._isBooleanValue(value);
233
+ case 'int':
234
+ return this._isIntegerValue(value);
235
+ case 'float':
236
+ return this._isFloatValue(value);
237
+ }
238
+
239
+ property.parse(value);
240
+ return true;
241
+ } catch (error) {
242
+ return false;
243
+ }
244
+ }
245
+
246
+ /**
247
+ * Checks if a value represents a boolean
248
+ * @param {any} value - Value to check
249
+ * @returns {boolean} Whether the value is boolean-like
250
+ * @private
251
+ */
252
+ _isBooleanValue(value) {
253
+ switch(typeof value) {
254
+ case 'boolean':
255
+ return true;
256
+ case 'number':
257
+ return value === 0 || value === 1;
258
+ case 'string':
259
+ const lower = value.toLowerCase().trim();
260
+ return ['true', 'false', '1', '0'].includes(lower);
261
+ default:
262
+ return false;
263
+ }
264
+ }
265
+
266
+ /**
267
+ * Checks if a value represents an integer
268
+ * @param {any} value - Value to check
269
+ * @returns {boolean} Whether the value is integer-like
270
+ * @private
271
+ */
272
+ _isIntegerValue(value) {
273
+ switch(typeof value) {
274
+ case 'bigint':
275
+ return true;
276
+ case 'number':
277
+ return Number.isInteger(value);
278
+ case 'string':
279
+ const trimmed = value.trim();
280
+ if (trimmed === '') {
281
+ return false;
282
+ }
283
+ const num = Number(trimmed);
284
+ return !isNaN(num) && Number.isInteger(num) && String(num) === trimmed;
285
+ }
286
+ return false;
287
+ }
288
+
289
+ /**
290
+ * Checks if a value represents a float
291
+ * @param {any} value - Value to check
292
+ * @returns {boolean} Whether the value is float-like
293
+ * @private
294
+ */
295
+ _isFloatValue(value) {
296
+ switch(typeof value) {
297
+ case 'number':
298
+ return !Number.isInteger(value) && isFinite(value);
299
+ case 'string':
300
+ const trimmed = value.trim();
301
+ if (trimmed === '') return false;
302
+ const num = Number(trimmed);
303
+ return !isNaN(num) && isFinite(num) && !Number.isInteger(num);
304
+ }
305
+ return false;
306
+ }
307
+
308
+ /**
309
+ * Sets the value and determines the appropriate type
310
+ * @param {any} value - Value to set
311
+ */
312
+ setValue(value) {
313
+ const detectedType = this._detectType(value);
314
+ if (this.currentType !== detectedType) {
315
+ this.currentType = detectedType;
316
+ this.currentProperty = this.internalProperties.get(this.currentType);
317
+ }
318
+ this.currentProperty.setValue(value);
319
+ }
320
+
321
+ /**
322
+ * Gets the current type name
323
+ * @returns {string} Current type name
324
+ */
325
+ getCurrentType() {
326
+ return this.currentProperty.constructor.type;
327
+ }
328
+
329
+ /**
330
+ * Gets the className of this Property type.
331
+ * @return {string} className
332
+ */
333
+ getClassName() {
334
+ return this.constructor.className;
335
+ }
336
+
337
+ /**
338
+ * Gets the internal property for a specific type
339
+ * @param {string} typeName - Type name
340
+ * @returns {Property} Internal property
341
+ */
342
+ getInternalProperty(typeName) {
343
+ return this.internalProperties.get(typeName);
344
+ }
345
+
346
+ // ______ __
347
+ // / ____/ _____ ____ / /______
348
+ // / __/ | | / / _ \/ __ \/ __/ ___/
349
+ // / /___ | |/ / __/ / / / /_(__ )
350
+ // /_____/ |___/\___/_/ /_/\__/____/
351
+ //
352
+ // (Override the EventEmitter methods to forward listener management to internal properties)
353
+
354
+
355
+ /**
356
+ * Add event listener to MixedProperty only
357
+ * Events from internal properties are forwarded via _setupEventForwarding
358
+ */
359
+ on(eventName, listener) {
360
+ return super.on(eventName, listener);
361
+ }
362
+
363
+ /**
364
+ * Add one-time event listener to MixedProperty only
365
+ */
366
+ once(eventName, listener) {
367
+ return super.once(eventName, listener);
368
+ }
369
+
370
+ /**
371
+ * Remove event listener from MixedProperty only
372
+ */
373
+ off(eventName, listener) {
374
+ return super.off(eventName, listener);
375
+ }
376
+
377
+ /**
378
+ * Remove all event listeners from MixedProperty and all internal properties
379
+ */
380
+ removeAllListeners(eventName) {
381
+ // Remove all listeners from this MixedProperty
382
+ super.removeAllListeners(eventName);
383
+
384
+ // Remove all listeners from internal properties
385
+ this.internalProperties.forEach(property => {
386
+ property.removeAllListeners(eventName);
387
+ });
388
+
389
+ return this;
390
+ }
391
+
392
+ /**
393
+ * Destroys all internal properties
394
+ */
395
+ destroy() {
396
+ // Clean up listener mappings
397
+ if (this._listenerMappings) {
398
+ this._listenerMappings.clear();
399
+ this._listenerMappings = null;
400
+ }
401
+
402
+ this.internalProperties.forEach((property) => property.destroy());
403
+ this.internalProperties.clear();
404
+ }
405
+ }
406
+
407
+ MixedProperty.className = 'Mixed';
408
+ MixedProperty.type = 'mixed';
@@ -9,6 +9,7 @@ import FileProperty from './File.js';
9
9
  import FloatProperty from './Float.js';
10
10
  import IntegerProperty from './Integer.js';
11
11
  import JsonProperty, { TagProperty } from './Json.js';
12
+ import MixedProperty from './Mixed.js';
12
13
  import PercentProperty from './Percent.js';
13
14
  import PercentIntProperty from './PercentInt.js';
14
15
  import Property from './Property.js';
@@ -26,6 +27,7 @@ const PropertyTypes = {
26
27
  [FloatProperty.type]: FloatProperty,
27
28
  [IntegerProperty.type]: IntegerProperty,
28
29
  [JsonProperty.type]: JsonProperty,
30
+ [MixedProperty.type]: MixedProperty,
29
31
  [PercentProperty.type]: PercentProperty,
30
32
  [PercentIntProperty.type]: PercentIntProperty,
31
33
  [Property.type]: Property,
@@ -544,20 +544,30 @@ class OneBuildRepository extends AjaxRepository {
544
544
  }
545
545
 
546
546
  return this.axios(data)
547
- .then((result) => {
548
- if (this.debugMode) {
549
- console.log('login response', result);
550
- }
547
+ .catch((error) => {
551
548
 
552
- const response = result.data;
553
- if (!response.success) {
554
- this.throwError(response.data); // TODO: Fix back-end, so OneBuild submits the error message on response.message, not response.data
555
- return false;
556
- }
549
+ if (this.debugMode) {
550
+ console.log(data.url + ' error', error);
551
+ console.log('response:', error.response);
552
+ }
553
+
554
+ this.throwError(error);
555
+ return;
556
+ })
557
+ .then((result) => {
558
+ if (this.debugMode) {
559
+ console.log('login response', result);
560
+ }
557
561
 
558
- const userData = response.data;
559
- return userData;
560
- });
562
+ const response = result.data;
563
+ if (!response.success) {
564
+ this.throwError(response.data); // TODO: Fix back-end, so OneBuild submits the error message on response.message, not response.data
565
+ return false;
566
+ }
567
+
568
+ const userData = response.data;
569
+ return userData;
570
+ });
561
571
  }
562
572
 
563
573
  /**