@inkeep/agents-core 0.59.3 → 0.60.0
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/dist/auth/auth-schema.d.ts +108 -108
- package/dist/auth/auth-validation-schemas.d.ts +154 -154
- package/dist/auth/auth.js +2 -2
- package/dist/auth/permissions.d.ts +9 -9
- package/dist/client-exports.d.ts +2 -2
- package/dist/client-exports.js +2 -2
- package/dist/constants/context-breakdown.js +5 -0
- package/dist/constants/otel-attributes.d.ts +25 -1
- package/dist/constants/otel-attributes.js +25 -1
- package/dist/data-access/manage/agentFull.js +5 -5
- package/dist/data-access/manage/agents.d.ts +31 -31
- package/dist/data-access/manage/artifactComponents.d.ts +14 -14
- package/dist/data-access/manage/contextConfigs.d.ts +8 -8
- package/dist/data-access/manage/credentialReferences.js +1 -1
- package/dist/data-access/manage/dataComponents.d.ts +6 -6
- package/dist/data-access/manage/dataComponents.js +1 -1
- package/dist/data-access/manage/functionTools.d.ts +14 -14
- package/dist/data-access/manage/functionTools.js +2 -2
- package/dist/data-access/manage/skills.d.ts +13 -13
- package/dist/data-access/manage/skills.js +2 -3
- package/dist/data-access/manage/subAgentExternalAgentRelations.d.ts +18 -18
- package/dist/data-access/manage/subAgentExternalAgentRelations.js +2 -2
- package/dist/data-access/manage/subAgentRelations.d.ts +20 -20
- package/dist/data-access/manage/subAgentRelations.js +2 -2
- package/dist/data-access/manage/subAgentTeamAgentRelations.d.ts +18 -18
- package/dist/data-access/manage/subAgentTeamAgentRelations.js +2 -2
- package/dist/data-access/manage/subAgents.d.ts +21 -21
- package/dist/data-access/manage/tools.d.ts +18 -18
- package/dist/data-access/manage/tools.js +2 -2
- package/dist/data-access/manage/triggers.d.ts +2 -2
- package/dist/data-access/runtime/apiKeys.d.ts +16 -16
- package/dist/data-access/runtime/apps.d.ts +13 -10
- package/dist/data-access/runtime/conversations.d.ts +18 -18
- package/dist/data-access/runtime/conversations.js +17 -1
- package/dist/data-access/runtime/messages.d.ts +18 -18
- package/dist/data-access/runtime/scheduledTriggerInvocations.d.ts +3 -3
- package/dist/data-access/runtime/tasks.d.ts +4 -4
- package/dist/db/manage/manage-schema.d.ts +453 -453
- package/dist/db/runtime/runtime-schema.d.ts +353 -334
- package/dist/db/runtime/runtime-schema.js +4 -1
- package/dist/db/runtime/test-runtime-client.js +1 -1
- package/dist/dolt/advisory-lock.d.ts +7 -0
- package/dist/dolt/advisory-lock.js +18 -0
- package/dist/dolt/branches-api.d.ts +19 -9
- package/dist/dolt/branches-api.js +58 -29
- package/dist/dolt/fk-map.d.ts +5 -0
- package/dist/dolt/fk-map.js +34 -0
- package/dist/dolt/index.d.ts +8 -4
- package/dist/dolt/index.js +8 -4
- package/dist/dolt/merge.d.ts +41 -5
- package/dist/dolt/merge.js +161 -24
- package/dist/dolt/pk-map.d.ts +6 -0
- package/dist/dolt/pk-map.js +27 -0
- package/dist/dolt/ref-middleware.d.ts +2 -1
- package/dist/dolt/ref-middleware.js +18 -4
- package/dist/dolt/resolve-conflicts.d.ts +20 -0
- package/dist/dolt/resolve-conflicts.js +158 -0
- package/dist/dolt/schema-sync.js +17 -17
- package/dist/index.d.ts +14 -8
- package/dist/index.js +14 -8
- package/dist/types/utility.d.ts +2 -0
- package/dist/utils/apiKeys.js +1 -1
- package/dist/utils/conversations.d.ts +7 -1
- package/dist/utils/conversations.js +10 -1
- package/dist/utils/error.d.ts +51 -51
- package/dist/utils/index.d.ts +6 -2
- package/dist/utils/index.js +5 -3
- package/dist/utils/model-factory.js +35 -10
- package/dist/utils/token-estimator.d.ts +19 -0
- package/dist/utils/token-estimator.js +17 -0
- package/dist/utils/usage-cost-middleware.d.ts +13 -0
- package/dist/utils/usage-cost-middleware.js +60 -0
- package/dist/utils/usage-tracker.d.ts +2 -0
- package/dist/utils/usage-tracker.js +1 -0
- package/dist/validation/dolt-schemas.d.ts +95 -2
- package/dist/validation/dolt-schemas.js +54 -2
- package/dist/validation/drizzle-schema-helpers.d.ts +3 -3
- package/dist/validation/index.d.ts +2 -2
- package/dist/validation/index.js +2 -2
- package/dist/validation/schemas.d.ts +2161 -2085
- package/drizzle/runtime/0025_faulty_kylun.sql +1 -0
- package/drizzle/runtime/meta/0025_snapshot.json +4276 -0
- package/drizzle/runtime/meta/_journal.json +7 -0
- package/package.json +2 -2
package/dist/dolt/merge.js
CHANGED
|
@@ -1,36 +1,150 @@
|
|
|
1
|
+
import { getLogger } from "../utils/logger.js";
|
|
2
|
+
import { doltAddAndCommit } from "./commit.js";
|
|
3
|
+
import { createApiError } from "../utils/error.js";
|
|
4
|
+
import { managePkMap } from "./pk-map.js";
|
|
5
|
+
import { ResolutionValidationError, applyResolutions } from "./resolve-conflicts.js";
|
|
1
6
|
import { doltCheckout } from "./branch.js";
|
|
2
7
|
import { sql } from "drizzle-orm";
|
|
3
8
|
|
|
4
9
|
//#region src/dolt/merge.ts
|
|
10
|
+
const logger = getLogger("dolt-merge");
|
|
11
|
+
const TIMESTAMP_COLUMNS = new Set(["created_at", "updated_at"]);
|
|
12
|
+
var MergeConflictError = class extends Error {
|
|
13
|
+
constructor(message, conflictCount, fromBranch, toBranch) {
|
|
14
|
+
super(message);
|
|
15
|
+
this.conflictCount = conflictCount;
|
|
16
|
+
this.fromBranch = fromBranch;
|
|
17
|
+
this.toBranch = toBranch;
|
|
18
|
+
this.name = "MergeConflictError";
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
function extractConflictCount(row) {
|
|
22
|
+
if (typeof row.conflicts === "number" || typeof row.conflicts === "string") return Number(row.conflicts);
|
|
23
|
+
const doltMerge$1 = row.dolt_merge;
|
|
24
|
+
if (Array.isArray(doltMerge$1)) return Number(doltMerge$1[2] ?? 0);
|
|
25
|
+
throw new Error(`Unexpected DOLT_MERGE result format: ${JSON.stringify(row)}`);
|
|
26
|
+
}
|
|
27
|
+
function isTimestampOnlyConflictRow(row, pkColumns) {
|
|
28
|
+
if (row.our_diff_type !== "modified" || row.their_diff_type !== "modified") return false;
|
|
29
|
+
const pkSet = new Set(pkColumns);
|
|
30
|
+
for (const key of Object.keys(row)) {
|
|
31
|
+
if (!key.startsWith("base_")) continue;
|
|
32
|
+
const col = key.slice(5);
|
|
33
|
+
if (col === "diff_type" || pkSet.has(col) || TIMESTAMP_COLUMNS.has(col)) continue;
|
|
34
|
+
if (String(row[`base_${col}`] ?? "") !== String(row[`our_${col}`] ?? "") || String(row[`base_${col}`] ?? "") !== String(row[`their_${col}`] ?? "")) return false;
|
|
35
|
+
}
|
|
36
|
+
return true;
|
|
37
|
+
}
|
|
38
|
+
function buildTimestampAutoResolution(tableName, row, pkColumns) {
|
|
39
|
+
const primaryKey = {};
|
|
40
|
+
for (const col of pkColumns) primaryKey[col] = String(row[`base_${col}`] ?? row[`our_${col}`] ?? row[`their_${col}`]);
|
|
41
|
+
return {
|
|
42
|
+
table: tableName,
|
|
43
|
+
primaryKey,
|
|
44
|
+
rowDefaultPick: (row.our_updated_at ? new Date(String(row.our_updated_at)) : /* @__PURE__ */ new Date(0)) >= (row.their_updated_at ? new Date(String(row.their_updated_at)) : /* @__PURE__ */ new Date(0)) ? "ours" : "theirs"
|
|
45
|
+
};
|
|
46
|
+
}
|
|
5
47
|
/**
|
|
6
|
-
* Merge
|
|
7
|
-
*
|
|
48
|
+
* Merge a branch into the currently checked out branch.
|
|
49
|
+
*
|
|
50
|
+
* Runs inside an explicit transaction so that conflicts and
|
|
51
|
+
* constraint-violations are surfaced to the caller instead of being
|
|
52
|
+
* auto-rolled-back by Dolt's AUTOCOMMIT mode.
|
|
53
|
+
*
|
|
54
|
+
* If conflicts arise and `resolutions` are provided, they are applied
|
|
55
|
+
* and the merge is committed. If conflicts arise without resolutions
|
|
56
|
+
* (or with insufficient resolutions), the transaction is rolled back
|
|
57
|
+
* and a `MergeConflictError` is thrown.
|
|
8
58
|
*/
|
|
9
59
|
const doltMerge = (db) => async (params) => {
|
|
10
|
-
|
|
60
|
+
logger.info({
|
|
61
|
+
fromBranch: params.fromBranch,
|
|
62
|
+
toBranch: params.toBranch
|
|
63
|
+
}, "Merging branch");
|
|
11
64
|
await doltCheckout(db)({ branch: params.toBranch });
|
|
12
65
|
const toHead = (await db.execute(sql`SELECT HASHOF('HEAD') as hash`)).rows[0]?.hash;
|
|
13
|
-
const args = [`'${params.fromBranch}'`];
|
|
66
|
+
const args = [`'${params.fromBranch.replace(/'/g, "''")}'`];
|
|
14
67
|
if (params.noFastForward) args.push("'--no-ff'");
|
|
15
|
-
if (params.message)
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
68
|
+
if (params.message) {
|
|
69
|
+
const cleanedMessage = params.message.replace(/'/g, "''");
|
|
70
|
+
args.push("'-m'", `'${cleanedMessage}'`);
|
|
71
|
+
}
|
|
72
|
+
const cleanedAuthor = params.author?.name?.replace(/'/g, "''");
|
|
73
|
+
const cleanedEmail = params.author?.email?.replace(/'/g, "''");
|
|
74
|
+
if (params.author) args.push("'--author'", `'${cleanedAuthor} <${cleanedEmail}>'`);
|
|
75
|
+
await db.execute(sql.raw("START TRANSACTION"));
|
|
76
|
+
let txFinalized = false;
|
|
77
|
+
try {
|
|
78
|
+
let result;
|
|
79
|
+
try {
|
|
80
|
+
result = await db.execute(sql.raw(`SELECT DOLT_MERGE(${args.join(", ")})`));
|
|
81
|
+
logger.info({ result }, "DOLT_MERGE result");
|
|
82
|
+
} catch (error) {
|
|
83
|
+
const cause = error?.cause;
|
|
84
|
+
logger.error({
|
|
85
|
+
message: error?.message,
|
|
86
|
+
code: cause?.code,
|
|
87
|
+
severity: cause?.severity,
|
|
88
|
+
detail: cause?.detail,
|
|
89
|
+
hint: cause?.hint,
|
|
90
|
+
query: error?.query,
|
|
91
|
+
fromBranch: params.fromBranch,
|
|
92
|
+
toBranch: params.toBranch
|
|
93
|
+
}, "Error merging branch");
|
|
94
|
+
throw error;
|
|
95
|
+
}
|
|
96
|
+
const conflicts = extractConflictCount(result.rows[0] ?? {});
|
|
97
|
+
if (Number.isFinite(conflicts) && conflicts > 0) {
|
|
98
|
+
const userResolutions = params.resolutions ?? [];
|
|
99
|
+
const autoResolutions = [];
|
|
100
|
+
const conflictTables = await doltConflicts(db)();
|
|
101
|
+
let manualConflicts = 0;
|
|
102
|
+
for (const ct of conflictTables) {
|
|
103
|
+
const tableConflicts = await doltTableConflicts(db)({ tableName: ct.table });
|
|
104
|
+
const pkColumns = managePkMap[ct.table] ?? [];
|
|
105
|
+
for (const row of tableConflicts) if (isTimestampOnlyConflictRow(row, pkColumns)) autoResolutions.push(buildTimestampAutoResolution(ct.table, row, pkColumns));
|
|
106
|
+
else manualConflicts++;
|
|
107
|
+
}
|
|
108
|
+
if (manualConflicts > 0 && userResolutions.length < manualConflicts) throw new MergeConflictError(manualConflicts > 0 && userResolutions.length === 0 ? "Merge has conflicts but no resolutions were provided." : `Resolutions provided (${userResolutions.length}) do not cover all conflicts (${manualConflicts}). All conflicts must be resolved.`, manualConflicts, params.fromBranch, params.toBranch);
|
|
109
|
+
const allResolutions = [...autoResolutions, ...userResolutions];
|
|
110
|
+
try {
|
|
111
|
+
await applyResolutions(db)(allResolutions);
|
|
112
|
+
} catch (error) {
|
|
113
|
+
if (error instanceof ResolutionValidationError) throw createApiError({
|
|
114
|
+
code: "bad_request",
|
|
115
|
+
message: `Invalid resolution: ${error.message}`
|
|
116
|
+
});
|
|
117
|
+
throw error;
|
|
118
|
+
}
|
|
119
|
+
await doltAddAndCommit(db)({
|
|
120
|
+
message: params.message ? `${params.message} (with conflict resolution)` : `Merge ${params.fromBranch} into ${params.toBranch} (with conflict resolution)`,
|
|
121
|
+
author: params.author
|
|
122
|
+
});
|
|
123
|
+
txFinalized = true;
|
|
124
|
+
return {
|
|
125
|
+
status: "success",
|
|
126
|
+
from: params.fromBranch,
|
|
127
|
+
to: params.toBranch,
|
|
128
|
+
toHead,
|
|
129
|
+
hasConflicts: true
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
await db.execute(sql.raw("COMMIT"));
|
|
133
|
+
txFinalized = true;
|
|
134
|
+
return {
|
|
135
|
+
status: "success",
|
|
136
|
+
from: params.fromBranch,
|
|
137
|
+
to: params.toBranch,
|
|
138
|
+
toHead,
|
|
139
|
+
hasConflicts: false
|
|
140
|
+
};
|
|
141
|
+
} finally {
|
|
142
|
+
if (!txFinalized) try {
|
|
143
|
+
await db.execute(sql.raw("ROLLBACK"));
|
|
144
|
+
} catch (rollbackError) {
|
|
145
|
+
logger.error({ error: rollbackError }, "Failed to rollback transaction");
|
|
146
|
+
}
|
|
147
|
+
}
|
|
34
148
|
};
|
|
35
149
|
/**
|
|
36
150
|
* Abort a merge
|
|
@@ -70,6 +184,29 @@ const doltSchemaConflicts = (db) => async () => {
|
|
|
70
184
|
return (await db.execute(sql`SELECT * FROM dolt_schema_conflicts`)).rows;
|
|
71
185
|
};
|
|
72
186
|
/**
|
|
187
|
+
* Preview merge conflicts without modifying the database (dry-run).
|
|
188
|
+
* Returns a summary of which tables have conflicts.
|
|
189
|
+
*/
|
|
190
|
+
const doltPreviewMergeConflictsSummary = (db) => async (params) => {
|
|
191
|
+
const escapedBaseBranch = params.baseBranch.replace(/'/g, "''");
|
|
192
|
+
const escapedMergeBranch = params.mergeBranch.replace(/'/g, "''");
|
|
193
|
+
return (await db.execute(sql.raw(`SELECT * FROM DOLT_PREVIEW_MERGE_CONFLICTS_SUMMARY('${escapedBaseBranch}', '${escapedMergeBranch}')`))).rows.map((row) => ({
|
|
194
|
+
table: row.table.replace("public.", ""),
|
|
195
|
+
numDataConflicts: Number(row.num_data_conflicts ?? 0),
|
|
196
|
+
numSchemaConflicts: Number(row.num_schema_conflicts ?? 0)
|
|
197
|
+
}));
|
|
198
|
+
};
|
|
199
|
+
/**
|
|
200
|
+
* Preview detailed merge conflicts for a specific table without modifying the database (dry-run).
|
|
201
|
+
* Returns the same column shape as dolt_conflicts_$table.
|
|
202
|
+
*/
|
|
203
|
+
const doltPreviewMergeConflicts = (db) => async (params) => {
|
|
204
|
+
const escapedBaseBranch = params.baseBranch.replace(/'/g, "''");
|
|
205
|
+
const escapedMergeBranch = params.mergeBranch.replace(/'/g, "''");
|
|
206
|
+
const escapedTableName = params.tableName.replace(/'/g, "''");
|
|
207
|
+
return (await db.execute(sql.raw(`SELECT * FROM DOLT_PREVIEW_MERGE_CONFLICTS('${escapedBaseBranch}', '${escapedMergeBranch}', '${escapedTableName}')`))).rows;
|
|
208
|
+
};
|
|
209
|
+
/**
|
|
73
210
|
* Resolve conflicts for a table using a strategy
|
|
74
211
|
*/
|
|
75
212
|
const doltResolveConflicts = (db) => async (params) => {
|
|
@@ -78,4 +215,4 @@ const doltResolveConflicts = (db) => async (params) => {
|
|
|
78
215
|
};
|
|
79
216
|
|
|
80
217
|
//#endregion
|
|
81
|
-
export { doltAbortMerge, doltConflicts, doltMerge, doltMergeStatus, doltResolveConflicts, doltSchemaConflicts, doltTableConflicts };
|
|
218
|
+
export { MergeConflictError, doltAbortMerge, doltConflicts, doltMerge, doltMergeStatus, doltPreviewMergeConflicts, doltPreviewMergeConflictsSummary, doltResolveConflicts, doltSchemaConflicts, doltTableConflicts };
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { manage_schema_exports } from "../db/manage/manage-schema.js";
|
|
2
|
+
import { getTableConfig } from "drizzle-orm/pg-core";
|
|
3
|
+
|
|
4
|
+
//#region src/dolt/pk-map.ts
|
|
5
|
+
function buildPkMapFromSchema() {
|
|
6
|
+
const pkMap = {};
|
|
7
|
+
for (const value of Object.values(manage_schema_exports)) {
|
|
8
|
+
if (value == null || typeof value !== "object") continue;
|
|
9
|
+
let config;
|
|
10
|
+
try {
|
|
11
|
+
config = getTableConfig(value);
|
|
12
|
+
} catch {
|
|
13
|
+
continue;
|
|
14
|
+
}
|
|
15
|
+
if (!config?.name || !config.primaryKeys?.length) continue;
|
|
16
|
+
const pkColumns = config.primaryKeys[0]?.columns.map((col) => col.name);
|
|
17
|
+
if (pkColumns && pkColumns.length > 0) pkMap[config.name] = pkColumns;
|
|
18
|
+
}
|
|
19
|
+
return pkMap;
|
|
20
|
+
}
|
|
21
|
+
const managePkMap = buildPkMapFromSchema();
|
|
22
|
+
const isValidManageTable = (tableName) => {
|
|
23
|
+
return tableName in managePkMap;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
//#endregion
|
|
27
|
+
export { isValidManageTable, managePkMap };
|
|
@@ -3,6 +3,7 @@ import { AgentsManageDatabaseClient } from "../db/manage/manage-client.js";
|
|
|
3
3
|
import { Context, Next } from "hono";
|
|
4
4
|
|
|
5
5
|
//#region src/dolt/ref-middleware.d.ts
|
|
6
|
+
declare function isMergeRoute(path: string): boolean;
|
|
6
7
|
type RefContext = {
|
|
7
8
|
resolvedRef?: ResolvedRef;
|
|
8
9
|
};
|
|
@@ -79,4 +80,4 @@ declare const refMiddlewareFactory: (db: AgentsManageDatabaseClient, options?: R
|
|
|
79
80
|
*/
|
|
80
81
|
declare const writeProtectionMiddlewareFactory: () => (c: Context, next: Next) => Promise<void>;
|
|
81
82
|
//#endregion
|
|
82
|
-
export { RefContext, RefMiddlewareOptions, createRefMiddleware, createWriteProtectionMiddleware, refMiddlewareFactory, writeProtectionMiddlewareFactory };
|
|
83
|
+
export { RefContext, RefMiddlewareOptions, createRefMiddleware, createWriteProtectionMiddleware, isMergeRoute, refMiddlewareFactory, writeProtectionMiddlewareFactory };
|
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
import { getLogger } from "../utils/logger.js";
|
|
2
|
+
import { createApiError } from "../utils/error.js";
|
|
2
3
|
import { isRefWritable, resolveRef } from "./ref-helpers.js";
|
|
3
4
|
import { ensureBranchExists } from "./branch.js";
|
|
4
|
-
import { createApiError } from "../utils/error.js";
|
|
5
5
|
|
|
6
6
|
//#region src/dolt/ref-middleware.ts
|
|
7
7
|
const logger = getLogger("ref-middleware");
|
|
8
|
+
const MERGE_ROUTE_PATTERN = /\/branches\/merge(\/preview)?$/;
|
|
9
|
+
function isMergeRoute(path) {
|
|
10
|
+
return MERGE_ROUTE_PATTERN.test(path);
|
|
11
|
+
}
|
|
8
12
|
/**
|
|
9
13
|
* Default tenant ID extractor - extracts from /tenants/{tenantId} path pattern
|
|
10
14
|
*/
|
|
@@ -43,8 +47,18 @@ const defaultExtractProjectId = (c) => {
|
|
|
43
47
|
const createRefMiddleware = (db, options = {}) => {
|
|
44
48
|
const { extractTenantId = defaultExtractTenantId, extractProjectId = defaultExtractProjectId, allowProjectIdFromBody = true } = options;
|
|
45
49
|
return async (c, next) => {
|
|
46
|
-
|
|
47
|
-
const
|
|
50
|
+
let ref = c.req.query("ref");
|
|
51
|
+
const path = c.req.path;
|
|
52
|
+
const pathSplit = path.split("/");
|
|
53
|
+
if (!ref && isMergeRoute(path) && c.req.method === "POST") try {
|
|
54
|
+
const body = await c.req.json();
|
|
55
|
+
if (body && typeof body.targetBranch === "string") {
|
|
56
|
+
ref = body.targetBranch;
|
|
57
|
+
logger.debug({ ref }, "Extracted targetBranch from merge route body as ref");
|
|
58
|
+
}
|
|
59
|
+
} catch {
|
|
60
|
+
logger.debug({}, "Could not extract targetBranch from merge route body");
|
|
61
|
+
}
|
|
48
62
|
const tenantId = extractTenantId(c);
|
|
49
63
|
let projectId = extractProjectId(c);
|
|
50
64
|
if (!projectId && allowProjectIdFromBody && [
|
|
@@ -214,4 +228,4 @@ const refMiddlewareFactory = createRefMiddleware;
|
|
|
214
228
|
const writeProtectionMiddlewareFactory = createWriteProtectionMiddleware;
|
|
215
229
|
|
|
216
230
|
//#endregion
|
|
217
|
-
export { createRefMiddleware, createWriteProtectionMiddleware, refMiddlewareFactory, writeProtectionMiddlewareFactory };
|
|
231
|
+
export { createRefMiddleware, createWriteProtectionMiddleware, isMergeRoute, refMiddlewareFactory, writeProtectionMiddlewareFactory };
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { ConflictResolution } from "../validation/dolt-schemas.js";
|
|
2
|
+
import { AgentsManageDatabaseClient } from "../db/manage/manage-client.js";
|
|
3
|
+
import { FkDeps } from "./fk-map.js";
|
|
4
|
+
|
|
5
|
+
//#region src/dolt/resolve-conflicts.d.ts
|
|
6
|
+
declare class ResolutionValidationError extends Error {
|
|
7
|
+
constructor(message: string);
|
|
8
|
+
}
|
|
9
|
+
type OperationType = 'skip' | 'delete' | 'update' | 'insert';
|
|
10
|
+
interface ClassifiedResolution {
|
|
11
|
+
resolution: ConflictResolution;
|
|
12
|
+
conflictRow: Record<string, unknown>;
|
|
13
|
+
pkColumns: string[];
|
|
14
|
+
operation: OperationType;
|
|
15
|
+
}
|
|
16
|
+
declare function computeTableInsertOrder(fkDeps: FkDeps): Map<string, number>;
|
|
17
|
+
declare function sortByFkDependencyOrder(classified: ClassifiedResolution[]): ClassifiedResolution[];
|
|
18
|
+
declare const applyResolutions: (db: AgentsManageDatabaseClient) => (resolutions: ConflictResolution[]) => Promise<void>;
|
|
19
|
+
//#endregion
|
|
20
|
+
export { ResolutionValidationError, applyResolutions, computeTableInsertOrder, sortByFkDependencyOrder };
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { isValidManageTable, managePkMap } from "./pk-map.js";
|
|
2
|
+
import { manageFkDeps } from "./fk-map.js";
|
|
3
|
+
import { sql } from "drizzle-orm";
|
|
4
|
+
|
|
5
|
+
//#region src/dolt/resolve-conflicts.ts
|
|
6
|
+
var ResolutionValidationError = class extends Error {
|
|
7
|
+
constructor(message) {
|
|
8
|
+
super(message);
|
|
9
|
+
this.name = "ResolutionValidationError";
|
|
10
|
+
}
|
|
11
|
+
};
|
|
12
|
+
function toSqlLiteral(val) {
|
|
13
|
+
if (val === null || val === void 0) return "NULL";
|
|
14
|
+
if (typeof val === "object") return `'${JSON.stringify(val).replace(/'/g, "''")}'`;
|
|
15
|
+
return `'${String(val).replace(/'/g, "''")}'`;
|
|
16
|
+
}
|
|
17
|
+
function classifyOperation(conflictRow, rowDefaultPick, hasColumnOverrides) {
|
|
18
|
+
const ourDiffType = conflictRow.our_diff_type;
|
|
19
|
+
const theirDiffType = conflictRow.their_diff_type;
|
|
20
|
+
if (rowDefaultPick === "theirs" && !hasColumnOverrides) {
|
|
21
|
+
if (theirDiffType === "removed") return "delete";
|
|
22
|
+
if (ourDiffType === "removed") return "insert";
|
|
23
|
+
return "update";
|
|
24
|
+
}
|
|
25
|
+
if (theirDiffType === "removed" || ourDiffType === "removed") {
|
|
26
|
+
if (rowDefaultPick === "ours") return "skip";
|
|
27
|
+
if (theirDiffType === "removed") return "delete";
|
|
28
|
+
if (ourDiffType === "removed") return "insert";
|
|
29
|
+
}
|
|
30
|
+
return "update";
|
|
31
|
+
}
|
|
32
|
+
function computeTableInsertOrder(fkDeps) {
|
|
33
|
+
const order = /* @__PURE__ */ new Map();
|
|
34
|
+
const visited = /* @__PURE__ */ new Set();
|
|
35
|
+
let counter = 0;
|
|
36
|
+
function visit(table) {
|
|
37
|
+
if (visited.has(table)) return;
|
|
38
|
+
visited.add(table);
|
|
39
|
+
for (const dep of fkDeps[table] ?? []) visit(dep);
|
|
40
|
+
order.set(table, counter++);
|
|
41
|
+
}
|
|
42
|
+
for (const table of Object.keys(fkDeps)) visit(table);
|
|
43
|
+
return order;
|
|
44
|
+
}
|
|
45
|
+
const PHASE_ORDER = {
|
|
46
|
+
delete: 0,
|
|
47
|
+
update: 1,
|
|
48
|
+
insert: 2,
|
|
49
|
+
skip: 3
|
|
50
|
+
};
|
|
51
|
+
const tableOrder = computeTableInsertOrder(manageFkDeps);
|
|
52
|
+
function sortByFkDependencyOrder(classified) {
|
|
53
|
+
return [...classified].sort((a, b) => {
|
|
54
|
+
const phaseA = PHASE_ORDER[a.operation];
|
|
55
|
+
const phaseB = PHASE_ORDER[b.operation];
|
|
56
|
+
if (phaseA !== phaseB) return phaseA - phaseB;
|
|
57
|
+
const orderA = tableOrder.get(a.resolution.table) ?? 0;
|
|
58
|
+
const orderB = tableOrder.get(b.resolution.table) ?? 0;
|
|
59
|
+
if (a.operation === "delete") return orderB - orderA;
|
|
60
|
+
if (a.operation === "insert") return orderA - orderB;
|
|
61
|
+
return 0;
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
const applyResolutions = (db) => async (resolutions) => {
|
|
65
|
+
const affectedTables = /* @__PURE__ */ new Set();
|
|
66
|
+
const classified = [];
|
|
67
|
+
for (const resolution of resolutions) {
|
|
68
|
+
if (!isValidManageTable(resolution.table)) throw new ResolutionValidationError(`Invalid table name: ${resolution.table}`);
|
|
69
|
+
affectedTables.add(resolution.table);
|
|
70
|
+
const pkColumns = managePkMap[resolution.table];
|
|
71
|
+
if (!pkColumns) throw new ResolutionValidationError(`No PK columns found for table: ${resolution.table}`);
|
|
72
|
+
const hasColumnOverrides = resolution.columns && Object.keys(resolution.columns).length > 0;
|
|
73
|
+
if (resolution.rowDefaultPick === "ours" && !hasColumnOverrides) {
|
|
74
|
+
classified.push({
|
|
75
|
+
resolution,
|
|
76
|
+
conflictRow: {},
|
|
77
|
+
pkColumns,
|
|
78
|
+
operation: "skip"
|
|
79
|
+
});
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
const conflictRow = await readConflictRow(db, resolution.table, resolution.primaryKey, pkColumns);
|
|
83
|
+
if (!conflictRow) throw new ResolutionValidationError(`No conflict found for table ${resolution.table} with PK ${JSON.stringify(resolution.primaryKey)}`);
|
|
84
|
+
if (hasColumnOverrides) {
|
|
85
|
+
const ourDiffType = conflictRow.our_diff_type;
|
|
86
|
+
const theirDiffType = conflictRow.their_diff_type;
|
|
87
|
+
if (Object.values(resolution.columns ?? {}).some((pick) => pick !== resolution.rowDefaultPick) && (ourDiffType === "removed" || theirDiffType === "removed")) {
|
|
88
|
+
const removedSide = ourDiffType === "removed" ? "ours" : "theirs";
|
|
89
|
+
throw new ResolutionValidationError(`Cannot apply column overrides for table ${resolution.table} (PK ${JSON.stringify(resolution.primaryKey)}): ${removedSide} side deleted the row`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
const operation = classifyOperation(conflictRow, resolution.rowDefaultPick, hasColumnOverrides ?? false);
|
|
93
|
+
classified.push({
|
|
94
|
+
resolution,
|
|
95
|
+
conflictRow,
|
|
96
|
+
pkColumns,
|
|
97
|
+
operation
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
const sorted = sortByFkDependencyOrder(classified);
|
|
101
|
+
for (const { resolution, conflictRow, pkColumns, operation } of sorted) {
|
|
102
|
+
if (operation === "skip") continue;
|
|
103
|
+
const pkWhere = buildPkWhere(pkColumns, resolution.primaryKey);
|
|
104
|
+
switch (operation) {
|
|
105
|
+
case "delete":
|
|
106
|
+
await db.execute(sql.raw(`DELETE FROM "${resolution.table}" WHERE ${pkWhere}`));
|
|
107
|
+
break;
|
|
108
|
+
case "insert": {
|
|
109
|
+
const columns = getColumnNames(conflictRow, pkColumns);
|
|
110
|
+
const allCols = [...pkColumns, ...columns];
|
|
111
|
+
const values = allCols.map((col) => {
|
|
112
|
+
return toSqlLiteral(pkColumns.includes(col) ? resolution.primaryKey[col] : conflictRow[`their_${col}`]);
|
|
113
|
+
});
|
|
114
|
+
await db.execute(sql.raw(`INSERT INTO "${resolution.table}" (${allCols.map((c) => `"${c}"`).join(", ")}) VALUES (${values.join(", ")})`));
|
|
115
|
+
break;
|
|
116
|
+
}
|
|
117
|
+
case "update": {
|
|
118
|
+
const setClauses = getColumnNames(conflictRow, pkColumns).map((col) => {
|
|
119
|
+
return `"${col}" = ${toSqlLiteral(conflictRow[`${(resolution.columns?.[col] ?? resolution.rowDefaultPick) === "theirs" ? "their_" : "our_"}${col}`])}`;
|
|
120
|
+
});
|
|
121
|
+
await db.execute(sql.raw(`UPDATE "${resolution.table}" SET ${setClauses.join(", ")} WHERE ${pkWhere}`));
|
|
122
|
+
break;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
for (const table of affectedTables) await db.execute(sql.raw(`SELECT DOLT_CONFLICTS_RESOLVE('--ours', '${table}')`));
|
|
127
|
+
};
|
|
128
|
+
async function readConflictRow(db, table, primaryKey, pkColumns) {
|
|
129
|
+
const whereClause = pkColumns.map((col) => {
|
|
130
|
+
const val = primaryKey[col];
|
|
131
|
+
if (val === void 0) throw new ResolutionValidationError(`Missing PK column ${col} for table ${table}`);
|
|
132
|
+
const escapedVal = val.replace(/'/g, "''");
|
|
133
|
+
return `(base_${col} = '${escapedVal}' OR (base_${col} IS NULL AND (our_${col} = '${escapedVal}' OR their_${col} = '${escapedVal}')))`;
|
|
134
|
+
}).join(" AND ");
|
|
135
|
+
return (await db.execute(sql.raw(`SELECT * FROM dolt_conflicts_${table} WHERE ${whereClause} LIMIT 1`))).rows[0] ?? null;
|
|
136
|
+
}
|
|
137
|
+
function buildPkWhere(pkColumns, primaryKey) {
|
|
138
|
+
return pkColumns.map((col) => {
|
|
139
|
+
return `"${col}" = '${primaryKey[col]?.replace(/'/g, "''")}'`;
|
|
140
|
+
}).join(" AND ");
|
|
141
|
+
}
|
|
142
|
+
function getColumnNames(conflictRow, pkColumns) {
|
|
143
|
+
const theirPrefix = "their_";
|
|
144
|
+
const skipColumns = new Set([
|
|
145
|
+
"our_diff_type",
|
|
146
|
+
"their_diff_type",
|
|
147
|
+
"base_diff_type",
|
|
148
|
+
...pkColumns.map((c) => `base_${c}`),
|
|
149
|
+
...pkColumns.map((c) => `our_${c}`),
|
|
150
|
+
...pkColumns.map((c) => `their_${c}`)
|
|
151
|
+
]);
|
|
152
|
+
const columns = [];
|
|
153
|
+
for (const key of Object.keys(conflictRow)) if (key.startsWith(theirPrefix) && !skipColumns.has(key)) columns.push(key.slice(6));
|
|
154
|
+
return columns;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
//#endregion
|
|
158
|
+
export { ResolutionValidationError, applyResolutions, computeTableInsertOrder, sortByFkDependencyOrder };
|
package/dist/dolt/schema-sync.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { doltAddAndCommit, doltStatus } from "./commit.js";
|
|
2
|
-
import {
|
|
2
|
+
import { MergeConflictError, doltMerge } from "./merge.js";
|
|
3
3
|
import { sql } from "drizzle-orm";
|
|
4
4
|
import { createHash } from "node:crypto";
|
|
5
5
|
|
|
@@ -135,23 +135,26 @@ const syncSchemaFromMain = (db) => async (options = {}) => {
|
|
|
135
135
|
error: "Cannot sync schema: uncommitted changes exist. Commit changes first or set autoCommitPending: true"
|
|
136
136
|
};
|
|
137
137
|
const mergeSchemaMessage = `Synced schema from ${SCHEMA_SOURCE_BRANCH}`;
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
138
|
+
const schemaSyncAuthor = {
|
|
139
|
+
name: "Schema Sync System",
|
|
140
|
+
email: "system@inkeep.com"
|
|
141
|
+
};
|
|
142
|
+
try {
|
|
143
|
+
await doltMerge(db)({
|
|
144
|
+
fromBranch: SCHEMA_SOURCE_BRANCH,
|
|
145
|
+
toBranch: currentBranch,
|
|
146
|
+
message: mergeSchemaMessage,
|
|
147
|
+
noFastForward: true,
|
|
148
|
+
author: schemaSyncAuthor
|
|
149
|
+
});
|
|
150
|
+
} catch (error) {
|
|
151
|
+
if (error instanceof MergeConflictError) return {
|
|
150
152
|
synced: false,
|
|
151
153
|
hadDifferences: true,
|
|
152
154
|
differences,
|
|
153
|
-
error: "Schema merge produced conflicts that require manual resolution. Merge has been
|
|
155
|
+
error: "Schema merge produced conflicts that require manual resolution. Merge has been rolled back."
|
|
154
156
|
};
|
|
157
|
+
throw error;
|
|
155
158
|
}
|
|
156
159
|
return {
|
|
157
160
|
synced: true,
|
|
@@ -160,9 +163,6 @@ const syncSchemaFromMain = (db) => async (options = {}) => {
|
|
|
160
163
|
mergeCommitHash: await getLatestCommitHash(db)()
|
|
161
164
|
};
|
|
162
165
|
} catch (error) {
|
|
163
|
-
try {
|
|
164
|
-
await doltAbortMerge(db)();
|
|
165
|
-
} catch {}
|
|
166
166
|
return {
|
|
167
167
|
synced: false,
|
|
168
168
|
hadDifferences: true,
|