@onehat/data 1.22.36 → 1.22.37

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,65 @@
1
+ # Copilot Instructions
2
+
3
+ ## General Principles
4
+ - Be direct, concise, and critical. Do not apologize, agree blindly, be sycophantic, or use flattery.
5
+ - If a request is inefficient or flawed, point it out and suggest a better, more secure, or more idiomatic approach.
6
+ - Use "Uncle Bob's Clean Code" principles as a baseline for code quality.
7
+ - Prioritize clarity over cleverness.
8
+ - Avoid unnecessary abstractions.
9
+ - Prefer explicit code over magic.
10
+ - Follow the project's existing coding style and conventions.
11
+ - Do not introduce new dependencies unless absolutely necessary.
12
+ - Always consider security implications and best practices.
13
+ - Aim for full test coverage of new code, and suggest tests for existing code when appropriate.
14
+
15
+ <!-- ## Coding Standards
16
+
17
+ ### Naming
18
+ - Use descriptive variable and function names.
19
+ - Do not abbreviate unless universally understood.
20
+ - Use camelCase for JavaScript variables and functions.
21
+ - Use PascalCase for React components.
22
+ - Use snake_case for database columns.
23
+
24
+ ### Comments
25
+ - Do not write obvious comments.
26
+ - Only comment non-obvious business logic.
27
+ - Prefer self-documenting code.
28
+
29
+ ## Error Handling
30
+ - Always handle errors explicitly.
31
+ - Do not swallow exceptions.
32
+ - Return meaningful error messages.
33
+
34
+ ## Security
35
+ - Never expose secrets or API keys.
36
+ - Validate and sanitize all user input.
37
+ - Use prepared statements for database queries.
38
+
39
+ ## Project-Specific Notes
40
+
41
+ ### Backend (PHP / CakePHP)
42
+ - Follow PSR-12 formatting.
43
+ - Use dependency injection where appropriate.
44
+ - Do not use deprecated framework methods.
45
+ - Prefer modern APIs over legacy helpers.
46
+
47
+ ### Frontend (React / Expo)
48
+ - Use functional components only.
49
+ - Prefer hooks over class components.
50
+ - Avoid inline styles unless necessary.
51
+ - Keep components under 200 lines.
52
+
53
+ ## Testing
54
+ - Suggest unit tests for new logic.
55
+ - Mock external dependencies.
56
+ - Keep tests deterministic.
57
+
58
+ ## Performance
59
+ - Avoid unnecessary loops.
60
+ - Do not perform database queries inside loops.
61
+ - Prefer memoization when appropriate.
62
+
63
+ ## Output Expectations
64
+ - Produce production-ready code.
65
+ - Avoid TODO comments unless necessary. -->
@@ -1214,6 +1214,56 @@ describe('Repository Base', function() {
1214
1214
  expect(unmapped).to.be.eql(data);
1215
1215
  });
1216
1216
 
1217
+ it('unmapData with scalar vs nested mapping conflict', function() {
1218
+ // Test for the WorkOrders issue:
1219
+ // work_orders__meter_reading (scalar) maps to 'meter_reading'
1220
+ // meter_readings__id, meter_readings__date, etc. (nested) map to 'meter_reading.id', 'meter_reading.date', etc.
1221
+ // The scalar mapping should take precedence, and nested properties should be skipped
1222
+ const
1223
+ schema = new Schema({
1224
+ name: 'conflict_test',
1225
+ model: {
1226
+ idProperty: 'id',
1227
+ displayProperty: 'name',
1228
+ properties: [
1229
+ { name: 'id', type: 'int' },
1230
+ { name: 'name' },
1231
+ { name: 'meter_reading_scalar', mapping: 'meter_reading', type: 'int', defaultValue: null }, // scalar
1232
+ { name: 'meter_readings__id', mapping: 'meter_reading.id', type: 'int', defaultValue: null }, // nested (conflict!)
1233
+ { name: 'meter_readings__date', mapping: 'meter_reading.date', type: 'date', defaultValue: null }, // nested (conflict!)
1234
+ { name: 'meter_readings__value', mapping: 'meter_reading.value', type: 'int', defaultValue: null }, // nested (conflict!)
1235
+ ],
1236
+ },
1237
+ });
1238
+ this.repository.schema = schema;
1239
+
1240
+ // Test unmapping: scalar value should be preserved, nested properties should be skipped
1241
+ const mapped = {
1242
+ id: 1,
1243
+ name: 'test',
1244
+ meter_reading_scalar: 42, // The scalar value that should be preserved
1245
+ meter_readings__id: 999, // Should be skipped due to conflict
1246
+ meter_readings__date: '2025-01-01', // Should be skipped due to conflict
1247
+ meter_readings__value: 888, // Should be skipped due to conflict
1248
+ };
1249
+
1250
+ const unmapped = this.repository.unmapData(mapped);
1251
+
1252
+ // The scalar value should be preserved
1253
+ expect(unmapped.meter_reading).to.equal(42);
1254
+
1255
+ // The nested properties should NOT have created an object structure
1256
+ // because they were skipped due to the conflict
1257
+ if (typeof unmapped.meter_reading === 'number') {
1258
+ // Correct: scalar value is intact and is a number
1259
+ expect(unmapped.meter_reading).to.equal(42);
1260
+ } else {
1261
+ // If unmapped.meter_reading is an object, check that it wasn't corrupted by merge operations
1262
+ // The test will show if there's a problem
1263
+ expect(unmapped.meter_reading).to.be.a('number');
1264
+ }
1265
+ });
1266
+
1217
1267
  it('toString', function() {
1218
1268
  const str = this.repository.toString();
1219
1269
  expect(str).to.be.eq('NullRepository {bar} - foo');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onehat/data",
3
- "version": "1.22.36",
3
+ "version": "1.22.37",
4
4
  "description": "JS data modeling package with adapters for many storage mediums.",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
@@ -2196,12 +2196,20 @@ export default class Repository extends EventEmitter {
2196
2196
  throw Error('No properties defined!');
2197
2197
  }
2198
2198
 
2199
- // Simply the definitions
2199
+ // Build property maps for conflict detection
2200
2200
  const
2201
2201
  UNMAPPED = 'UNMAPPED',
2202
- properties = {};
2202
+ properties = {},
2203
+ scalarRoots = new Set(); // Tracks root paths that are bound to scalar properties
2204
+
2203
2205
  _.each(propertiesDef, (def) => {
2204
- properties[def.name] = def.mapping || UNMAPPED;
2206
+ const mapping = def.mapping || UNMAPPED;
2207
+ properties[def.name] = mapping;
2208
+
2209
+ // Track scalar mappings (single-level) to detect conflicts with nested properties
2210
+ if (mapping !== UNMAPPED && !mapping.includes('.')) {
2211
+ scalarRoots.add(mapping);
2212
+ }
2205
2213
  });
2206
2214
 
2207
2215
  // Build the unmapped data
@@ -2213,10 +2221,18 @@ export default class Repository extends EventEmitter {
2213
2221
  unmappedData[field] = value;
2214
2222
  } else {
2215
2223
  // This is the more complicated one. Need to build up the hierarchy of unmapped data
2224
+ const mapStack = mapping.split('.');
2225
+
2226
+ // CONFLICT DETECTION:
2227
+ // If this property has a nested mapping (a.b.c) and a scalar property already
2228
+ // maps to the root (a), skip this property to avoid overwriting the scalar value.
2229
+ // This prevents: work_orders__meter_reading (scalar, maps to 'meter_reading')
2230
+ // from being overwritten by meter_readings__id (nested, maps to 'meter_reading.id')
2231
+ if (mapStack.length > 1 && scalarRoots.has(mapStack[0])) {
2232
+ return; // Skip this nested property to preserve the scalar value
2233
+ }
2216
2234
 
2217
- const
2218
- mapStack = mapping.split('.'),
2219
- rawValue = value;
2235
+ const rawValue = value;
2220
2236
 
2221
2237
  // Build up the hierarchy
2222
2238
  let thisValue = {},
@@ -173,6 +173,7 @@ export default class Schema extends EventEmitter {
173
173
  this.isDestroyed = false;
174
174
 
175
175
  this.normalizeRepositoryConfig();
176
+ this._validatePropertyMappings();
176
177
 
177
178
  this.registerEvents([
178
179
  'destroy',
@@ -189,6 +190,62 @@ export default class Schema extends EventEmitter {
189
190
  }
190
191
  }
191
192
 
193
+ /**
194
+ * Validates property mappings to detect conflicts between scalar and nested properties.
195
+ * Warns if a scalar property maps to a path that is also used as a root path by nested properties.
196
+ * Example conflict:
197
+ * - work_orders__meter_reading maps to 'meter_reading' (scalar)
198
+ * - meter_readings__id maps to 'meter_reading.id' (nested, uses 'meter_reading' as root)
199
+ * This conflict can cause the scalar value to be overwritten by an object.
200
+ * @private
201
+ */
202
+ _validatePropertyMappings = () => {
203
+ if (!this.model || !this.model.properties || this.model.properties.length === 0) {
204
+ return;
205
+ }
206
+
207
+ const mappings = {};
208
+ const scalarRoots = new Set();
209
+
210
+ // First pass: collect all mappings and identify scalar roots
211
+ _.each(this.model.properties, (property) => {
212
+ if (!property.mapping) {
213
+ return;
214
+ }
215
+ const mapping = property.mapping;
216
+ if (!mappings[mapping]) {
217
+ mappings[mapping] = [];
218
+ }
219
+ mappings[mapping].push(property.name);
220
+
221
+ // If this is a scalar mapping (no dots), track the root
222
+ if (!mapping.includes('.')) {
223
+ scalarRoots.add(mapping);
224
+ }
225
+ });
226
+
227
+ // Second pass: detect conflicts
228
+ _.each(this.model.properties, (property) => {
229
+ if (!property.mapping) {
230
+ return;
231
+ }
232
+ const mapping = property.mapping;
233
+ const mapStack = mapping.split('.');
234
+
235
+ // Check if this is a nested property (has dots) and conflicts with a scalar root
236
+ if (mapStack.length > 1 && scalarRoots.has(mapStack[0])) {
237
+ console.warn(
238
+ `[OneHatData Schema Warning] Scalar-vs-nested property mapping conflict in schema "${this.name}": ` +
239
+ `Property "${property.name}" (mapping: "${mapping}") has a nested path, ` +
240
+ `but another property maps to the scalar root "${mapStack[0]}". ` +
241
+ `This can cause data conflicts during unmapping. ` +
242
+ `Consider renaming one of the properties or adjusting their mappings. ` +
243
+ `The unmapData() method will skip this nested property to preserve the scalar value.`
244
+ );
245
+ }
246
+ });
247
+ }
248
+
192
249
  /**
193
250
  * Sets the Repository bound to this Schema.
194
251
  * @param {object} _boundRepository - The bound Repository
@@ -1,6 +1,6 @@
1
1
  import numeral from 'numeral';
2
2
  import moment from 'moment';
3
- import accounting from 'accounting-js';
3
+ import * as accounting from 'accounting-js';
4
4
  import _ from 'lodash';
5
5
 
6
6
  class Formatters {
@@ -1,6 +1,6 @@
1
1
  import moment from 'moment';
2
2
  import momentAlt from 'relative-time-parser'; // Notice this version of moment is imported from 'relative-time-parser', and may be out of sync with our general 'moment' package
3
- import accounting from 'accounting-js';
3
+ import * as accounting from 'accounting-js';
4
4
  // import * as chrono from 'chrono-node'; // Doesn't yet work in React Native ("SyntaxError: Invalid RegExp: Quantifier has nothing to repeat, js engine: hermes") Github ticket: https://github.com/facebook/hermes/blob/main/doc/RegExp.md
5
5
  import _ from 'lodash';
6
6