@env-hopper/table-sync 2.0.1-alpha-20260224145405
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/LICENSE +21 -0
- package/README.md +19 -0
- package/dist/esm/index.d.ts +5 -0
- package/dist/esm/index.js +5 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/tableSync.d.ts +20 -0
- package/dist/esm/tableSync.js +112 -0
- package/dist/esm/tableSync.js.map +1 -0
- package/package.json +53 -0
- package/src/index.ts +5 -0
- package/src/tableSync.ts +155 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Igor Golovin
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# @env-hopper/table-sync
|
|
2
|
+
|
|
3
|
+
Database table sync utilities for Env Hopper.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @env-hopper/table-sync
|
|
9
|
+
# or
|
|
10
|
+
pnpm add @env-hopper/table-sync
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Usage
|
|
14
|
+
|
|
15
|
+
This package provides utilities for syncing database tables in Env Hopper.
|
|
16
|
+
|
|
17
|
+
## License
|
|
18
|
+
|
|
19
|
+
MIT
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":";"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { FilteredKeys } from 'radashi';
|
|
2
|
+
export interface TableSyncParams<T extends object, TUniqColumns extends ReadonlyArray<keyof T>, TId extends keyof T & (string | number)> {
|
|
3
|
+
id: TId;
|
|
4
|
+
readAll: () => Promise<Array<T>>;
|
|
5
|
+
writeAll: (create: Array<Omit<T, TId>>, update: Array<{
|
|
6
|
+
data: Partial<T>;
|
|
7
|
+
where: Pick<T, FilteredKeys<T, TUniqColumns>>;
|
|
8
|
+
}>, deleteIds: Array<T[TId]>) => Promise<Array<T>>;
|
|
9
|
+
uniqColumns: TUniqColumns;
|
|
10
|
+
}
|
|
11
|
+
export declare function tableSync<T extends object, TUniqColumns extends ReadonlyArray<keyof T>, TId extends keyof T & (string | number)>(params: TableSyncParams<T, TUniqColumns, TId>): {
|
|
12
|
+
sync<TUpsert extends Partial<T> & Pick<T, TUniqColumns[number]>>(upsertRaw: Array<TUpsert>): Promise<{
|
|
13
|
+
findIdMaybe: (key: Pick<T, TUniqColumns[number]>) => T[TId] | undefined;
|
|
14
|
+
findIdOrThrow: (key: Pick<T, TUniqColumns[number]>) => T[TId];
|
|
15
|
+
getActual(): (Pick<T, TUniqColumns[number]> & T[TId])[];
|
|
16
|
+
}>;
|
|
17
|
+
};
|
|
18
|
+
export interface TableSyncIdBag<T, TUniqColumns extends ReadonlyArray<keyof T>> {
|
|
19
|
+
findId: (key: Pick<T, TUniqColumns[number]>) => T[keyof T] | undefined;
|
|
20
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { objectify, group, pick, isEqual } from "radashi";
|
|
2
|
+
function tableSync(params) {
|
|
3
|
+
return {
|
|
4
|
+
async sync(upsertRaw) {
|
|
5
|
+
const existingData = await params.readAll();
|
|
6
|
+
function toStrKey(item) {
|
|
7
|
+
return params.uniqColumns.map((column) => {
|
|
8
|
+
if (item[column] === void 0) {
|
|
9
|
+
throw new Error(
|
|
10
|
+
`unique column ${String(column)} can't be undefined in entry ${JSON.stringify(item)}`
|
|
11
|
+
);
|
|
12
|
+
}
|
|
13
|
+
return item[column];
|
|
14
|
+
}).join(":");
|
|
15
|
+
}
|
|
16
|
+
const existingByStrKey = objectify(existingData, (item) => toStrKey(item));
|
|
17
|
+
const isEqualOnKeys = (upsert, existing) => {
|
|
18
|
+
for (const key in upsert) {
|
|
19
|
+
if (!(key in existing)) {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
if (!isEqual(existing[key], upsert[key])) {
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return true;
|
|
27
|
+
};
|
|
28
|
+
const upsertItems = upsertRaw;
|
|
29
|
+
const { toCreate, toUpdate, unmodified } = group(
|
|
30
|
+
upsertItems,
|
|
31
|
+
(upsertItem) => {
|
|
32
|
+
const strKey = toStrKey(upsertItem);
|
|
33
|
+
const existing = existingByStrKey[strKey];
|
|
34
|
+
if (existing !== void 0) {
|
|
35
|
+
if (isEqualOnKeys(upsertItem, existing)) {
|
|
36
|
+
return "unmodified";
|
|
37
|
+
} else {
|
|
38
|
+
return "toUpdate";
|
|
39
|
+
}
|
|
40
|
+
} else {
|
|
41
|
+
return "toCreate";
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
);
|
|
45
|
+
const deletedStrKeys = new Set(
|
|
46
|
+
Object.keys(existingByStrKey)
|
|
47
|
+
);
|
|
48
|
+
for (const item of upsertItems) {
|
|
49
|
+
deletedStrKeys.delete(toStrKey(item));
|
|
50
|
+
}
|
|
51
|
+
const deletedIds = [...deletedStrKeys].map((strKey) => {
|
|
52
|
+
var _a;
|
|
53
|
+
const existingByStrKeyElementElement = (_a = existingByStrKey[strKey]) == null ? void 0 : _a[params.id];
|
|
54
|
+
if (existingByStrKeyElementElement === void 0) {
|
|
55
|
+
throw new Error(
|
|
56
|
+
`The PK column '${params.id}' is not presented in ${JSON.stringify(existingByStrKey[strKey])}`
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
return existingByStrKeyElementElement;
|
|
60
|
+
});
|
|
61
|
+
const inserted = await params.writeAll(
|
|
62
|
+
toCreate || [],
|
|
63
|
+
(toUpdate || []).map((update) => {
|
|
64
|
+
return {
|
|
65
|
+
data: update,
|
|
66
|
+
where: pick(update, params.uniqColumns)
|
|
67
|
+
};
|
|
68
|
+
}),
|
|
69
|
+
deletedIds
|
|
70
|
+
);
|
|
71
|
+
const findActual = (key) => {
|
|
72
|
+
const strKey = toStrKey(key);
|
|
73
|
+
const maybeExisting = existingByStrKey[strKey];
|
|
74
|
+
if (maybeExisting !== void 0 && !(strKey in deletedStrKeys)) {
|
|
75
|
+
return maybeExisting;
|
|
76
|
+
}
|
|
77
|
+
return inserted.find((ins) => {
|
|
78
|
+
return toStrKey(ins) === strKey;
|
|
79
|
+
});
|
|
80
|
+
};
|
|
81
|
+
function getActual() {
|
|
82
|
+
return [...inserted, ...toUpdate || [], ...unmodified || []].map(
|
|
83
|
+
(row) => findActual(row)
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
return {
|
|
87
|
+
findIdMaybe: (key) => {
|
|
88
|
+
var _a;
|
|
89
|
+
return (_a = findActual(key)) == null ? void 0 : _a[params.id];
|
|
90
|
+
},
|
|
91
|
+
findIdOrThrow: (key) => {
|
|
92
|
+
var _a;
|
|
93
|
+
const maybeInserted = (_a = findActual(key)) == null ? void 0 : _a[params.id];
|
|
94
|
+
if (!maybeInserted) {
|
|
95
|
+
const total = [toCreate, toUpdate, unmodified].map((s) => (s == null ? void 0 : s.length) || 0).reduce((a, c) => a + c, 0);
|
|
96
|
+
throw new Error(
|
|
97
|
+
`findOrThrow: '${toStrKey(key)}' not found. (Existed #: ${total})`
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
return maybeInserted;
|
|
101
|
+
},
|
|
102
|
+
getActual() {
|
|
103
|
+
return getActual();
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
export {
|
|
110
|
+
tableSync
|
|
111
|
+
};
|
|
112
|
+
//# sourceMappingURL=tableSync.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tableSync.js","sources":["../../src/tableSync.ts"],"sourcesContent":["import type { FilteredKeys } from 'radashi'\nimport { group, isEqual, objectify, pick } from 'radashi'\n\nexport interface TableSyncParams<\n T extends object,\n TUniqColumns extends ReadonlyArray<keyof T>,\n TId extends keyof T & (string | number),\n> {\n id: TId\n readAll: () => Promise<Array<T>>\n writeAll: (\n create: Array<Omit<T, TId>>,\n update: Array<{\n data: Partial<T>\n where: Pick<T, FilteredKeys<T, TUniqColumns>>\n }>,\n deleteIds: Array<T[TId]>,\n ) => Promise<Array<T>>\n uniqColumns: TUniqColumns\n}\n\nexport function tableSync<\n T extends object,\n TUniqColumns extends ReadonlyArray<keyof T>,\n TId extends keyof T & (string | number),\n>(params: TableSyncParams<T, TUniqColumns, TId>) {\n return {\n async sync<TUpsert extends Partial<T> & Pick<T, TUniqColumns[number]>>(\n upsertRaw: Array<TUpsert>,\n ) {\n const existingData = await params.readAll()\n\n function toStrKey(item: T) {\n return params.uniqColumns\n .map((column) => {\n if (item[column] === undefined) {\n throw new Error(\n `unique column ${String(column)} can't be undefined in entry ${JSON.stringify(item)}`,\n )\n }\n return item[column]\n })\n .join(':')\n }\n\n const existingByStrKey = objectify(existingData, (item) => toStrKey(item))\n\n const isEqualOnKeys = (upsert: Partial<T>, existing: T) => {\n for (const key in upsert) {\n if (!(key in existing)) {\n return false\n }\n if (!isEqual(existing[key], upsert[key])) {\n return false\n }\n }\n return true\n }\n\n const upsertItems = upsertRaw\n const { toCreate, toUpdate, unmodified } = group(\n upsertItems,\n (upsertItem) => {\n const strKey = toStrKey(upsertItem)\n const existing = existingByStrKey[strKey]\n if (existing !== undefined) {\n if (isEqualOnKeys(upsertItem, existing)) {\n return 'unmodified'\n } else {\n return 'toUpdate'\n }\n } else {\n return 'toCreate'\n }\n },\n )\n\n const deletedStrKeys = new Set<keyof typeof existingByStrKey>(\n Object.keys(existingByStrKey),\n )\n for (const item of upsertItems) {\n deletedStrKeys.delete(toStrKey(item))\n }\n const deletedIds = [...deletedStrKeys].map((strKey) => {\n const existingByStrKeyElementElement =\n existingByStrKey[strKey]?.[params.id]\n if (existingByStrKeyElementElement === undefined) {\n throw new Error(\n `The PK column '${params.id}' is not presented in ${JSON.stringify(existingByStrKey[strKey])}`,\n )\n }\n return existingByStrKeyElementElement\n })\n\n const inserted = await params.writeAll(\n toCreate || [],\n (toUpdate || []).map((update) => {\n return {\n data: update,\n where: pick<T, TUniqColumns>(update, params.uniqColumns),\n }\n }),\n deletedIds as Array<T[TId]>,\n )\n\n const findActual = (key: Pick<T, TUniqColumns[number]>) => {\n const strKey = toStrKey(key)\n const maybeExisting = existingByStrKey[strKey]\n if (maybeExisting !== undefined && !(strKey in deletedStrKeys)) {\n return maybeExisting\n }\n return inserted.find((ins) => {\n return toStrKey(ins) === strKey\n })\n }\n\n function getActual() {\n return [...inserted, ...(toUpdate || []), ...(unmodified || [])].map(\n (row) => findActual(row),\n ) as Array<Pick<T, TUniqColumns[number]> & T[TId]>\n }\n\n return {\n findIdMaybe: (\n key: Pick<T, TUniqColumns[number]>,\n ): T[TId] | undefined => {\n return findActual(key)?.[params.id]\n },\n\n findIdOrThrow: (key: Pick<T, TUniqColumns[number]>): T[TId] => {\n const maybeInserted = findActual(key)?.[params.id]\n if (!maybeInserted) {\n const total = [toCreate, toUpdate, unmodified]\n .map((s) => s?.length || 0)\n .reduce((a, c) => a + c, 0)\n throw new Error(\n `findOrThrow: '${toStrKey(key)}' not found. (Existed #: ${total})`,\n )\n }\n return maybeInserted\n },\n getActual() {\n return getActual()\n },\n }\n },\n }\n}\n\nexport interface TableSyncIdBag<\n T,\n TUniqColumns extends ReadonlyArray<keyof T>,\n> {\n findId: (key: Pick<T, TUniqColumns[number]>) => T[keyof T] | undefined\n}\n"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@env-hopper/table-sync",
|
|
3
|
+
"version": "2.0.1-alpha-20260224145405",
|
|
4
|
+
"description": "Database table sync utilities for Env Hopper",
|
|
5
|
+
"homepage": "https://github.com/lislon/env-hopper",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/lislon/env-hopper.git",
|
|
9
|
+
"directory": "packages/table-sync"
|
|
10
|
+
},
|
|
11
|
+
"license": "MIT",
|
|
12
|
+
"author": "Igor Golovin",
|
|
13
|
+
"sideEffects": false,
|
|
14
|
+
"type": "module",
|
|
15
|
+
"exports": {
|
|
16
|
+
".": {
|
|
17
|
+
"my-custom-condition": "./src/index.ts",
|
|
18
|
+
"import": {
|
|
19
|
+
"types": "./dist/esm/index.d.ts",
|
|
20
|
+
"default": "./dist/esm/index.js"
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
"./package.json": "./package.json"
|
|
24
|
+
},
|
|
25
|
+
"module": "dist/esm/index.js",
|
|
26
|
+
"types": "dist/esm/index.d.ts",
|
|
27
|
+
"files": [
|
|
28
|
+
"dist",
|
|
29
|
+
"src"
|
|
30
|
+
],
|
|
31
|
+
"dependencies": {
|
|
32
|
+
"radashi": "12.5.0-beta.6d5c035"
|
|
33
|
+
},
|
|
34
|
+
"devDependencies": {
|
|
35
|
+
"@tanstack/vite-config": "^0.4.3",
|
|
36
|
+
"@types/node": "^24.3.0",
|
|
37
|
+
"esbuild": "^0.25.5",
|
|
38
|
+
"tsx": "^4.19.4",
|
|
39
|
+
"typescript": "5.9.2"
|
|
40
|
+
},
|
|
41
|
+
"engines": {
|
|
42
|
+
"node": ">=16"
|
|
43
|
+
},
|
|
44
|
+
"scripts": {
|
|
45
|
+
"build": "vite build",
|
|
46
|
+
"clean": "premove ./dist ./coverage ./dist-ts",
|
|
47
|
+
"compile": "tsc --build",
|
|
48
|
+
"dev": "tsx watch src/index.ts",
|
|
49
|
+
"test:eslint": "eslint ./src",
|
|
50
|
+
"test:unit": "echo \"skipped\"",
|
|
51
|
+
"test:unit:dev": "echo \"skipped\""
|
|
52
|
+
}
|
|
53
|
+
}
|
package/src/index.ts
ADDED
package/src/tableSync.ts
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import type { FilteredKeys } from 'radashi'
|
|
2
|
+
import { group, isEqual, objectify, pick } from 'radashi'
|
|
3
|
+
|
|
4
|
+
export interface TableSyncParams<
|
|
5
|
+
T extends object,
|
|
6
|
+
TUniqColumns extends ReadonlyArray<keyof T>,
|
|
7
|
+
TId extends keyof T & (string | number),
|
|
8
|
+
> {
|
|
9
|
+
id: TId
|
|
10
|
+
readAll: () => Promise<Array<T>>
|
|
11
|
+
writeAll: (
|
|
12
|
+
create: Array<Omit<T, TId>>,
|
|
13
|
+
update: Array<{
|
|
14
|
+
data: Partial<T>
|
|
15
|
+
where: Pick<T, FilteredKeys<T, TUniqColumns>>
|
|
16
|
+
}>,
|
|
17
|
+
deleteIds: Array<T[TId]>,
|
|
18
|
+
) => Promise<Array<T>>
|
|
19
|
+
uniqColumns: TUniqColumns
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function tableSync<
|
|
23
|
+
T extends object,
|
|
24
|
+
TUniqColumns extends ReadonlyArray<keyof T>,
|
|
25
|
+
TId extends keyof T & (string | number),
|
|
26
|
+
>(params: TableSyncParams<T, TUniqColumns, TId>) {
|
|
27
|
+
return {
|
|
28
|
+
async sync<TUpsert extends Partial<T> & Pick<T, TUniqColumns[number]>>(
|
|
29
|
+
upsertRaw: Array<TUpsert>,
|
|
30
|
+
) {
|
|
31
|
+
const existingData = await params.readAll()
|
|
32
|
+
|
|
33
|
+
function toStrKey(item: T) {
|
|
34
|
+
return params.uniqColumns
|
|
35
|
+
.map((column) => {
|
|
36
|
+
if (item[column] === undefined) {
|
|
37
|
+
throw new Error(
|
|
38
|
+
`unique column ${String(column)} can't be undefined in entry ${JSON.stringify(item)}`,
|
|
39
|
+
)
|
|
40
|
+
}
|
|
41
|
+
return item[column]
|
|
42
|
+
})
|
|
43
|
+
.join(':')
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const existingByStrKey = objectify(existingData, (item) => toStrKey(item))
|
|
47
|
+
|
|
48
|
+
const isEqualOnKeys = (upsert: Partial<T>, existing: T) => {
|
|
49
|
+
for (const key in upsert) {
|
|
50
|
+
if (!(key in existing)) {
|
|
51
|
+
return false
|
|
52
|
+
}
|
|
53
|
+
if (!isEqual(existing[key], upsert[key])) {
|
|
54
|
+
return false
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return true
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const upsertItems = upsertRaw
|
|
61
|
+
const { toCreate, toUpdate, unmodified } = group(
|
|
62
|
+
upsertItems,
|
|
63
|
+
(upsertItem) => {
|
|
64
|
+
const strKey = toStrKey(upsertItem)
|
|
65
|
+
const existing = existingByStrKey[strKey]
|
|
66
|
+
if (existing !== undefined) {
|
|
67
|
+
if (isEqualOnKeys(upsertItem, existing)) {
|
|
68
|
+
return 'unmodified'
|
|
69
|
+
} else {
|
|
70
|
+
return 'toUpdate'
|
|
71
|
+
}
|
|
72
|
+
} else {
|
|
73
|
+
return 'toCreate'
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
const deletedStrKeys = new Set<keyof typeof existingByStrKey>(
|
|
79
|
+
Object.keys(existingByStrKey),
|
|
80
|
+
)
|
|
81
|
+
for (const item of upsertItems) {
|
|
82
|
+
deletedStrKeys.delete(toStrKey(item))
|
|
83
|
+
}
|
|
84
|
+
const deletedIds = [...deletedStrKeys].map((strKey) => {
|
|
85
|
+
const existingByStrKeyElementElement =
|
|
86
|
+
existingByStrKey[strKey]?.[params.id]
|
|
87
|
+
if (existingByStrKeyElementElement === undefined) {
|
|
88
|
+
throw new Error(
|
|
89
|
+
`The PK column '${params.id}' is not presented in ${JSON.stringify(existingByStrKey[strKey])}`,
|
|
90
|
+
)
|
|
91
|
+
}
|
|
92
|
+
return existingByStrKeyElementElement
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
const inserted = await params.writeAll(
|
|
96
|
+
toCreate || [],
|
|
97
|
+
(toUpdate || []).map((update) => {
|
|
98
|
+
return {
|
|
99
|
+
data: update,
|
|
100
|
+
where: pick<T, TUniqColumns>(update, params.uniqColumns),
|
|
101
|
+
}
|
|
102
|
+
}),
|
|
103
|
+
deletedIds as Array<T[TId]>,
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
const findActual = (key: Pick<T, TUniqColumns[number]>) => {
|
|
107
|
+
const strKey = toStrKey(key)
|
|
108
|
+
const maybeExisting = existingByStrKey[strKey]
|
|
109
|
+
if (maybeExisting !== undefined && !(strKey in deletedStrKeys)) {
|
|
110
|
+
return maybeExisting
|
|
111
|
+
}
|
|
112
|
+
return inserted.find((ins) => {
|
|
113
|
+
return toStrKey(ins) === strKey
|
|
114
|
+
})
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function getActual() {
|
|
118
|
+
return [...inserted, ...(toUpdate || []), ...(unmodified || [])].map(
|
|
119
|
+
(row) => findActual(row),
|
|
120
|
+
) as Array<Pick<T, TUniqColumns[number]> & T[TId]>
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return {
|
|
124
|
+
findIdMaybe: (
|
|
125
|
+
key: Pick<T, TUniqColumns[number]>,
|
|
126
|
+
): T[TId] | undefined => {
|
|
127
|
+
return findActual(key)?.[params.id]
|
|
128
|
+
},
|
|
129
|
+
|
|
130
|
+
findIdOrThrow: (key: Pick<T, TUniqColumns[number]>): T[TId] => {
|
|
131
|
+
const maybeInserted = findActual(key)?.[params.id]
|
|
132
|
+
if (!maybeInserted) {
|
|
133
|
+
const total = [toCreate, toUpdate, unmodified]
|
|
134
|
+
.map((s) => s?.length || 0)
|
|
135
|
+
.reduce((a, c) => a + c, 0)
|
|
136
|
+
throw new Error(
|
|
137
|
+
`findOrThrow: '${toStrKey(key)}' not found. (Existed #: ${total})`,
|
|
138
|
+
)
|
|
139
|
+
}
|
|
140
|
+
return maybeInserted
|
|
141
|
+
},
|
|
142
|
+
getActual() {
|
|
143
|
+
return getActual()
|
|
144
|
+
},
|
|
145
|
+
}
|
|
146
|
+
},
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export interface TableSyncIdBag<
|
|
151
|
+
T,
|
|
152
|
+
TUniqColumns extends ReadonlyArray<keyof T>,
|
|
153
|
+
> {
|
|
154
|
+
findId: (key: Pick<T, TUniqColumns[number]>) => T[keyof T] | undefined
|
|
155
|
+
}
|