@coffer-org/server 3.3.0 → 4.0.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-api.js +4 -5
- package/dist/cache-clock.d.ts +8 -0
- package/dist/cache-clock.js +103 -0
- package/dist/cache-refresh.d.ts +7 -0
- package/dist/cache-refresh.js +322 -0
- package/dist/collection-io.d.ts +13 -0
- package/dist/collection-io.js +42 -28
- package/dist/compute-unit.d.ts +66 -0
- package/dist/compute-unit.js +369 -0
- package/dist/condition-where.js +1 -3
- package/dist/data-dir.d.ts +3 -0
- package/dist/data-dir.js +39 -0
- package/dist/db.js +2 -0
- package/dist/entity-schema.d.ts +1 -0
- package/dist/entity-schema.js +25 -2
- package/dist/extend-io.d.ts +3 -2
- package/dist/extend-io.js +4 -4
- package/dist/extend-table.d.ts +3 -2
- package/dist/extend-table.js +48 -11
- package/dist/file-fields.js +50 -23
- package/dist/global-search.js +1 -1
- package/dist/http-errors.d.ts +7 -0
- package/dist/http-errors.js +24 -0
- package/dist/index.js +31 -35
- package/dist/mcp-contract/client.d.ts +24 -0
- package/dist/mcp-contract/client.js +12 -0
- package/dist/mcp-contract/schema.d.ts +46 -0
- package/dist/mcp-contract/schema.js +90 -0
- package/dist/mcp-contract/tools.d.ts +19 -0
- package/dist/mcp-contract/tools.js +160 -0
- package/dist/mcp-local.d.ts +1 -1
- package/dist/mcp-local.js +4 -2
- package/dist/mcp-tools.d.ts +12 -1
- package/dist/mcp-tools.js +41 -8
- package/dist/migrations.d.ts +30 -2
- package/dist/migrations.js +368 -19
- package/dist/mutate.js +86 -57
- package/dist/orchestrator/agent-capabilities.d.ts +2 -0
- package/dist/orchestrator/agent-capabilities.js +201 -0
- package/dist/orchestrator/allow.d.ts +16 -0
- package/dist/orchestrator/allow.js +65 -0
- package/dist/orchestrator/attachments.d.ts +2 -0
- package/dist/orchestrator/attachments.js +11 -0
- package/dist/orchestrator/config.d.ts +4 -0
- package/dist/orchestrator/config.js +19 -0
- package/dist/orchestrator/db.d.ts +17 -0
- package/dist/orchestrator/db.js +12 -0
- package/dist/orchestrator/diagnostics.d.ts +9 -0
- package/dist/orchestrator/diagnostics.js +13 -0
- package/dist/orchestrator/environment.d.ts +3 -0
- package/dist/orchestrator/environment.js +30 -0
- package/dist/orchestrator/file-inspection.d.ts +3 -0
- package/dist/orchestrator/file-inspection.js +61 -0
- package/dist/orchestrator/format.d.ts +1 -0
- package/dist/orchestrator/format.js +6 -0
- package/dist/orchestrator/index.d.ts +30 -0
- package/dist/orchestrator/index.js +56 -0
- package/dist/orchestrator/live-message.d.ts +14 -0
- package/dist/orchestrator/live-message.js +100 -0
- package/dist/orchestrator/pipeline.d.ts +11 -0
- package/dist/orchestrator/pipeline.js +198 -0
- package/dist/orchestrator/registry.d.ts +13 -0
- package/dist/orchestrator/registry.js +74 -0
- package/dist/orchestrator/starters.d.ts +19 -0
- package/dist/orchestrator/starters.js +81 -0
- package/dist/orchestrator/suggestion-capabilities.d.ts +5 -0
- package/dist/orchestrator/suggestion-capabilities.js +33 -0
- package/dist/orchestrator/system-assembly.d.ts +7 -0
- package/dist/orchestrator/system-assembly.js +13 -0
- package/dist/orchestrator/system-capabilities.d.ts +2 -0
- package/dist/orchestrator/system-capabilities.js +63 -0
- package/dist/orchestrator/types.d.ts +144 -0
- package/dist/orchestrator/types.js +1 -0
- package/dist/part-errors.d.ts +3 -0
- package/dist/part-errors.js +6 -0
- package/dist/part-injection.d.ts +11 -3
- package/dist/part-injection.js +72 -92
- package/dist/part-ref.d.ts +13 -0
- package/dist/part-ref.js +109 -0
- package/dist/plugin-hooks.d.ts +1 -0
- package/dist/plugin-i18n.js +2 -1
- package/dist/plugin-runtime.d.ts +2 -2
- package/dist/plugin-runtime.js +21 -11
- package/dist/plugin-updates.d.ts +2 -2
- package/dist/plugin-updates.js +6 -12
- package/dist/public-url.js +2 -1
- package/dist/recompute-derived.d.ts +2 -1
- package/dist/recompute-derived.js +123 -26
- package/dist/records-api.d.ts +5 -1
- package/dist/records-api.js +47 -15
- package/dist/registry-context.d.ts +1 -1
- package/dist/registry-context.js +3 -0
- package/dist/rename-guard.d.ts +1 -0
- package/dist/rename-guard.js +9 -0
- package/dist/schema-sync.js +38 -1
- package/dist/search-index.js +1 -1
- package/dist/search-indexer.d.ts +1 -1
- package/dist/search-indexer.js +3 -3
- package/dist/settings-groups.d.ts +14 -0
- package/dist/settings-groups.js +25 -0
- package/dist/settings-write.d.ts +4 -4
- package/dist/settings-write.js +23 -26
- package/dist/system-settings.d.ts +3 -0
- package/dist/system-settings.js +27 -0
- package/dist/thread-store.d.ts +3 -2
- package/dist/thread-store.js +6 -5
- package/dist/trigger-queue.d.ts +33 -0
- package/dist/trigger-queue.js +432 -0
- package/dist/uploads.js +6 -3
- package/dist/value-compare.d.ts +1 -0
- package/dist/value-compare.js +7 -0
- package/package.json +17 -3
package/dist/migrations.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { DatabaseSchema } from '@mikro-orm/sql';
|
|
2
|
+
import { fieldEntries } from '@coffer-org/sdk/shelf';
|
|
2
3
|
import { dialectOf } from "./dialect.js";
|
|
3
4
|
import { getLogger } from '@coffer-org/sdk/logger';
|
|
5
|
+
import { SYSTEM_SETTINGS } from "./system-settings.js";
|
|
4
6
|
const log = getLogger('migrations');
|
|
5
7
|
function sqlType(col) {
|
|
6
8
|
if (col === 'integer' || col === 'boolean')
|
|
@@ -25,6 +27,13 @@ function pgType(col) {
|
|
|
25
27
|
return 'text';
|
|
26
28
|
}
|
|
27
29
|
const q = (id) => `"${id.replace(/"/g, '""')}"`;
|
|
30
|
+
const handedOverColumns = new Set();
|
|
31
|
+
export function markColumnHandedOver(table, column) {
|
|
32
|
+
handedOverColumns.add(`${table}.${column}`);
|
|
33
|
+
}
|
|
34
|
+
export function isColumnHandedOver(table, column) {
|
|
35
|
+
return handedOverColumns.has(`${table}.${column}`);
|
|
36
|
+
}
|
|
28
37
|
export async function introspectTable(em, table) {
|
|
29
38
|
const conn = em.getConnection();
|
|
30
39
|
const platform = em.getPlatform();
|
|
@@ -38,31 +47,122 @@ export async function introspectTable(em, table) {
|
|
|
38
47
|
: [],
|
|
39
48
|
};
|
|
40
49
|
}
|
|
41
|
-
export function
|
|
50
|
+
export async function introspectAllColumns(em, tables) {
|
|
51
|
+
const out = new Map();
|
|
52
|
+
if (!tables.length)
|
|
53
|
+
return out;
|
|
54
|
+
const conn = em.getConnection();
|
|
55
|
+
const platform = em.getPlatform();
|
|
56
|
+
const schema = await DatabaseSchema.create(conn, platform, em.config, undefined, undefined, tables);
|
|
57
|
+
for (const t of schema.getTables()) {
|
|
58
|
+
out.set(t.name, new Set(t.getColumns().map((c) => c.name)));
|
|
59
|
+
}
|
|
60
|
+
return out;
|
|
61
|
+
}
|
|
62
|
+
export const CONCERNING_CODES = new Set([
|
|
63
|
+
'no-source-column',
|
|
64
|
+
'target-column-exists',
|
|
65
|
+
'converted-nothing',
|
|
66
|
+
]);
|
|
67
|
+
export function describeMigrationConcerns(plugin, version, ops) {
|
|
68
|
+
const concerning = ops.filter((o) => !o.performed && o.code && CONCERNING_CODES.has(o.code));
|
|
69
|
+
const lopsided = ops.filter((o) => o.performed && o.emptyTargets?.length);
|
|
70
|
+
const stranded = ops.filter((o) => o.strandedSources?.length);
|
|
71
|
+
const performedAny = ops.some((o) => o.performed);
|
|
72
|
+
const describe = (o) => {
|
|
73
|
+
const bits = [];
|
|
74
|
+
if (!o.performed)
|
|
75
|
+
bits.push(`changed nothing — ${o.reason ?? 'unknown reason'}`);
|
|
76
|
+
if (o.emptyTargets?.length)
|
|
77
|
+
bits.push(`wrote nothing into ${o.emptyTargets.join(', ')} on any of ${o.scanned ?? 0} row(s)`);
|
|
78
|
+
if (o.strandedSources?.length)
|
|
79
|
+
bits.push(`left ${o.strandedSources.join(', ')} holding data that reached no target column`);
|
|
80
|
+
return `${o.op}(${o.table}) ${bits.join(', and ')}`;
|
|
81
|
+
};
|
|
82
|
+
const tail = `The journal is bumped to v${version}, so it will never run again on this database — ` +
|
|
83
|
+
`if data was supposed to move, it did not, and a later schema sync will drop the columns it still lives in.`;
|
|
84
|
+
if (!ops.length)
|
|
85
|
+
return (`${plugin} v${version}: the migration completed without performing a single table operation. ` +
|
|
86
|
+
`The journal is bumped to v${version} regardless, so if its body swallowed an error or was ` +
|
|
87
|
+
`written for a state this database is not in, it will never run again.`);
|
|
88
|
+
if (!concerning.length && !lopsided.length && !stranded.length)
|
|
89
|
+
return null;
|
|
90
|
+
if (!performedAny)
|
|
91
|
+
return `${plugin} v${version}: the migration completed WITHOUT changing anything (${ops
|
|
92
|
+
.map(describe)
|
|
93
|
+
.join('; ')}). ${tail}`;
|
|
94
|
+
const flagged = [...new Set([...concerning, ...lopsided, ...stranded])];
|
|
95
|
+
return (`${plugin} v${version}: the migration changed some data but ${flagged.length} of its operations did not ` +
|
|
96
|
+
`(${flagged.map(describe).join('; ')}). ${tail}`);
|
|
97
|
+
}
|
|
98
|
+
function sameCell(a, b) {
|
|
99
|
+
const na = a === undefined ? null : a;
|
|
100
|
+
const nb = b === undefined ? null : b;
|
|
101
|
+
if (na === null || nb === null)
|
|
102
|
+
return na === nb;
|
|
103
|
+
return String(na) === String(nb);
|
|
104
|
+
}
|
|
105
|
+
export function makeTable(em, table, record) {
|
|
42
106
|
const conn = em.getConnection();
|
|
43
107
|
const T = q(table);
|
|
108
|
+
const countRows = async (where = '') => {
|
|
109
|
+
const rows = (await conn.execute(`SELECT COUNT(*) AS n FROM ${T}${where ? ` WHERE ${where}` : ''}`));
|
|
110
|
+
return Number(rows[0]?.n ?? 0);
|
|
111
|
+
};
|
|
112
|
+
const done = (op, rows, scanned) => record?.({ op, table, performed: true, rows, ...(scanned === undefined ? {} : { scanned }) });
|
|
113
|
+
const nothing = (op, code, reason, extra) => record?.({ op, table, performed: false, code, reason, ...extra });
|
|
44
114
|
return {
|
|
45
115
|
async renameColumn(from, to) {
|
|
46
116
|
const info = await introspectTable(em, table);
|
|
47
|
-
if (!info.exists
|
|
48
|
-
return;
|
|
117
|
+
if (!info.exists)
|
|
118
|
+
return nothing('renameColumn', 'no-table', `table ${table} does not exist`);
|
|
119
|
+
if (!info.columns.has(from))
|
|
120
|
+
return nothing('renameColumn', 'no-source-column', `source column ${from} does not exist on a table that does — an earlier schema sync already dropped it`);
|
|
121
|
+
if (info.columns.has(to)) {
|
|
122
|
+
const stranded = await countRows(`${q(from)} IS NOT NULL`);
|
|
123
|
+
log.warn(`${table}: renameColumn(${from} → ${to}) skipped — target column already exists`);
|
|
124
|
+
return nothing('renameColumn', 'target-column-exists', `target column ${to} already exists, so the rename was skipped while ${stranded} row(s) still hold data in ${from}`, { rows: 0, scanned: stranded });
|
|
125
|
+
}
|
|
126
|
+
const moved = await countRows(`${q(from)} IS NOT NULL`);
|
|
49
127
|
await conn.execute(`ALTER TABLE ${T} RENAME COLUMN ${q(from)} TO ${q(to)}`);
|
|
128
|
+
done('renameColumn', moved, await countRows());
|
|
50
129
|
},
|
|
51
130
|
async fill(column, value, opts) {
|
|
52
131
|
const info = await introspectTable(em, table);
|
|
53
|
-
if (!info.exists
|
|
54
|
-
return;
|
|
132
|
+
if (!info.exists)
|
|
133
|
+
return nothing('fill', 'no-table', `table ${table} does not exist`);
|
|
134
|
+
if (!info.columns.has(column))
|
|
135
|
+
return nothing('fill', 'no-source-column', `column ${column} does not exist on a table that does — an earlier schema sync already dropped it`);
|
|
55
136
|
const where = opts?.where ?? {};
|
|
56
137
|
const keys = Object.keys(where);
|
|
57
138
|
const clauses = keys.map((k) => (where[k] === null ? `${q(k)} IS NULL` : `${q(k)} = ?`));
|
|
58
|
-
const
|
|
139
|
+
const whereParams = keys.filter((k) => where[k] !== null).map((k) => where[k]);
|
|
140
|
+
const params = [value, ...whereParams];
|
|
141
|
+
const matchSql = clauses.join(' AND ');
|
|
142
|
+
const differs = `(${q(column)} IS NULL OR ${q(column)} <> ?)`;
|
|
143
|
+
const countWhere = [matchSql, differs].filter(Boolean).join(' AND ');
|
|
144
|
+
const changing = (await conn.execute(`SELECT COUNT(*) AS n FROM ${T} WHERE ${countWhere}`, [
|
|
145
|
+
...whereParams,
|
|
146
|
+
value,
|
|
147
|
+
]));
|
|
148
|
+
const changed = Number(changing[0]?.n ?? 0);
|
|
59
149
|
const sql = `UPDATE ${T} SET ${q(column)} = ?` + (clauses.length ? ` WHERE ${clauses.join(' AND ')}` : '');
|
|
60
150
|
await conn.execute(sql, params);
|
|
151
|
+
const scanned = await countRows();
|
|
152
|
+
if (changed)
|
|
153
|
+
return done('fill', changed, scanned);
|
|
154
|
+
if (!scanned)
|
|
155
|
+
return nothing('fill', 'no-rows', `table ${table} has no rows`, { rows: 0, scanned });
|
|
156
|
+
nothing('fill', 'no-change', `every matching row in ${column} already held that value`, { rows: 0, scanned });
|
|
61
157
|
},
|
|
62
158
|
async changeType(column, newType, opts) {
|
|
63
159
|
const info = await introspectTable(em, table);
|
|
64
|
-
if (!info.exists
|
|
65
|
-
return;
|
|
160
|
+
if (!info.exists)
|
|
161
|
+
return nothing('changeType', 'no-table', `table ${table} does not exist`);
|
|
162
|
+
if (!info.columns.has(column))
|
|
163
|
+
return nothing('changeType', 'no-source-column', `column ${column} does not exist on a table that does — an earlier schema sync already dropped it`);
|
|
164
|
+
const carried = await countRows(`${q(column)} IS NOT NULL`);
|
|
165
|
+
const total = await countRows();
|
|
66
166
|
const tmp = `__ct_${column}`;
|
|
67
167
|
const def = opts?.default;
|
|
68
168
|
if (dialectOf(em) === 'sqlite') {
|
|
@@ -108,6 +208,7 @@ export function makeTable(em, table) {
|
|
|
108
208
|
await conn.execute(`ROLLBACK`);
|
|
109
209
|
throw e;
|
|
110
210
|
}
|
|
211
|
+
done('changeType', carried, total);
|
|
111
212
|
return;
|
|
112
213
|
}
|
|
113
214
|
await conn.execute(`BEGIN`);
|
|
@@ -135,13 +236,14 @@ export function makeTable(em, table) {
|
|
|
135
236
|
await conn.execute(`ROLLBACK`);
|
|
136
237
|
throw e;
|
|
137
238
|
}
|
|
239
|
+
done('changeType', carried, total);
|
|
138
240
|
},
|
|
139
241
|
async convert(c) {
|
|
140
242
|
const info = await introspectTable(em, table);
|
|
141
243
|
if (!info.exists)
|
|
142
|
-
return;
|
|
244
|
+
return nothing('convert', 'no-table', `table ${table} does not exist`);
|
|
143
245
|
if (!c.from.some((f) => info.columns.has(f)))
|
|
144
|
-
return;
|
|
246
|
+
return nothing('convert', 'no-source-column', `none of the source columns (${c.from.join(', ')}) exist on a table that does — an earlier schema sync already dropped them`);
|
|
145
247
|
const sources = new Set(c.from);
|
|
146
248
|
for (const t of c.to) {
|
|
147
249
|
if (info.columns.has(t.name) && !sources.has(t.name)) {
|
|
@@ -152,24 +254,77 @@ export function makeTable(em, table) {
|
|
|
152
254
|
const idCol = sqlite ? 'rowid' : 'id';
|
|
153
255
|
const colType = sqlite ? sqlType : pgType;
|
|
154
256
|
const present = c.from.filter((f) => info.columns.has(f));
|
|
257
|
+
const readable = [...new Set([...present, ...c.to.filter((t) => info.columns.has(t.name)).map((t) => t.name)])];
|
|
258
|
+
let scanned = 0;
|
|
259
|
+
let changed = 0;
|
|
260
|
+
let filled = 0;
|
|
261
|
+
const filledPerTarget = new Map(c.to.map((t) => [t.name, 0]));
|
|
262
|
+
const strandedPerSource = new Map(present.filter((f) => !c.to.some((t) => t.name === f)).map((f) => [f, 0]));
|
|
263
|
+
const inPlaceTargets = c.to.filter((t) => sources.has(t.name)).map((t) => t.name);
|
|
264
|
+
const blankTargets = Object.fromEntries(inPlaceTargets.map((n) => [n, null]));
|
|
265
|
+
const normalize = (dst) => c.to.map((t) => {
|
|
266
|
+
const v = dst[t.name];
|
|
267
|
+
return v === undefined || (typeof v === 'number' && Number.isNaN(v)) ? null : v;
|
|
268
|
+
});
|
|
155
269
|
await conn.execute(`BEGIN`);
|
|
156
270
|
try {
|
|
157
271
|
for (const t of c.to) {
|
|
158
272
|
if (!info.columns.has(t.name))
|
|
159
273
|
await conn.execute(`ALTER TABLE ${T} ADD COLUMN ${q(t.name)} ${colType(t.type)}`);
|
|
160
274
|
}
|
|
161
|
-
const select =
|
|
275
|
+
const select = readable.map((f) => q(f)).join(', ');
|
|
162
276
|
const rows = (await conn.execute(`SELECT ${idCol} AS __rid, ${select} FROM ${T}`));
|
|
163
277
|
const setSql = c.to.map((t) => `${q(t.name)} = ?`).join(', ');
|
|
164
278
|
for (const r of rows) {
|
|
279
|
+
scanned++;
|
|
165
280
|
const src = {};
|
|
166
281
|
for (const f of c.from)
|
|
167
282
|
src[f] = r[f] ?? null;
|
|
168
|
-
const
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
283
|
+
const params = normalize(c.map(src));
|
|
284
|
+
if (params.some((v) => v !== null))
|
|
285
|
+
filled++;
|
|
286
|
+
c.to.forEach((t, i) => {
|
|
287
|
+
if (params[i] !== null)
|
|
288
|
+
filledPerTarget.set(t.name, filledPerTarget.get(t.name) + 1);
|
|
172
289
|
});
|
|
290
|
+
const differs = c.to.some((t, i) => !sameCell(params[i], r[t.name] ?? null));
|
|
291
|
+
if (strandedPerSource.size) {
|
|
292
|
+
const probe = (over) => {
|
|
293
|
+
try {
|
|
294
|
+
return normalize(c.map({ ...src, ...over }));
|
|
295
|
+
}
|
|
296
|
+
catch {
|
|
297
|
+
return null;
|
|
298
|
+
}
|
|
299
|
+
};
|
|
300
|
+
let fresh;
|
|
301
|
+
const carriedAcross = (f, sv) => {
|
|
302
|
+
if (params.some((v) => v !== null && sameCell(v, sv)))
|
|
303
|
+
return true;
|
|
304
|
+
const without = probe({ [f]: null });
|
|
305
|
+
if (without && params.some((v, i) => v !== null && !sameCell(v, without[i])))
|
|
306
|
+
return true;
|
|
307
|
+
if (!inPlaceTargets.length)
|
|
308
|
+
return false;
|
|
309
|
+
if (fresh === undefined)
|
|
310
|
+
fresh = probe(blankTargets);
|
|
311
|
+
const freshWithout = fresh ? probe({ ...blankTargets, [f]: null }) : null;
|
|
312
|
+
if (!fresh || !freshWithout)
|
|
313
|
+
return false;
|
|
314
|
+
return fresh.some((v, i) => v !== null && !sameCell(v, freshWithout[i]) && sameCell(params[i], v));
|
|
315
|
+
};
|
|
316
|
+
for (const [f, n] of strandedPerSource) {
|
|
317
|
+
const sv = src[f];
|
|
318
|
+
if (sv === null)
|
|
319
|
+
continue;
|
|
320
|
+
if (carriedAcross(f, sv))
|
|
321
|
+
continue;
|
|
322
|
+
strandedPerSource.set(f, n + 1);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
if (!differs)
|
|
326
|
+
continue;
|
|
327
|
+
changed++;
|
|
173
328
|
await conn.execute(`UPDATE ${T} SET ${setSql} WHERE ${idCol} = ?`, [...params, r['__rid']]);
|
|
174
329
|
}
|
|
175
330
|
await conn.execute(`COMMIT`);
|
|
@@ -178,11 +333,43 @@ export function makeTable(em, table) {
|
|
|
178
333
|
await conn.execute(`ROLLBACK`);
|
|
179
334
|
throw e;
|
|
180
335
|
}
|
|
336
|
+
for (const [f, left] of strandedPerSource) {
|
|
337
|
+
if (left === 0) {
|
|
338
|
+
markColumnHandedOver(table, f);
|
|
339
|
+
continue;
|
|
340
|
+
}
|
|
341
|
+
log.warn(`${table}.${f}: convert left ${left} row(s) unconverted — their value in ${f} reached no target column ` +
|
|
342
|
+
`(the map declined them, or a target-first map kept a value already stored). ` +
|
|
343
|
+
`The column is NOT handed over to schema sync: it stays under the drop warning until the rows are repaired or removed.`);
|
|
344
|
+
}
|
|
345
|
+
const emptyTargets = [...filledPerTarget].filter(([, n]) => n === 0).map(([name]) => name);
|
|
346
|
+
const lopsided = emptyTargets.length && emptyTargets.length < c.to.length ? emptyTargets : [];
|
|
347
|
+
const strandedSources = [...strandedPerSource].filter(([, left]) => left > 0).map(([f]) => f);
|
|
348
|
+
const stranded = strandedSources.length ? { strandedSources } : {};
|
|
349
|
+
if (changed)
|
|
350
|
+
return record?.({
|
|
351
|
+
op: 'convert',
|
|
352
|
+
table,
|
|
353
|
+
performed: true,
|
|
354
|
+
rows: changed,
|
|
355
|
+
scanned,
|
|
356
|
+
...(lopsided.length ? { emptyTargets: lopsided } : {}),
|
|
357
|
+
...stranded,
|
|
358
|
+
});
|
|
359
|
+
if (!scanned)
|
|
360
|
+
return nothing('convert', 'no-rows', `table ${table} has no rows`, { rows: 0, scanned });
|
|
361
|
+
if (!filled)
|
|
362
|
+
return nothing('convert', 'converted-nothing', `no row has a value in any target column (${c.to.map((t) => t.name).join(', ')}) across ${scanned} row(s). ` +
|
|
363
|
+
`Column state cannot tell the two causes apart: the field may simply never have been used on this ` +
|
|
364
|
+
`install, or its source data was dropped before this migration ran. Check one record before assuming either`, { rows: 0, scanned, ...stranded });
|
|
365
|
+
nothing('convert', 'no-change', `every one of ${scanned} row(s) already held the converted value in ${c.to.map((t) => t.name).join(', ')}`, { rows: 0, scanned, ...stranded });
|
|
181
366
|
},
|
|
182
367
|
async dropColumn(column) {
|
|
183
368
|
const info = await introspectTable(em, table);
|
|
184
|
-
if (!info.exists
|
|
185
|
-
return;
|
|
369
|
+
if (!info.exists)
|
|
370
|
+
return nothing('dropColumn', 'no-table', `table ${table} does not exist`);
|
|
371
|
+
if (!info.columns.has(column))
|
|
372
|
+
return nothing('dropColumn', 'no-change', `column ${column} is already absent`, { rows: 0 });
|
|
186
373
|
if (dialectOf(em) === 'sqlite') {
|
|
187
374
|
const idxList = (await conn.execute(`PRAGMA index_list(${q(table)})`));
|
|
188
375
|
for (const idx of idxList) {
|
|
@@ -197,13 +384,136 @@ export function makeTable(em, table) {
|
|
|
197
384
|
await conn.execute(`DROP INDEX ${q(idx.name)}`);
|
|
198
385
|
}
|
|
199
386
|
}
|
|
387
|
+
const held = await countRows(`${q(column)} IS NOT NULL`);
|
|
388
|
+
const total = await countRows();
|
|
200
389
|
await conn.execute(`ALTER TABLE ${T} DROP COLUMN ${q(column)}`);
|
|
390
|
+
done('dropColumn', held, total);
|
|
201
391
|
},
|
|
202
392
|
};
|
|
203
393
|
}
|
|
204
394
|
export async function renameSystemShelfKey(em) {
|
|
205
395
|
await makeTable(em, '_embeddings').renameColumn('type', 'shelf_key');
|
|
206
396
|
}
|
|
397
|
+
const LEGACY_ORCHESTRATOR_FIELDS = ['agent_id', 'access_password', 'trigger_prefix', 'reply_window'];
|
|
398
|
+
export const SYSTEM_MIGRATIONS = [
|
|
399
|
+
{
|
|
400
|
+
name: 'embeddings-shelf-key',
|
|
401
|
+
up: renameSystemShelfKey,
|
|
402
|
+
},
|
|
403
|
+
{
|
|
404
|
+
name: 'core-settings-to-system',
|
|
405
|
+
up: async (em) => {
|
|
406
|
+
const conn = em.getConnection();
|
|
407
|
+
const src = await introspectTable(em, '_settings__core');
|
|
408
|
+
const dst = await introspectTable(em, '_settings__system');
|
|
409
|
+
if (!src.exists || !dst.exists)
|
|
410
|
+
return;
|
|
411
|
+
const already = (await conn.execute(`SELECT 1 FROM "_settings__system" WHERE "plugin_id" = 'system' LIMIT 1`));
|
|
412
|
+
if (already.length > 0)
|
|
413
|
+
return;
|
|
414
|
+
const rows = (await conn.execute(`SELECT * FROM "_settings__core" WHERE "plugin_id" = 'core' LIMIT 1`));
|
|
415
|
+
const row = rows[0];
|
|
416
|
+
if (!row)
|
|
417
|
+
return;
|
|
418
|
+
const carried = ['publicUrl', 'language'].filter((c) => src.columns.has(c) && dst.columns.has(c));
|
|
419
|
+
if (carried.length === 0)
|
|
420
|
+
return;
|
|
421
|
+
const cols = ['plugin_id', ...carried];
|
|
422
|
+
const values = ['system', ...carried.map((c) => row[c] ?? null)];
|
|
423
|
+
await conn.execute(`INSERT INTO "_settings__system" (${cols.map((c) => `"${c}"`).join(', ')}) ` +
|
|
424
|
+
`VALUES (${cols.map(() => '?').join(', ')})`, values);
|
|
425
|
+
log.info(`[system-migration] settings moved from _settings__core to _settings__system`);
|
|
426
|
+
},
|
|
427
|
+
},
|
|
428
|
+
{
|
|
429
|
+
name: 'core-state-to-system',
|
|
430
|
+
up: async (em) => {
|
|
431
|
+
const conn = em.getConnection();
|
|
432
|
+
if (!(await introspectTable(em, '_plugin_state')).exists)
|
|
433
|
+
return;
|
|
434
|
+
await conn.execute(`UPDATE "_plugin_state" SET "plugin" = 'system' ` +
|
|
435
|
+
`WHERE "plugin" = 'core' ` +
|
|
436
|
+
`AND "key" NOT IN (SELECT "key" FROM "_plugin_state" WHERE "plugin" = 'system')`);
|
|
437
|
+
await conn.execute(`DELETE FROM "_plugin_state" WHERE "plugin" = 'core'`);
|
|
438
|
+
},
|
|
439
|
+
},
|
|
440
|
+
{
|
|
441
|
+
name: 'drop-legacy-core-settings',
|
|
442
|
+
up: async (em) => {
|
|
443
|
+
const conn = em.getConnection();
|
|
444
|
+
if (!(await introspectTable(em, '_settings__core')).exists)
|
|
445
|
+
return;
|
|
446
|
+
if (!(await introspectTable(em, '_settings__system')).exists)
|
|
447
|
+
return;
|
|
448
|
+
const landed = (await conn.execute(`SELECT 1 FROM "_settings__system" WHERE "plugin_id" = 'system' LIMIT 1`));
|
|
449
|
+
if (landed.length === 0)
|
|
450
|
+
return;
|
|
451
|
+
await conn.execute(`DROP TABLE IF EXISTS "_settings__core"`);
|
|
452
|
+
log.info('[system-migration] dropped the legacy _settings__core table');
|
|
453
|
+
},
|
|
454
|
+
},
|
|
455
|
+
{
|
|
456
|
+
name: 'orchestrator-settings-to-system',
|
|
457
|
+
up: async (em) => {
|
|
458
|
+
const DEFAULTS = Object.fromEntries(fieldEntries(SYSTEM_SETTINGS.fields)
|
|
459
|
+
.filter(([k]) => LEGACY_ORCHESTRATOR_FIELDS.includes(k))
|
|
460
|
+
.map(([k, f]) => [k, f.default]));
|
|
461
|
+
const conn = em.getConnection();
|
|
462
|
+
const src = await introspectTable(em, '_settings__orchestrator');
|
|
463
|
+
const dst = await introspectTable(em, '_settings__system');
|
|
464
|
+
if (!src.exists || !dst.exists)
|
|
465
|
+
return;
|
|
466
|
+
const carried = LEGACY_ORCHESTRATOR_FIELDS.filter((c) => src.columns.has(c) && dst.columns.has(c));
|
|
467
|
+
if (carried.length === 0)
|
|
468
|
+
return;
|
|
469
|
+
const rows = (await conn.execute(`SELECT * FROM "_settings__orchestrator" WHERE "plugin_id" = 'orchestrator' LIMIT 1`));
|
|
470
|
+
const row = rows[0];
|
|
471
|
+
if (!row)
|
|
472
|
+
return;
|
|
473
|
+
let wrote = false;
|
|
474
|
+
for (const c of carried) {
|
|
475
|
+
const value = row[c];
|
|
476
|
+
if (value === undefined || value === null)
|
|
477
|
+
continue;
|
|
478
|
+
const def = DEFAULTS[c];
|
|
479
|
+
const whereExtra = def === undefined ? `${q(c)} IS NULL` : `(${q(c)} IS NULL OR ${q(c)} = ?)`;
|
|
480
|
+
const params = def === undefined ? [value] : [value, def];
|
|
481
|
+
await conn.execute(`UPDATE "_settings__system" SET ${q(c)} = ? WHERE "plugin_id" = 'system' AND ${whereExtra}`, params);
|
|
482
|
+
wrote = true;
|
|
483
|
+
}
|
|
484
|
+
if (wrote)
|
|
485
|
+
log.info('[system-migration] orchestrator settings carried into _settings__system');
|
|
486
|
+
},
|
|
487
|
+
},
|
|
488
|
+
{
|
|
489
|
+
name: 'drop-legacy-orchestrator-settings',
|
|
490
|
+
up: async (em) => {
|
|
491
|
+
const conn = em.getConnection();
|
|
492
|
+
const src = await introspectTable(em, '_settings__orchestrator');
|
|
493
|
+
const dst = await introspectTable(em, '_settings__system');
|
|
494
|
+
if (!src.exists || !dst.exists)
|
|
495
|
+
return;
|
|
496
|
+
const fields = LEGACY_ORCHESTRATOR_FIELDS;
|
|
497
|
+
const srcRows = (await conn.execute(`SELECT * FROM "_settings__orchestrator" WHERE "plugin_id" = 'orchestrator' LIMIT 1`));
|
|
498
|
+
const srcRow = srcRows[0];
|
|
499
|
+
const needsLanding = fields.filter((c) => src.columns.has(c) && srcRow?.[c] !== undefined && srcRow?.[c] !== null);
|
|
500
|
+
if (needsLanding.length > 0) {
|
|
501
|
+
const dstRows = (await conn.execute(`SELECT * FROM "_settings__system" WHERE "plugin_id" = 'system' LIMIT 1`));
|
|
502
|
+
const dstRow = dstRows[0];
|
|
503
|
+
const landed = needsLanding.every((c) => dst.columns.has(c) && dstRow?.[c] !== undefined && dstRow?.[c] !== null);
|
|
504
|
+
if (!landed)
|
|
505
|
+
return;
|
|
506
|
+
}
|
|
507
|
+
await conn.execute(`DROP TABLE IF EXISTS "_settings__orchestrator"`);
|
|
508
|
+
log.info('[system-migration] dropped the legacy _settings__orchestrator table');
|
|
509
|
+
},
|
|
510
|
+
},
|
|
511
|
+
];
|
|
512
|
+
export async function runSystemMigrations(em) {
|
|
513
|
+
for (const m of SYSTEM_MIGRATIONS) {
|
|
514
|
+
await m.up(em);
|
|
515
|
+
}
|
|
516
|
+
}
|
|
207
517
|
export function validateMigrations(pluginId, list) {
|
|
208
518
|
const versions = list.map((m) => m.version).sort((a, b) => a - b);
|
|
209
519
|
versions.forEach((v, i) => {
|
|
@@ -212,13 +522,46 @@ export function validateMigrations(pluginId, list) {
|
|
|
212
522
|
}
|
|
213
523
|
});
|
|
214
524
|
}
|
|
525
|
+
async function persistOps(em, plugin, version, ops) {
|
|
526
|
+
if (!ops.length)
|
|
527
|
+
return;
|
|
528
|
+
if (!(await introspectTable(em, '_migration_ops')).exists) {
|
|
529
|
+
log.debug(`${plugin} v${version}: _migration_ops is absent — the accounting is logged but not persisted`);
|
|
530
|
+
return;
|
|
531
|
+
}
|
|
532
|
+
const conn = em.getConnection();
|
|
533
|
+
const at = new Date().toISOString();
|
|
534
|
+
for (const [seq, o] of ops.entries()) {
|
|
535
|
+
await conn.execute(`INSERT INTO _migration_ops (plugin_id, version, seq, op, table_name, performed, rows, scanned, code, reason, empty_targets, stranded_sources, applied_at)
|
|
536
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
537
|
+
ON CONFLICT(plugin_id, version, seq) DO UPDATE SET
|
|
538
|
+
op = excluded.op, table_name = excluded.table_name, performed = excluded.performed,
|
|
539
|
+
rows = excluded.rows, scanned = excluded.scanned, code = excluded.code,
|
|
540
|
+
reason = excluded.reason, empty_targets = excluded.empty_targets,
|
|
541
|
+
stranded_sources = excluded.stranded_sources, applied_at = excluded.applied_at`, [
|
|
542
|
+
plugin,
|
|
543
|
+
version,
|
|
544
|
+
seq,
|
|
545
|
+
o.op,
|
|
546
|
+
o.table,
|
|
547
|
+
o.performed ? 1 : 0,
|
|
548
|
+
o.rows ?? null,
|
|
549
|
+
o.scanned ?? null,
|
|
550
|
+
o.code ?? null,
|
|
551
|
+
o.reason ?? null,
|
|
552
|
+
o.emptyTargets?.length ? o.emptyTargets.join(', ') : null,
|
|
553
|
+
o.strandedSources?.length ? o.strandedSources.join(', ') : null,
|
|
554
|
+
at,
|
|
555
|
+
]);
|
|
556
|
+
}
|
|
557
|
+
}
|
|
215
558
|
async function anyTableExists(em, tables) {
|
|
216
559
|
for (const t of tables)
|
|
217
560
|
if ((await introspectTable(em, t)).exists)
|
|
218
561
|
return true;
|
|
219
562
|
return false;
|
|
220
563
|
}
|
|
221
|
-
export async function runMigrations({ em, plugins, hooks }) {
|
|
564
|
+
export async function runMigrations({ em, plugins, hooks, onApplied }) {
|
|
222
565
|
const conn = em.getConnection();
|
|
223
566
|
for (const p of plugins) {
|
|
224
567
|
const list = hooks[p.id]?.migrations ?? [];
|
|
@@ -237,9 +580,15 @@ export async function runMigrations({ em, plugins, hooks }) {
|
|
|
237
580
|
continue;
|
|
238
581
|
}
|
|
239
582
|
const pending = [...list].filter((m) => m.version > stored).sort((a, b) => a.version - b.version);
|
|
240
|
-
const ctx = { table: (name) => makeTable(em, name) };
|
|
241
583
|
for (const m of pending) {
|
|
584
|
+
const ops = [];
|
|
585
|
+
const ctx = { table: (name) => makeTable(em, name, (o) => ops.push(o)) };
|
|
242
586
|
await m.up(ctx);
|
|
587
|
+
const concern = describeMigrationConcerns(p.id, m.version, ops);
|
|
588
|
+
if (concern)
|
|
589
|
+
log.warn(concern);
|
|
590
|
+
await persistOps(em, p.id, m.version, ops);
|
|
591
|
+
onApplied?.({ plugin: p.id, version: m.version, ops });
|
|
243
592
|
await conn.execute(`INSERT INTO _migrations (plugin_id, version) VALUES (?, ?)
|
|
244
593
|
ON CONFLICT(plugin_id) DO UPDATE SET version = excluded.version`, [p.id, m.version]);
|
|
245
594
|
}
|