@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.
@@ -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;
@@ -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 fieldDef = fm[k];
95
- if (fieldDef && !fieldDef.virtual)
96
- where[k] = coerceFilter(String(v), fieldDef.column);
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
- await restoreRecord(m, ename, id, { actor }, (tx, recordId) => readExtends(tx, library, shelf, recordId));
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;
@@ -0,0 +1,9 @@
1
+ import { SchemaComparator } from '@mikro-orm/sqlite';
2
+ let applied = false;
3
+ export function disableColumnRenameDetection() {
4
+ if (applied)
5
+ return;
6
+ SchemaComparator.prototype['detectColumnRenamings'] = function noop() {
7
+ };
8
+ applied = true;
9
+ }
@@ -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.0",
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.3.0",
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",