@coffer-org/server 2.3.1 → 2.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/embed-openai.d.ts +14 -0
- package/dist/embed-openai.js +65 -6
- package/dist/entity-schema.js +1 -0
- package/dist/index.js +38 -1
- package/dist/mcp-local.d.ts +3 -0
- package/dist/mcp-local.js +27 -1
- package/dist/mcp-tools.js +6 -3
- package/dist/mutate.d.ts +9 -3
- package/dist/mutate.js +46 -7
- package/dist/records-api.d.ts +14 -2
- package/dist/records-api.js +37 -10
- package/dist/search-indexer.js +1 -1
- package/package.json +1 -1
package/dist/embed-openai.d.ts
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
export declare const EMBED_MODEL = "text-embedding-3-small";
|
|
2
2
|
export declare const EMBED_DIM = 1024;
|
|
3
|
+
export type EmbeddingFailureReason = 'billing' | 'auth' | 'rate_limit' | 'server' | 'network' | 'unknown';
|
|
4
|
+
export declare class OpenAIEmbeddingError extends Error {
|
|
5
|
+
readonly status: number | null;
|
|
6
|
+
readonly code: string | null;
|
|
7
|
+
readonly reason: EmbeddingFailureReason;
|
|
8
|
+
constructor(args: {
|
|
9
|
+
status?: number | null;
|
|
10
|
+
code?: string | null;
|
|
11
|
+
detail?: string;
|
|
12
|
+
reason?: EmbeddingFailureReason;
|
|
13
|
+
cause?: unknown;
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
export declare function embeddingFailureMessage(error: unknown): string;
|
|
3
17
|
export declare function embedBatch(texts: string[], apiKey: string, fetchImpl?: typeof fetch): Promise<{
|
|
4
18
|
vectors: number[][];
|
|
5
19
|
tokens: number;
|
package/dist/embed-openai.js
CHANGED
|
@@ -1,19 +1,78 @@
|
|
|
1
1
|
const OPENAI_URL = 'https://api.openai.com/v1/embeddings';
|
|
2
2
|
export const EMBED_MODEL = 'text-embedding-3-small';
|
|
3
3
|
export const EMBED_DIM = 1024;
|
|
4
|
+
export class OpenAIEmbeddingError extends Error {
|
|
5
|
+
status;
|
|
6
|
+
code;
|
|
7
|
+
reason;
|
|
8
|
+
constructor(args) {
|
|
9
|
+
const status = args.status ?? null;
|
|
10
|
+
const suffix = args.detail ? `: ${args.detail}` : '';
|
|
11
|
+
super(`openai embeddings${status == null ? '' : ` ${status}`}${suffix}`, { cause: args.cause });
|
|
12
|
+
this.name = 'OpenAIEmbeddingError';
|
|
13
|
+
this.status = status;
|
|
14
|
+
this.code = args.code ?? null;
|
|
15
|
+
this.reason = args.reason ?? classifyEmbeddingFailure(status, args.code, args.detail);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
function parseErrorBody(body) {
|
|
19
|
+
try {
|
|
20
|
+
const value = JSON.parse(body);
|
|
21
|
+
const code = typeof value.error?.code === 'string' ? value.error.code : null;
|
|
22
|
+
const message = typeof value.error?.message === 'string' ? value.error.message : '';
|
|
23
|
+
if (code || message)
|
|
24
|
+
return { code, detail: message || code || 'request failed' };
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
}
|
|
28
|
+
return { code: null, detail: body.slice(0, 200) || 'request failed' };
|
|
29
|
+
}
|
|
30
|
+
function classifyEmbeddingFailure(status, code, detail) {
|
|
31
|
+
const marker = `${code ?? ''} ${detail ?? ''}`.toLowerCase();
|
|
32
|
+
if (status === 402 || /insufficient_quota|billing|payment_required|payment required|hard.?limit|quota exceeded/.test(marker))
|
|
33
|
+
return 'billing';
|
|
34
|
+
if (status === 401 || status === 403)
|
|
35
|
+
return 'auth';
|
|
36
|
+
if (status === 429 || /rate.?limit/.test(marker))
|
|
37
|
+
return 'rate_limit';
|
|
38
|
+
if (status != null && status >= 500)
|
|
39
|
+
return 'server';
|
|
40
|
+
return 'unknown';
|
|
41
|
+
}
|
|
42
|
+
export function embeddingFailureMessage(error) {
|
|
43
|
+
if (!(error instanceof OpenAIEmbeddingError))
|
|
44
|
+
return error instanceof Error ? error.message : String(error);
|
|
45
|
+
switch (error.reason) {
|
|
46
|
+
case 'billing':
|
|
47
|
+
return 'OpenAI billing or quota is unavailable. RAG indexing/search is paused; records are still saved and indexing will retry after billing is restored.';
|
|
48
|
+
case 'auth':
|
|
49
|
+
return 'The OpenAI API key was rejected. RAG indexing/search is unavailable; records are still saved.';
|
|
50
|
+
case 'rate_limit':
|
|
51
|
+
return 'OpenAI rate limit reached. RAG indexing/search will retry; records are still saved.';
|
|
52
|
+
default:
|
|
53
|
+
return `OpenAI embeddings are unavailable${error.status == null ? '' : ` (HTTP ${error.status})`}. RAG will retry; records are still saved.`;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
4
56
|
export async function embedBatch(texts, apiKey, fetchImpl = fetch) {
|
|
5
57
|
if (texts.length === 0)
|
|
6
58
|
return { vectors: [], tokens: 0 };
|
|
7
59
|
if (!apiKey)
|
|
8
60
|
throw new Error('embeddings: missing API key (OPENAI_API_KEY)');
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
61
|
+
let res;
|
|
62
|
+
try {
|
|
63
|
+
res = await fetchImpl(OPENAI_URL, {
|
|
64
|
+
method: 'POST',
|
|
65
|
+
headers: { 'content-type': 'application/json', authorization: `Bearer ${apiKey}` },
|
|
66
|
+
body: JSON.stringify({ model: EMBED_MODEL, input: texts, dimensions: EMBED_DIM }),
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
catch (cause) {
|
|
70
|
+
throw new OpenAIEmbeddingError({ reason: 'network', detail: 'network request failed', cause });
|
|
71
|
+
}
|
|
14
72
|
if (!res.ok) {
|
|
15
73
|
const body = await res.text().catch(() => '');
|
|
16
|
-
|
|
74
|
+
const parsed = parseErrorBody(body);
|
|
75
|
+
throw new OpenAIEmbeddingError({ status: res.status, code: parsed.code, detail: parsed.detail });
|
|
17
76
|
}
|
|
18
77
|
const json = (await res.json());
|
|
19
78
|
const vectors = [...json.data].sort((a, b) => a.index - b.index).map((d) => d.embedding);
|
package/dist/entity-schema.js
CHANGED
|
@@ -42,6 +42,7 @@ export function buildEntitySchema(m) {
|
|
|
42
42
|
id: { type: 'integer', primary: true, autoincrement: true },
|
|
43
43
|
created_at: { type: 'text' },
|
|
44
44
|
updated_at: { type: 'text' },
|
|
45
|
+
deleted_at: { type: 'text', nullable: true },
|
|
45
46
|
};
|
|
46
47
|
addFieldProps(properties, m.fields, (f) => !f.required);
|
|
47
48
|
const name = shelfTableName(m.library, m.shelf);
|
package/dist/index.js
CHANGED
|
@@ -24,7 +24,7 @@ import { baseUrl } from "./public-url.js";
|
|
|
24
24
|
import { discoverPluginAssets, discoverRuntime } from "./plugin-discovery.js";
|
|
25
25
|
import { checkLatestVersion, resolveUpdateTarget, resolveAllUpdateTargets, resolveBaseTargets, resolveRuntimeTarget, runNpmInstall, } from "./plugin-updates.js";
|
|
26
26
|
import { buildClientSchema } from "./schema-api.js";
|
|
27
|
-
import { recordList, recordListPage, isPagedRequest, recordGet, recordCreate, recordUpdate, recordDelete, UnknownShelfError, } from "./records-api.js";
|
|
27
|
+
import { recordList, recordListPage, isPagedRequest, recordGet, recordCreate, recordUpdate, recordDelete, recordRestore, recordPurge, recordTrashList, UnknownShelfError, } from "./records-api.js";
|
|
28
28
|
import { maskTree, preserveTree } from "./field-masking.js";
|
|
29
29
|
import { writePluginSettings } from "./settings-write.js";
|
|
30
30
|
import { rootLogger, getLogger } from "./log.js";
|
|
@@ -363,6 +363,43 @@ function maskRecordRow(library, shelf, row) {
|
|
|
363
363
|
}
|
|
364
364
|
return out;
|
|
365
365
|
}
|
|
366
|
+
app.get('/api/trash', async (_req, reply) => {
|
|
367
|
+
const rows = await recordTrashList();
|
|
368
|
+
return rows.map((entry) => ({ ...entry, record: maskRecordRow(entry.library, entry.shelf, entry.record) }));
|
|
369
|
+
});
|
|
370
|
+
app.post('/api/trash/:library/:shelf/:id/restore', async (req, reply) => {
|
|
371
|
+
const { library, shelf, id } = req.params;
|
|
372
|
+
const rid = Number(id);
|
|
373
|
+
if (isNaN(rid))
|
|
374
|
+
return reply.code(400).send({ error: 'invalid_id' });
|
|
375
|
+
try {
|
|
376
|
+
await recordRestore(library, shelf, rid);
|
|
377
|
+
return { ok: true };
|
|
378
|
+
}
|
|
379
|
+
catch (e) {
|
|
380
|
+
if (e instanceof UnknownShelfError || e instanceof NotFoundError)
|
|
381
|
+
return reply.code(404).send({ error: 'not_found' });
|
|
382
|
+
throw e;
|
|
383
|
+
}
|
|
384
|
+
});
|
|
385
|
+
app.delete('/api/trash/:library/:shelf/:id', async (req, reply) => {
|
|
386
|
+
const { library, shelf, id } = req.params;
|
|
387
|
+
const rid = Number(id);
|
|
388
|
+
if (isNaN(rid))
|
|
389
|
+
return reply.code(400).send({ error: 'invalid_id' });
|
|
390
|
+
if (req.body?.confirmed !== true) {
|
|
391
|
+
return reply.code(400).send({ error: 'explicit_confirmation_required' });
|
|
392
|
+
}
|
|
393
|
+
try {
|
|
394
|
+
await recordPurge(library, shelf, rid);
|
|
395
|
+
return reply.code(204).send();
|
|
396
|
+
}
|
|
397
|
+
catch (e) {
|
|
398
|
+
if (e instanceof UnknownShelfError || e instanceof NotFoundError)
|
|
399
|
+
return reply.code(404).send({ error: 'not_found' });
|
|
400
|
+
throw e;
|
|
401
|
+
}
|
|
402
|
+
});
|
|
366
403
|
app.get('/api/:library/:shelf', async (req, reply) => {
|
|
367
404
|
const { library, shelf } = req.params;
|
|
368
405
|
const { _fields, _extends, limit, offset, ...query } = req.query;
|
package/dist/mcp-local.d.ts
CHANGED
|
@@ -14,4 +14,7 @@ export declare class LocalClient implements CofferClientApi {
|
|
|
14
14
|
createRecord(library: string, shelf: string, fields: Record<string, unknown>): Promise<unknown>;
|
|
15
15
|
updateRecord(library: string, shelf: string, id: number, fields: Record<string, unknown>): Promise<unknown>;
|
|
16
16
|
deleteRecord(library: string, shelf: string, id: number): Promise<unknown>;
|
|
17
|
+
listTrash(): Promise<unknown>;
|
|
18
|
+
restoreRecord(library: string, shelf: string, id: number): Promise<unknown>;
|
|
19
|
+
purgeRecord(library: string, shelf: string, id: number): Promise<unknown>;
|
|
17
20
|
}
|
package/dist/mcp-local.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { ValidationError, NotFoundError } from '@coffer-org/mcp/client';
|
|
2
|
-
import { recordList, recordListPage, recordGet, recordCreate, recordUpdate, recordDelete, UnknownShelfError, } from "./records-api.js";
|
|
2
|
+
import { recordList, recordListPage, recordGet, recordCreate, recordUpdate, recordDelete, UnknownShelfError, recordTrashList, recordRestore, recordPurge, } from "./records-api.js";
|
|
3
3
|
import { ValidationError as ServerValidationError } from "./mutate.js";
|
|
4
4
|
import { buildClientSchema } from "./schema-api.js";
|
|
5
5
|
export function mapError(e) {
|
|
@@ -63,4 +63,30 @@ export class LocalClient {
|
|
|
63
63
|
mapError(e);
|
|
64
64
|
}
|
|
65
65
|
}
|
|
66
|
+
async listTrash() {
|
|
67
|
+
try {
|
|
68
|
+
return await recordTrashList();
|
|
69
|
+
}
|
|
70
|
+
catch (e) {
|
|
71
|
+
mapError(e);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
async restoreRecord(library, shelf, id) {
|
|
75
|
+
try {
|
|
76
|
+
await recordRestore(library, shelf, id);
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
catch (e) {
|
|
80
|
+
mapError(e);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
async purgeRecord(library, shelf, id) {
|
|
84
|
+
try {
|
|
85
|
+
await recordPurge(library, shelf, id);
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
catch (e) {
|
|
89
|
+
mapError(e);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
66
92
|
}
|
package/dist/mcp-tools.js
CHANGED
|
@@ -11,7 +11,7 @@ import { describeCondition } from '@coffer-org/sdk/condition';
|
|
|
11
11
|
import { getEm } from "./db.js";
|
|
12
12
|
import { getPluginSettings } from "./plugin-runtime.js";
|
|
13
13
|
import { searchEmbeddings } from "./embeddings.js";
|
|
14
|
-
import { embedOne } from "./embed-openai.js";
|
|
14
|
+
import { embedOne, embeddingFailureMessage } from "./embed-openai.js";
|
|
15
15
|
import { getLogger } from "./log.js";
|
|
16
16
|
import { writePluginSettings, listSettings } from "./settings-write.js";
|
|
17
17
|
import { ValidationError, NotFoundError } from "./mutate.js";
|
|
@@ -143,7 +143,7 @@ export async function collectMcpTools(opts = {}) {
|
|
|
143
143
|
return { content: [{ type: 'text', text: formatHits(hits) }] };
|
|
144
144
|
}
|
|
145
145
|
catch (e) {
|
|
146
|
-
return fail(`
|
|
146
|
+
return fail(`RAG unavailable: ${embeddingFailureMessage(e)} Use the regular coffer tools (list_records/get_record) instead.`);
|
|
147
147
|
}
|
|
148
148
|
},
|
|
149
149
|
});
|
|
@@ -265,7 +265,9 @@ export async function buildDomainSections() {
|
|
|
265
265
|
"some are collections (nested rows — an array). Extends add extra field-sets to a shelf's records, shown only when a " +
|
|
266
266
|
'condition holds (the "when …" notes below); in a fetched record they sit under `_extends`. ' +
|
|
267
267
|
'Read: list_libraries → describe_shelf → list_records/get_record. Write: create_record/update_record (call describe_shelf first); ' +
|
|
268
|
-
'
|
|
268
|
+
'delete_record moves a record to reversible trash — do not blank fields. Before editing, read the complete record and patch only requested fields. ' +
|
|
269
|
+
'For duplicates, read both records completely, compare fields/collections/extends/attachments, recommend the less complete record for trash, and offer a field-by-field merge first. ' +
|
|
270
|
+
'Use list_trash and restore_record for recovery; purge_record is irreversible and requires explicit confirmation.';
|
|
269
271
|
const rules = (await collectPluginInstructions()).map(({ id, instructions }) => `## ${id}\n${instructions}`);
|
|
270
272
|
const site = await frontendInstructions();
|
|
271
273
|
return [
|
|
@@ -281,6 +283,7 @@ export function buildMcpInstructions(sections) {
|
|
|
281
283
|
'Use the tools to read and write this data: call list_libraries to see what exists, describe_shelf before create_record/update_record, then list_records / get_record to read. When a search_records tool is available, use it for semantic lookup.',
|
|
282
284
|
'The per-library notes below name the shelves and the rules — not every field. For exact field names, types, and which are required, call describe_shelf(library, shelf) rather than guessing from the notes.',
|
|
283
285
|
'Record field values may be JSON-encoded (e.g. quantity {"value":2000,"unit":"ml"}) — parse them.',
|
|
286
|
+
'Action safety: read a complete record before editing or deleting it. update_record must be the smallest patch and must preserve unmentioned fields. delete_record moves the record to reversible trash; never blank fields to simulate deletion. For suspected duplicates, read both complete records, compare fields/collections/extends/attachments, identify the less complete record, and offer a field-by-field merge before moving anything to trash. Use list_trash/restore_record for recovery; purge_record is irreversible and requires explicit confirmation.',
|
|
284
287
|
'File fields (file/document/audio/video/image/media/avatar/cover/poster) hold a file uploaded to this server, ' +
|
|
285
288
|
'never a remote URL: {"name":"<filename from POST /api/upload>"}. Writing a URL is rejected. ' +
|
|
286
289
|
'To store a picture found on the web, download it locally first, then create_upload_ticket → POST /api/upload → write the returned name. ' +
|
package/dist/mutate.d.ts
CHANGED
|
@@ -20,8 +20,14 @@ export declare class NotFoundError extends Error {
|
|
|
20
20
|
constructor();
|
|
21
21
|
}
|
|
22
22
|
export declare function toIssue(i: z.ZodIssue): ValidationIssue;
|
|
23
|
-
export declare function listRecords(entityName: string
|
|
24
|
-
|
|
23
|
+
export declare function listRecords(entityName: string, opts?: {
|
|
24
|
+
deleted?: 'active' | 'deleted' | 'all';
|
|
25
|
+
}): Promise<Record<string, unknown>[]>;
|
|
26
|
+
export declare function getRecord(m: ShelfDef, entityName: string, id: number, opts?: {
|
|
27
|
+
includeDeleted?: boolean;
|
|
28
|
+
}): Promise<Record<string, unknown> | undefined>;
|
|
25
29
|
export declare function createRecord(m: ShelfDef, entityName: string, input: unknown, ctx: MutateCtx, afterBase?: AfterBaseHook): Promise<Record<string, unknown>>;
|
|
26
30
|
export declare function updateRecord(m: ShelfDef, entityName: string, id: number, input: unknown, ctx: MutateCtx, afterBase?: AfterBaseHook): Promise<Record<string, unknown>>;
|
|
27
|
-
export declare function deleteRecord(m: ShelfDef, entityName: string, id: number, ctx: MutateCtx
|
|
31
|
+
export declare function deleteRecord(m: ShelfDef, entityName: string, id: number, ctx: MutateCtx): Promise<void>;
|
|
32
|
+
export declare function restoreRecord(m: ShelfDef, entityName: string, id: number, ctx: MutateCtx, afterBase?: AfterBaseHook): Promise<Record<string, unknown>>;
|
|
33
|
+
export declare function purgeRecord(m: ShelfDef, entityName: string, id: number, ctx: MutateCtx, afterBase?: AfterBaseHook): Promise<void>;
|
package/dist/mutate.js
CHANGED
|
@@ -50,14 +50,19 @@ function writeEvent(em, actor, op, type, recordId, before, after) {
|
|
|
50
50
|
after: after == null ? null : JSON.stringify(after),
|
|
51
51
|
});
|
|
52
52
|
}
|
|
53
|
-
export async function listRecords(entityName) {
|
|
53
|
+
export async function listRecords(entityName, opts = {}) {
|
|
54
54
|
const fork = getEm().fork();
|
|
55
|
-
const
|
|
55
|
+
const deleted = opts.deleted ?? 'active';
|
|
56
|
+
const where = deleted === 'active' ? { deleted_at: null } : deleted === 'deleted' ? { deleted_at: { $ne: null } } : {};
|
|
57
|
+
const rows = await fork.findAll(entityName, { where, orderBy: { id: 'asc' } });
|
|
56
58
|
return serialize(rows);
|
|
57
59
|
}
|
|
58
|
-
export async function getRecord(m, entityName, id) {
|
|
60
|
+
export async function getRecord(m, entityName, id, opts = {}) {
|
|
59
61
|
const fork = getEm().fork();
|
|
60
|
-
const row = await fork.findOne(entityName, {
|
|
62
|
+
const row = await fork.findOne(entityName, {
|
|
63
|
+
id,
|
|
64
|
+
...(opts.includeDeleted ? {} : { deleted_at: null }),
|
|
65
|
+
});
|
|
61
66
|
if (!row)
|
|
62
67
|
return undefined;
|
|
63
68
|
const flat = decodeTemporal(m, serialize(row));
|
|
@@ -148,11 +153,45 @@ export async function updateRecord(m, entityName, id, input, ctx, afterBase) {
|
|
|
148
153
|
notifyRecordsChanged();
|
|
149
154
|
return result;
|
|
150
155
|
}
|
|
151
|
-
export async function deleteRecord(m, entityName, id, ctx
|
|
156
|
+
export async function deleteRecord(m, entityName, id, ctx) {
|
|
152
157
|
await getEm()
|
|
153
158
|
.fork()
|
|
154
159
|
.transactional(async (tx) => {
|
|
155
|
-
const found = await tx.findOne(entityName, { id });
|
|
160
|
+
const found = await tx.findOne(entityName, { id, deleted_at: null });
|
|
161
|
+
if (!found)
|
|
162
|
+
throw new NotFoundError();
|
|
163
|
+
const existing = serialize(found);
|
|
164
|
+
found.deleted_at = nowIso();
|
|
165
|
+
await tx.flush();
|
|
166
|
+
writeEvent(tx, ctx.actor, 'delete', `${m.library}/${m.shelf}`, id, existing, null);
|
|
167
|
+
});
|
|
168
|
+
notifyRecordsChanged();
|
|
169
|
+
}
|
|
170
|
+
export async function restoreRecord(m, entityName, id, ctx, afterBase) {
|
|
171
|
+
let result;
|
|
172
|
+
await getEm()
|
|
173
|
+
.fork()
|
|
174
|
+
.transactional(async (tx) => {
|
|
175
|
+
const found = await tx.findOne(entityName, { id, deleted_at: { $ne: null } });
|
|
176
|
+
if (!found)
|
|
177
|
+
throw new NotFoundError();
|
|
178
|
+
found.deleted_at = null;
|
|
179
|
+
await tx.flush();
|
|
180
|
+
const flat = decodeTemporal(m, serialize(found));
|
|
181
|
+
const nested = nestEmbedded(m, flat);
|
|
182
|
+
const collections = await readCollections(tx, m, id);
|
|
183
|
+
const _extends = afterBase ? await afterBase(tx, id) : undefined;
|
|
184
|
+
result = { ...nested, ...collections, ...(_extends ? { _extends } : {}) };
|
|
185
|
+
writeEvent(tx, ctx.actor, 'restore', `${m.library}/${m.shelf}`, id, null, result);
|
|
186
|
+
});
|
|
187
|
+
notifyRecordsChanged();
|
|
188
|
+
return result;
|
|
189
|
+
}
|
|
190
|
+
export async function purgeRecord(m, entityName, id, ctx, afterBase) {
|
|
191
|
+
await getEm()
|
|
192
|
+
.fork()
|
|
193
|
+
.transactional(async (tx) => {
|
|
194
|
+
const found = await tx.findOne(entityName, { id, deleted_at: { $ne: null } });
|
|
156
195
|
if (!found)
|
|
157
196
|
throw new NotFoundError();
|
|
158
197
|
const existing = serialize(found);
|
|
@@ -160,7 +199,7 @@ export async function deleteRecord(m, entityName, id, ctx, afterBase) {
|
|
|
160
199
|
if (afterBase)
|
|
161
200
|
await afterBase(tx, id);
|
|
162
201
|
tx.remove(found);
|
|
163
|
-
writeEvent(tx, ctx.actor, '
|
|
202
|
+
writeEvent(tx, ctx.actor, 'purge', `${m.library}/${m.shelf}`, id, existing, null);
|
|
164
203
|
});
|
|
165
204
|
notifyRecordsChanged();
|
|
166
205
|
}
|
package/dist/records-api.d.ts
CHANGED
|
@@ -12,6 +12,7 @@ export type RecordListOpts = {
|
|
|
12
12
|
limit?: number;
|
|
13
13
|
offset?: number;
|
|
14
14
|
orderBy?: Record<string, 'ASC' | 'DESC'>;
|
|
15
|
+
deleted?: 'active' | 'deleted' | 'all';
|
|
15
16
|
};
|
|
16
17
|
export declare const MAX_PAGE = 500;
|
|
17
18
|
export declare function coerceFilter(v: string, column: string): unknown;
|
|
@@ -21,14 +22,25 @@ export declare function rowMatch(mdef: ShelfDef, row: Record<string, unknown>, t
|
|
|
21
22
|
score: number;
|
|
22
23
|
snippet: string;
|
|
23
24
|
} | null;
|
|
24
|
-
export declare function recordCount(library: string, shelf: string, query?: RecordListQuery): Promise<number>;
|
|
25
|
+
export declare function recordCount(library: string, shelf: string, query?: RecordListQuery, opts?: Pick<RecordListOpts, 'deleted'>): Promise<number>;
|
|
25
26
|
export declare function recordList(library: string, shelf: string, query?: RecordListQuery, opts?: RecordListOpts): Promise<Record<string, unknown>[]>;
|
|
26
27
|
export declare function isPagedRequest(limit?: string, offset?: string): boolean;
|
|
27
28
|
export declare function recordListPage(library: string, shelf: string, query?: RecordListQuery, opts?: RecordListOpts): Promise<{
|
|
28
29
|
rows: Record<string, unknown>[];
|
|
29
30
|
total: number;
|
|
30
31
|
}>;
|
|
31
|
-
export declare function recordGet(library: string, shelf: string, id: number
|
|
32
|
+
export declare function recordGet(library: string, shelf: string, id: number, opts?: {
|
|
33
|
+
includeDeleted?: boolean;
|
|
34
|
+
}): Promise<Record<string, unknown> | null>;
|
|
32
35
|
export declare function recordCreate(library: string, shelf: string, body: unknown, actor?: string): Promise<Record<string, unknown>>;
|
|
33
36
|
export declare function recordUpdate(library: string, shelf: string, id: number, body: unknown, actor?: string): Promise<Record<string, unknown>>;
|
|
34
37
|
export declare function recordDelete(library: string, shelf: string, id: number, actor?: string): Promise<void>;
|
|
38
|
+
export declare function recordRestore(library: string, shelf: string, id: number, actor?: string): Promise<void>;
|
|
39
|
+
export declare function recordPurge(library: string, shelf: string, id: number, actor?: string): Promise<void>;
|
|
40
|
+
export interface TrashRecord {
|
|
41
|
+
library: string;
|
|
42
|
+
shelf: string;
|
|
43
|
+
label: string;
|
|
44
|
+
record: Record<string, unknown>;
|
|
45
|
+
}
|
|
46
|
+
export declare function recordTrashList(): Promise<TrashRecord[]>;
|
package/dist/records-api.js
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { fieldMap, textSearchKeys, titleKey, recordTitle, listKeys, storageColumnsFor } from '@coffer-org/sdk/shelf';
|
|
2
2
|
import { tokenize, matchScoreFolded, foldText } from '@coffer-org/core/search';
|
|
3
3
|
import { serialize } from '@mikro-orm/core';
|
|
4
|
-
import { getShelf, getExtendsFor } from "./registry-context.js";
|
|
4
|
+
import { getActiveRegistry, getShelf, getExtendsFor } from "./registry-context.js";
|
|
5
5
|
import { shelfTableName } from "./entity-schema.js";
|
|
6
6
|
import { getExtendRecord, getExtendRecords } from "./extend-table.js";
|
|
7
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
|
-
import { createRecord, updateRecord, getRecord, deleteRecord } from "./mutate.js";
|
|
10
|
+
import { createRecord, updateRecord, getRecord, deleteRecord, restoreRecord, purgeRecord } from "./mutate.js";
|
|
11
11
|
export class UnknownShelfError extends Error {
|
|
12
12
|
}
|
|
13
13
|
export const MAX_PAGE = 500;
|
|
@@ -98,20 +98,29 @@ function buildWhere(m, filterParams) {
|
|
|
98
98
|
}
|
|
99
99
|
return where;
|
|
100
100
|
}
|
|
101
|
-
export async function recordCount(library, shelf, query = {}) {
|
|
101
|
+
export async function recordCount(library, shelf, query = {}, opts = {}) {
|
|
102
102
|
const { m, ename } = resolve(library, shelf);
|
|
103
103
|
const { q: _q, ...filterParams } = query;
|
|
104
104
|
const where = buildWhere(m, filterParams);
|
|
105
|
-
|
|
105
|
+
const deleted = opts.deleted ?? 'active';
|
|
106
|
+
if (deleted === 'active')
|
|
107
|
+
where.deleted_at = null;
|
|
108
|
+
else if (deleted === 'deleted')
|
|
109
|
+
where.deleted_at = { $ne: null };
|
|
110
|
+
if (m.standalone === false && Object.keys(filterParams).length === 0)
|
|
106
111
|
return 0;
|
|
107
112
|
return getEm().fork().count(ename, where);
|
|
108
113
|
}
|
|
109
114
|
export async function recordList(library, shelf, query = {}, opts = {}) {
|
|
110
|
-
const { view = 'full', extends: withExt = true } = opts;
|
|
115
|
+
const { view = 'full', extends: withExt = true, deleted = 'active' } = opts;
|
|
111
116
|
const { m, ename } = resolve(library, shelf);
|
|
112
117
|
const { q, ...filterParams } = query;
|
|
113
118
|
const where = buildWhere(m, filterParams);
|
|
114
|
-
if (
|
|
119
|
+
if (deleted === 'active')
|
|
120
|
+
where.deleted_at = null;
|
|
121
|
+
else if (deleted === 'deleted')
|
|
122
|
+
where.deleted_at = { $ne: null };
|
|
123
|
+
if (m.standalone === false && Object.keys(filterParams).length === 0)
|
|
115
124
|
return [];
|
|
116
125
|
const fork = getEm().fork();
|
|
117
126
|
const tokens = typeof q === 'string' && q ? tokenize(q) : [];
|
|
@@ -157,14 +166,14 @@ export async function recordListPage(library, shelf, query = {}, opts = {}) {
|
|
|
157
166
|
return { rows: all.slice(offset, offset + limit), total: all.length };
|
|
158
167
|
}
|
|
159
168
|
const rows = await recordList(library, shelf, query, { ...opts, orderBy, limit, offset });
|
|
160
|
-
const total = await recordCount(library, shelf, query);
|
|
169
|
+
const total = await recordCount(library, shelf, query, opts);
|
|
161
170
|
return { rows, total };
|
|
162
171
|
}
|
|
163
|
-
export async function recordGet(library, shelf, id) {
|
|
172
|
+
export async function recordGet(library, shelf, id, opts = {}) {
|
|
164
173
|
const { m, ename } = resolve(library, shelf);
|
|
165
174
|
if (m.standalone === false)
|
|
166
175
|
return null;
|
|
167
|
-
const row = await getRecord(m, ename, id);
|
|
176
|
+
const row = await getRecord(m, ename, id, opts);
|
|
168
177
|
if (!row)
|
|
169
178
|
return null;
|
|
170
179
|
return withExtends(row, library, shelf);
|
|
@@ -191,5 +200,23 @@ export async function recordUpdate(library, shelf, id, body, actor = 'gui') {
|
|
|
191
200
|
}
|
|
192
201
|
export async function recordDelete(library, shelf, id, actor = 'gui') {
|
|
193
202
|
const { m, ename } = resolve(library, shelf);
|
|
194
|
-
await deleteRecord(m, ename, id, { actor }
|
|
203
|
+
await deleteRecord(m, ename, id, { actor });
|
|
204
|
+
}
|
|
205
|
+
export async function recordRestore(library, shelf, id, actor = 'gui') {
|
|
206
|
+
const { m, ename } = resolve(library, shelf);
|
|
207
|
+
await restoreRecord(m, ename, id, { actor }, (tx, recordId) => readExtends(tx, library, shelf, recordId));
|
|
208
|
+
}
|
|
209
|
+
export async function recordPurge(library, shelf, id, actor = 'gui') {
|
|
210
|
+
const { m, ename } = resolve(library, shelf);
|
|
211
|
+
await purgeRecord(m, ename, id, { actor }, (tx, recordId) => deleteExtends(tx, library, shelf, recordId));
|
|
212
|
+
}
|
|
213
|
+
export async function recordTrashList() {
|
|
214
|
+
const shelves = getActiveRegistry().shelves.filter((s) => s.standalone !== false);
|
|
215
|
+
const out = [];
|
|
216
|
+
for (const m of shelves) {
|
|
217
|
+
const rows = await recordList(m.library, m.shelf, {}, { view: 'full', extends: true, deleted: 'deleted' });
|
|
218
|
+
for (const record of rows)
|
|
219
|
+
out.push({ library: m.library, shelf: m.shelf, label: m.label, record });
|
|
220
|
+
}
|
|
221
|
+
return out.sort((a, b) => String(b.record.deleted_at ?? '').localeCompare(String(a.record.deleted_at ?? '')));
|
|
195
222
|
}
|
package/dist/search-indexer.js
CHANGED
|
@@ -23,7 +23,7 @@ export async function indexSearchOnce(batch = DEFAULT_BATCH) {
|
|
|
23
23
|
if (!library || !shelf)
|
|
24
24
|
continue;
|
|
25
25
|
const key = `${r.type}/${r.record_id}`;
|
|
26
|
-
if (r.op === 'delete') {
|
|
26
|
+
if (r.op === 'delete' || r.op === 'purge') {
|
|
27
27
|
byRef.set(key, { shelf: r.type, recordId: r.record_id, op: 'delete', folded: '' });
|
|
28
28
|
continue;
|
|
29
29
|
}
|