@coffer-org/server 3.3.0 → 3.4.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/collection-io.js +13 -10
- package/dist/db.js +2 -0
- package/dist/entity-schema.d.ts +1 -0
- package/dist/entity-schema.js +20 -0
- 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 +32 -9
- package/dist/http-errors.d.ts +7 -0
- package/dist/http-errors.js +24 -0
- package/dist/index.js +11 -9
- package/dist/mcp-local.js +3 -1
- package/dist/migrations.d.ts +25 -2
- package/dist/migrations.js +246 -19
- package/dist/mutate.js +21 -33
- package/dist/part-errors.d.ts +3 -0
- package/dist/part-errors.js +6 -0
- package/dist/part-injection.d.ts +12 -3
- package/dist/part-injection.js +379 -56
- package/dist/part-ref.d.ts +13 -0
- package/dist/part-ref.js +109 -0
- package/dist/plugin-runtime.js +1 -0
- package/dist/recompute-derived.js +24 -4
- package/dist/records-api.d.ts +5 -1
- package/dist/records-api.js +47 -13
- package/dist/rename-guard.d.ts +1 -0
- package/dist/rename-guard.js +9 -0
- package/dist/schema-sync.js +38 -1
- package/package.json +2 -2
package/dist/records-api.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ShelfDef } from '@coffer-org/sdk/shelf';
|
|
2
|
+
import { type PartMemo } from './part-injection.ts';
|
|
2
3
|
export declare class UnknownShelfError extends Error {
|
|
3
4
|
}
|
|
4
5
|
export declare class SingleShelfError extends Error {
|
|
@@ -20,12 +21,15 @@ export type RecordListOpts = {
|
|
|
20
21
|
};
|
|
21
22
|
export declare const MAX_PAGE = 500;
|
|
22
23
|
export declare function coerceFilter(v: string, column: string): unknown;
|
|
23
|
-
export declare function withExtends(record: Record<string, unknown>, library: string, shelf: string): Promise<Record<string, unknown>>;
|
|
24
|
+
export declare function withExtends(record: Record<string, unknown>, library: string, shelf: string, memo?: PartMemo): Promise<Record<string, unknown>>;
|
|
24
25
|
export declare function withExtendsMany(rows: Record<string, unknown>[], library: string, shelf: string): Promise<Record<string, unknown>[]>;
|
|
25
26
|
export declare function rowMatch(mdef: ShelfDef, row: Record<string, unknown>, tokens: string[]): {
|
|
26
27
|
score: number;
|
|
27
28
|
snippet: string;
|
|
28
29
|
} | null;
|
|
30
|
+
export declare class FilterError extends Error {
|
|
31
|
+
constructor(message: string);
|
|
32
|
+
}
|
|
29
33
|
export declare function recordCount(library: string, shelf: string, query?: RecordListQuery, opts?: Pick<RecordListOpts, 'deleted' | 'where'>): Promise<number>;
|
|
30
34
|
export declare function recordList(library: string, shelf: string, query?: RecordListQuery, opts?: RecordListOpts): Promise<Record<string, unknown>[]>;
|
|
31
35
|
export declare function isPagedRequest(limit?: string, offset?: string): boolean;
|
package/dist/records-api.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { fieldMap, textSearchKeys, titleKey, recordTitle, listKeys, storageColumnsFor } from '@coffer-org/sdk/shelf';
|
|
1
|
+
import { fieldMap, textSearchKeys, titleKey, recordTitle, listKeys, storageColumnsFor, magnitudeSub, } from '@coffer-org/sdk/shelf';
|
|
2
2
|
import { tokenize, matchScoreFolded, foldText } from '@coffer-org/core/search';
|
|
3
3
|
import { getActiveRegistry, getShelf, getExtendsFor } from "./registry-context.js";
|
|
4
4
|
import { selectRows } from "./read-rows.js";
|
|
@@ -8,6 +8,7 @@ import { getEm } from "./db.js";
|
|
|
8
8
|
import { decodeTemporal, dtStringToDate } from "./temporal.js";
|
|
9
9
|
import { splitBody, saveExtends, deleteExtends, validateExtends, readExtends } from "./extend-io.js";
|
|
10
10
|
import { createRecord, updateRecord, getRecord, deleteRecord, restoreRecord, purgeRecord } from "./mutate.js";
|
|
11
|
+
import { newPartMemo } from "./part-injection.js";
|
|
11
12
|
import { deleteRecordActivity } from "./record-activity.js";
|
|
12
13
|
export class UnknownShelfError extends Error {
|
|
13
14
|
}
|
|
@@ -42,13 +43,13 @@ export function coerceFilter(v, column) {
|
|
|
42
43
|
return dtStringToDate(v);
|
|
43
44
|
return v;
|
|
44
45
|
}
|
|
45
|
-
export async function withExtends(record, library, shelf) {
|
|
46
|
+
export async function withExtends(record, library, shelf, memo) {
|
|
46
47
|
const matchedExtends = getExtendsFor(library, shelf);
|
|
47
48
|
if (!matchedExtends.length)
|
|
48
49
|
return record;
|
|
49
50
|
const _extends = {};
|
|
50
51
|
await Promise.all(matchedExtends.map(async (e) => {
|
|
51
|
-
_extends[e.id] = (await getExtendRecord(e, record.id)) ?? null;
|
|
52
|
+
_extends[e.id] = (await getExtendRecord(e, record.id, undefined, memo)) ?? null;
|
|
52
53
|
}));
|
|
53
54
|
return { ...record, _extends };
|
|
54
55
|
}
|
|
@@ -85,15 +86,45 @@ export function rowMatch(mdef, row, tokens) {
|
|
|
85
86
|
const snippet = raw.length > 120 ? raw.slice(0, 120) + '…' : raw;
|
|
86
87
|
return { score, snippet };
|
|
87
88
|
}
|
|
89
|
+
export class FilterError extends Error {
|
|
90
|
+
constructor(message) {
|
|
91
|
+
super(message);
|
|
92
|
+
this.name = 'FilterError';
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
function resolveFilterKey(m, fm, k) {
|
|
96
|
+
const direct = fm[k];
|
|
97
|
+
if (direct) {
|
|
98
|
+
if (direct.virtual)
|
|
99
|
+
return undefined;
|
|
100
|
+
if (!direct.columns)
|
|
101
|
+
return { column: k, type: direct.column };
|
|
102
|
+
const sub = magnitudeSub(direct);
|
|
103
|
+
if (sub === undefined)
|
|
104
|
+
throw new FilterError(`${m.library}/${m.shelf}: '${k}' is a composite whose parts are all computed or pinned — it owns no column to filter on`);
|
|
105
|
+
return { column: `${k}__${sub}`, type: direct.columns[sub] };
|
|
106
|
+
}
|
|
107
|
+
const cut = k.lastIndexOf('__');
|
|
108
|
+
if (cut <= 0)
|
|
109
|
+
return undefined;
|
|
110
|
+
const base = fm[k.slice(0, cut)];
|
|
111
|
+
if (!base?.columns)
|
|
112
|
+
return undefined;
|
|
113
|
+
const part = k.slice(cut + 2);
|
|
114
|
+
const type = base.columns[part];
|
|
115
|
+
if (type === undefined)
|
|
116
|
+
throw new FilterError(`${m.library}/${m.shelf}: '${k}' names no part of composite '${k.slice(0, cut)}' (parts: ${Object.keys(base.columns).join(', ')})`);
|
|
117
|
+
return { column: k, type };
|
|
118
|
+
}
|
|
88
119
|
function buildWhere(m, filterParams) {
|
|
89
120
|
const where = {};
|
|
90
121
|
const fm = fieldMap(m.fields);
|
|
91
122
|
for (const [k, v] of Object.entries(filterParams)) {
|
|
92
123
|
if (k === 'id' || v === undefined)
|
|
93
124
|
continue;
|
|
94
|
-
const
|
|
95
|
-
if (
|
|
96
|
-
where[
|
|
125
|
+
const target = resolveFilterKey(m, fm, k);
|
|
126
|
+
if (target)
|
|
127
|
+
where[target.column] = coerceFilter(String(v), target.type);
|
|
97
128
|
}
|
|
98
129
|
if (filterParams['id']) {
|
|
99
130
|
const ids = String(filterParams['id'])
|
|
@@ -197,11 +228,12 @@ async function createOne(library, shelf, body, actor = 'gui') {
|
|
|
197
228
|
const { m, ename } = resolve(library, shelf);
|
|
198
229
|
const { base, extData } = splitBody(body);
|
|
199
230
|
validateExtends(library, shelf, extData);
|
|
231
|
+
const memo = newPartMemo();
|
|
200
232
|
const row = await createRecord(m, ename, base, { actor }, async (tx, id) => {
|
|
201
|
-
await saveExtends(tx, library, shelf, id, extData);
|
|
202
|
-
return readExtends(tx, library, shelf, id);
|
|
233
|
+
await saveExtends(tx, library, shelf, id, extData, memo);
|
|
234
|
+
return readExtends(tx, library, shelf, id, memo);
|
|
203
235
|
});
|
|
204
|
-
return withExtends(row, library, shelf);
|
|
236
|
+
return withExtends(row, library, shelf, memo);
|
|
205
237
|
}
|
|
206
238
|
export async function recordCreate(library, shelf, body, actor = 'gui') {
|
|
207
239
|
const { m } = resolve(library, shelf);
|
|
@@ -238,11 +270,12 @@ export async function recordUpdate(library, shelf, id, body, actor = 'gui') {
|
|
|
238
270
|
const { m, ename } = resolve(library, shelf);
|
|
239
271
|
const { base, extData } = splitBody(body);
|
|
240
272
|
validateExtends(library, shelf, extData);
|
|
273
|
+
const memo = newPartMemo();
|
|
241
274
|
const row = await updateRecord(m, ename, id, base, { actor }, async (tx) => {
|
|
242
|
-
await saveExtends(tx, library, shelf, id, extData);
|
|
243
|
-
return readExtends(tx, library, shelf, id);
|
|
275
|
+
await saveExtends(tx, library, shelf, id, extData, memo);
|
|
276
|
+
return readExtends(tx, library, shelf, id, memo);
|
|
244
277
|
});
|
|
245
|
-
return withExtends(row, library, shelf);
|
|
278
|
+
return withExtends(row, library, shelf, memo);
|
|
246
279
|
}
|
|
247
280
|
export async function recordDelete(library, shelf, id, actor = 'gui') {
|
|
248
281
|
const { m, ename } = resolve(library, shelf);
|
|
@@ -252,7 +285,8 @@ export async function recordDelete(library, shelf, id, actor = 'gui') {
|
|
|
252
285
|
}
|
|
253
286
|
export async function recordRestore(library, shelf, id, actor = 'gui') {
|
|
254
287
|
const { m, ename } = resolve(library, shelf);
|
|
255
|
-
|
|
288
|
+
const memo = newPartMemo();
|
|
289
|
+
await restoreRecord(m, ename, id, { actor }, (tx, recordId) => readExtends(tx, library, shelf, recordId, memo));
|
|
256
290
|
}
|
|
257
291
|
export async function recordPurge(library, shelf, id, actor = 'gui') {
|
|
258
292
|
const { m, ename } = resolve(library, shelf);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function disableColumnRenameDetection(): void;
|
package/dist/schema-sync.js
CHANGED
|
@@ -1,4 +1,41 @@
|
|
|
1
|
-
import { getOrm } from "./db.js";
|
|
1
|
+
import { getOrm, getEm } from "./db.js";
|
|
2
|
+
import { introspectAllColumns, isColumnHandedOver } from "./migrations.js";
|
|
3
|
+
import { getLogger } from '@coffer-org/sdk/logger';
|
|
4
|
+
const log = getLogger('schema-sync');
|
|
5
|
+
const q = (id) => `"${id.replace(/"/g, '""')}"`;
|
|
6
|
+
async function warnAboutColumnsToBeDropped() {
|
|
7
|
+
try {
|
|
8
|
+
const orm = getOrm();
|
|
9
|
+
const em = getEm().fork();
|
|
10
|
+
const metas = [...orm.getMetadata().getAll().values()].filter((m) => !m.virtual && !m.pivotTable && m.tableName);
|
|
11
|
+
const live = await introspectAllColumns(em, [...new Set(metas.map((m) => m.tableName))]);
|
|
12
|
+
for (const meta of metas) {
|
|
13
|
+
const tableName = meta.tableName;
|
|
14
|
+
const columns = live.get(tableName);
|
|
15
|
+
if (!columns)
|
|
16
|
+
continue;
|
|
17
|
+
const known = new Set();
|
|
18
|
+
for (const prop of meta.props)
|
|
19
|
+
for (const f of prop.fieldNames ?? [])
|
|
20
|
+
known.add(f);
|
|
21
|
+
const droppable = [...columns].filter((c) => !known.has(c) && !isColumnHandedOver(tableName, c));
|
|
22
|
+
if (!droppable.length)
|
|
23
|
+
continue;
|
|
24
|
+
const conn = em.getConnection();
|
|
25
|
+
for (const col of droppable) {
|
|
26
|
+
const rows = (await conn.execute(`SELECT COUNT(*) AS n FROM ${q(tableName)} WHERE ${q(col)} IS NOT NULL`));
|
|
27
|
+
const n = rows[0]?.n ?? 0;
|
|
28
|
+
if (n > 0) {
|
|
29
|
+
log.warn(`schema sync is about to DROP ${tableName}.${col} — it still holds ${n} row(s) of data. Add a migration (TableOps.convert/renameColumn/changeType) before the next start, or the data is lost.`);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
catch (e) {
|
|
35
|
+
log.error('drop-warning check failed (diagnostic only, sync continues)', e);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
2
38
|
export async function syncSchema() {
|
|
39
|
+
await warnAboutColumnsToBeDropped();
|
|
3
40
|
await getOrm().schema.update({ safe: false, dropTables: false });
|
|
4
41
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@coffer-org/server",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.4.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"engines": {
|
|
6
6
|
"node": ">=24"
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
},
|
|
26
26
|
"dependencies": {
|
|
27
27
|
"@coffer-org/core": "^3.3.0",
|
|
28
|
-
"@coffer-org/sdk": "^3.
|
|
28
|
+
"@coffer-org/sdk": "^3.4.0",
|
|
29
29
|
"@extractus/oembed-extractor": "^4.1.0",
|
|
30
30
|
"@fastify/cors": "^11.2.0",
|
|
31
31
|
"@fastify/multipart": "^10.0.0",
|