@onehat/data 1.22.36 → 1.22.38
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/.github/copilot-instructions.md.bak.20260307094057 +65 -0
- package/cypress/e2e/Repository/Repository.cy.js +50 -0
- package/package.json +1 -1
- package/src/Repository/OneBuild.js +1 -1
- package/src/Repository/Repository.js +22 -6
- package/src/Schema/Schema.js +57 -0
- package/src/Util/Formatters.js +1 -1
- package/src/Util/Parsers.js +1 -1
|
@@ -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
|
@@ -175,7 +175,7 @@ class OneBuildRepository extends AjaxRepository {
|
|
|
175
175
|
*/
|
|
176
176
|
_getReloadEntityParams(entity) {
|
|
177
177
|
const params = { conditions: {}, };
|
|
178
|
-
params.conditions[entity.
|
|
178
|
+
params.conditions[entity.getModel() + '.id'] = entity.id;
|
|
179
179
|
return params;
|
|
180
180
|
}
|
|
181
181
|
|
|
@@ -2196,12 +2196,20 @@ export default class Repository extends EventEmitter {
|
|
|
2196
2196
|
throw Error('No properties defined!');
|
|
2197
2197
|
}
|
|
2198
2198
|
|
|
2199
|
-
//
|
|
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
|
-
|
|
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 = {},
|
package/src/Schema/Schema.js
CHANGED
|
@@ -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
|
package/src/Util/Formatters.js
CHANGED
package/src/Util/Parsers.js
CHANGED
|
@@ -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
|
|