@proteinjs/db 1.21.2 → 1.21.4
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/CHANGELOG.md +11 -0
- package/dist/generated/index.js +1 -1
- package/dist/generated/index.js.map +1 -1
- package/dist/generated/test/index.js +1 -1
- package/dist/generated/test/index.js.map +1 -1
- package/dist/src/source/SourceRecordLoader.d.ts +18 -0
- package/dist/src/source/SourceRecordLoader.d.ts.map +1 -1
- package/dist/src/source/SourceRecordLoader.js +125 -18
- package/dist/src/source/SourceRecordLoader.js.map +1 -1
- package/generated/index.ts +1 -1
- package/generated/test/index.ts +1 -1
- package/package.json +11 -11
- package/src/source/SourceRecordLoader.ts +104 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@proteinjs/db",
|
|
3
|
-
"version": "1.21.
|
|
3
|
+
"version": "1.21.4",
|
|
4
4
|
"main": "./dist/generated/index.js",
|
|
5
5
|
"types": "./dist/generated/index.d.ts",
|
|
6
6
|
"exports": {
|
|
@@ -41,19 +41,19 @@
|
|
|
41
41
|
"test": "jest"
|
|
42
42
|
},
|
|
43
43
|
"dependencies": {
|
|
44
|
-
"@proteinjs/db-query": "^1.4.
|
|
45
|
-
"@proteinjs/logger": "1.0.
|
|
46
|
-
"@proteinjs/reflection": "1.1.
|
|
47
|
-
"@proteinjs/serializer": "1.1.
|
|
48
|
-
"@proteinjs/server-api": "3.0.
|
|
49
|
-
"@proteinjs/service": "1.2.
|
|
50
|
-
"@proteinjs/user-auth": "1.1.
|
|
51
|
-
"@proteinjs/util": "1.
|
|
44
|
+
"@proteinjs/db-query": "^1.4.3",
|
|
45
|
+
"@proteinjs/logger": "^1.0.15",
|
|
46
|
+
"@proteinjs/reflection": "^1.1.11",
|
|
47
|
+
"@proteinjs/serializer": "^1.1.4",
|
|
48
|
+
"@proteinjs/server-api": "^3.0.2",
|
|
49
|
+
"@proteinjs/service": "^1.2.11",
|
|
50
|
+
"@proteinjs/user-auth": "^1.1.9",
|
|
51
|
+
"@proteinjs/util": "^1.6.0",
|
|
52
52
|
"moment": "2.29.4",
|
|
53
53
|
"uuid": "8.3.0"
|
|
54
54
|
},
|
|
55
55
|
"devDependencies": {
|
|
56
|
-
"@proteinjs/reflection-build": "1.4.
|
|
56
|
+
"@proteinjs/reflection-build": "^1.4.3",
|
|
57
57
|
"@types/jest": "29.5.5",
|
|
58
58
|
"@types/node": "14.0.27",
|
|
59
59
|
"@types/uuid": "8.3.0",
|
|
@@ -66,5 +66,5 @@
|
|
|
66
66
|
"ts-jest": "29.1.1",
|
|
67
67
|
"typescript": "5.2.2"
|
|
68
68
|
},
|
|
69
|
-
"gitHead": "
|
|
69
|
+
"gitHead": "b437c941c73c3c0757fce492e1e918fc32457645"
|
|
70
70
|
}
|
|
@@ -4,6 +4,7 @@ import { getSourceRecordLoaders, SourceRecord, getSourceRecordTables } from './S
|
|
|
4
4
|
import { Table } from '../Table';
|
|
5
5
|
import { getDbAsSystem } from '../Db';
|
|
6
6
|
import { SourceRecordRepo } from './SourceRecordRepo';
|
|
7
|
+
import { RecordSerializer } from '../Record';
|
|
7
8
|
|
|
8
9
|
type SourceRecordsMap = {
|
|
9
10
|
[tableName: string]: { table: Table<any>; records: Omit<SourceRecord, 'created' | 'updated'>[]; recordIds: string[] };
|
|
@@ -18,6 +19,7 @@ export class SourceRecordLoader {
|
|
|
18
19
|
for (const tableName in sourceRecordsMap) {
|
|
19
20
|
let insertCount = 0;
|
|
20
21
|
let updateCount = 0;
|
|
22
|
+
let unchangedCount = 0;
|
|
21
23
|
let deleteCount = 0;
|
|
22
24
|
const table = sourceRecordsMap[tableName].table;
|
|
23
25
|
const sourceRecordIds = sourceRecordsMap[tableName].recordIds;
|
|
@@ -33,9 +35,14 @@ export class SourceRecordLoader {
|
|
|
33
35
|
const sourceRecords = sourceRecordsMap[tableName].records;
|
|
34
36
|
for (let sourceRecord of sourceRecords) {
|
|
35
37
|
sourceRecord.isLoadedFromSource = true;
|
|
36
|
-
const
|
|
37
|
-
if (
|
|
38
|
-
|
|
38
|
+
const existingRecord = await db.get(table, { id: sourceRecord.id });
|
|
39
|
+
if (existingRecord) {
|
|
40
|
+
if (await this.hasChanges(table, sourceRecord, existingRecord)) {
|
|
41
|
+
await db.update(table, sourceRecord);
|
|
42
|
+
updateCount += 1;
|
|
43
|
+
} else {
|
|
44
|
+
unchangedCount += 1;
|
|
45
|
+
}
|
|
39
46
|
} else {
|
|
40
47
|
const dbSourceRecord = await db.insert(table, sourceRecord);
|
|
41
48
|
sourceRecord = { ...sourceRecord, ...dbSourceRecord };
|
|
@@ -50,12 +57,106 @@ export class SourceRecordLoader {
|
|
|
50
57
|
obj: {
|
|
51
58
|
inserts: insertCount,
|
|
52
59
|
updates: updateCount,
|
|
60
|
+
unchanged: unchangedCount,
|
|
53
61
|
deletes: deleteCount,
|
|
54
62
|
},
|
|
55
63
|
});
|
|
56
64
|
}
|
|
57
65
|
}
|
|
58
66
|
|
|
67
|
+
/**
|
|
68
|
+
* Compare source record fields against the existing DB record to detect actual changes.
|
|
69
|
+
* Only fields present on the source record are compared (ignoring `created`, `updated`).
|
|
70
|
+
* Uses serialization to normalize values (e.g. Reference objects, Moment, JSON) before comparison.
|
|
71
|
+
*
|
|
72
|
+
* The comparison checks that every value in the source record exists with the same value
|
|
73
|
+
* in the existing DB record. Extra keys in the DB record are ignored — table watchers
|
|
74
|
+
* and hooks may enrich records with additional data after insert/update.
|
|
75
|
+
*/
|
|
76
|
+
private async hasChanges(table: Table<any>, sourceRecord: any, existingRecord: any): Promise<boolean> {
|
|
77
|
+
const serializer = new RecordSerializer(table);
|
|
78
|
+
const serializedSource = await serializer.serialize(sourceRecord);
|
|
79
|
+
const serializedExisting = await serializer.serialize(existingRecord);
|
|
80
|
+
for (const columnName in serializedSource) {
|
|
81
|
+
if (columnName === 'created' || columnName === 'updated') {
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const sourceValue = serializedSource[columnName];
|
|
86
|
+
const existingValue = serializedExisting[columnName];
|
|
87
|
+
if (this.findMismatchPath(sourceValue, existingValue, columnName)) {
|
|
88
|
+
return true;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Find the first point of divergence between source and existing values.
|
|
97
|
+
* Returns a description of the mismatch path, or null if they match.
|
|
98
|
+
* For objects, extra keys in `existing` are ignored — they may have been added by
|
|
99
|
+
* table watchers or hooks after the source record was loaded.
|
|
100
|
+
* For arrays, order and length must match exactly.
|
|
101
|
+
*/
|
|
102
|
+
private findMismatchPath(source: any, existing: any, path: string): string | null {
|
|
103
|
+
if (source === existing) {
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (source == null || existing == null) {
|
|
108
|
+
if (source == existing) {
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
return `${path}: source=${JSON.stringify(source)}, existing=${JSON.stringify(existing)}`;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (typeof source !== typeof existing) {
|
|
115
|
+
return `${path}: type mismatch: source=${typeof source}, existing=${typeof existing}`;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (typeof source !== 'object') {
|
|
119
|
+
const sourceStr = typeof source === 'string' && source.length > 80 ? source.substring(0, 80) + '...' : source;
|
|
120
|
+
const existingStr =
|
|
121
|
+
typeof existing === 'string' && existing.length > 80 ? existing.substring(0, 80) + '...' : existing;
|
|
122
|
+
return `${path}: source=${JSON.stringify(sourceStr)}, existing=${JSON.stringify(existingStr)}`;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (Array.isArray(source) !== Array.isArray(existing)) {
|
|
126
|
+
return `${path}: array mismatch: source isArray=${Array.isArray(source)}, existing isArray=${Array.isArray(existing)}`;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (Array.isArray(source)) {
|
|
130
|
+
if (source.length !== existing.length) {
|
|
131
|
+
return `${path}: array length: source=${source.length}, existing=${existing.length}`;
|
|
132
|
+
}
|
|
133
|
+
for (let i = 0; i < source.length; i++) {
|
|
134
|
+
const result = this.findMismatchPath(source[i], existing[i], `${path}[${i}]`);
|
|
135
|
+
if (result) {
|
|
136
|
+
return result;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
for (const key of Object.keys(source)) {
|
|
143
|
+
// Skip undefined values — they don't survive JSON serialization (JSON.stringify
|
|
144
|
+
// drops undefined), so the DB record won't have them.
|
|
145
|
+
if (source[key] === undefined) {
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
if (!(key in existing)) {
|
|
149
|
+
return `${path}.${key}: key missing in existing`;
|
|
150
|
+
}
|
|
151
|
+
const result = this.findMismatchPath(source[key], existing[key], `${path}.${key}`);
|
|
152
|
+
if (result) {
|
|
153
|
+
return result;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
|
|
59
160
|
private async getSourceRecordsMap() {
|
|
60
161
|
const sourceRecordsMap: SourceRecordsMap = {};
|
|
61
162
|
const sourceRecordTables = getSourceRecordTables();
|