@coffer-org/server 1.10.0 → 1.12.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/batch.d.ts +2 -0
- package/dist/batch.js +9 -0
- package/dist/collection-io.d.ts +1 -0
- package/dist/collection-io.js +29 -0
- package/dist/extend-table.d.ts +1 -0
- package/dist/extend-table.js +22 -1
- package/dist/index.js +53 -8
- package/dist/mcp-tools.d.ts +1 -1
- package/dist/mcp-tools.js +22 -0
- package/dist/plugin-discovery.d.ts +2 -1
- package/dist/plugin-discovery.js +15 -6
- package/dist/plugins-api.d.ts +1 -1
- package/dist/plugins-api.js +7 -4
- package/dist/records-api.d.ts +6 -1
- package/dist/records-api.js +37 -5
- package/dist/upload-ticket.d.ts +7 -0
- package/dist/upload-ticket.js +29 -0
- package/package.json +2 -2
package/dist/batch.d.ts
ADDED
package/dist/batch.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export const IN_CHUNK = 500;
|
|
2
|
+
export function chunk(items, size = IN_CHUNK) {
|
|
3
|
+
if (items.length <= size)
|
|
4
|
+
return items.length ? [items] : [];
|
|
5
|
+
const out = [];
|
|
6
|
+
for (let i = 0; i < items.length; i += size)
|
|
7
|
+
out.push(items.slice(i, i + size));
|
|
8
|
+
return out;
|
|
9
|
+
}
|
package/dist/collection-io.d.ts
CHANGED
|
@@ -9,6 +9,7 @@ export declare function splitAt(fields: LayoutEl[], input: Row): {
|
|
|
9
9
|
};
|
|
10
10
|
export declare function writeAt(tx: EntityManager, prefix: string, fields: LayoutEl[], parentId: number, collections: Record<string, Row[]>): Promise<void>;
|
|
11
11
|
export declare function readAt(em: EntityManager, prefix: string, fields: LayoutEl[], parentId: number): Promise<Record<string, Row[]>>;
|
|
12
|
+
export declare function readAtMany(em: EntityManager, prefix: string, fields: LayoutEl[], parentIds: number[]): Promise<Map<number, Record<string, Row[]>>>;
|
|
12
13
|
export declare function deleteAt(tx: EntityManager, prefix: string, fields: LayoutEl[], parentId: number): Promise<void>;
|
|
13
14
|
export declare const splitCollections: (m: ShelfDef, input: Row) => {
|
|
14
15
|
base: Row;
|
package/dist/collection-io.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { serialize } from '@mikro-orm/core';
|
|
2
2
|
import { collectionGroups, fieldEntries, storageColumns } from '@coffer-org/sdk/shelf';
|
|
3
3
|
import { isGroup, isEmbeddedGroup, isJsonStored } from '@coffer-org/sdk/fields';
|
|
4
|
+
import { chunk } from "./batch.js";
|
|
4
5
|
function flattenLayout(fields) {
|
|
5
6
|
const out = [];
|
|
6
7
|
for (const f of fields) {
|
|
@@ -78,6 +79,34 @@ export async function readAt(em, prefix, fields, parentId) {
|
|
|
78
79
|
}
|
|
79
80
|
return out;
|
|
80
81
|
}
|
|
82
|
+
export async function readAtMany(em, prefix, fields, parentIds) {
|
|
83
|
+
const out = new Map();
|
|
84
|
+
for (const id of parentIds)
|
|
85
|
+
out.set(id, {});
|
|
86
|
+
if (parentIds.length === 0)
|
|
87
|
+
return out;
|
|
88
|
+
for (const c of collectionGroups(fields)) {
|
|
89
|
+
const table = `${prefix}__${c.key}`;
|
|
90
|
+
const childFields = c.group.fields;
|
|
91
|
+
const rows = [];
|
|
92
|
+
for (const ids of chunk(parentIds)) {
|
|
93
|
+
rows.push(...serialize(await em.find(table, { parent_id: { $in: ids } }, {
|
|
94
|
+
orderBy: { position: 'asc' },
|
|
95
|
+
})));
|
|
96
|
+
}
|
|
97
|
+
const nestedByChild = await readAtMany(em, table, childFields, rows.map((r) => r.id));
|
|
98
|
+
for (const id of parentIds)
|
|
99
|
+
out.get(id)[c.key] = [];
|
|
100
|
+
for (const row of rows) {
|
|
101
|
+
const { id, parent_id: pid, position: _pos, ...rest } = row;
|
|
102
|
+
const bucket = out.get(pid);
|
|
103
|
+
if (!bucket)
|
|
104
|
+
continue;
|
|
105
|
+
bucket[c.key].push({ ...nestEmbeddedAt(childFields, rest), ...(nestedByChild.get(id) ?? {}) });
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return out;
|
|
109
|
+
}
|
|
81
110
|
export async function deleteAt(tx, prefix, fields, parentId) {
|
|
82
111
|
for (const c of collectionGroups(fields)) {
|
|
83
112
|
const table = `${prefix}__${c.key}`;
|
package/dist/extend-table.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ import type { ExtendDef } from '@coffer-org/sdk/extend';
|
|
|
3
3
|
type Row = Record<string, unknown>;
|
|
4
4
|
export declare function extendEntityName(e: ExtendDef): string;
|
|
5
5
|
export declare function getExtendRecord(e: ExtendDef, baseId: number, em?: EntityManager): Promise<Row | undefined>;
|
|
6
|
+
export declare function getExtendRecords(e: ExtendDef, baseIds: number[], em?: EntityManager): Promise<Map<number, Row>>;
|
|
6
7
|
export declare function upsertExtendRecord(em: EntityManager, e: ExtendDef, baseId: number, data: Row): Promise<void>;
|
|
7
8
|
export declare function deleteExtendRecord(em: EntityManager, e: ExtendDef, baseId: number): Promise<void>;
|
|
8
9
|
export {};
|
package/dist/extend-table.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { serialize } from '@mikro-orm/core';
|
|
2
2
|
import { getEm } from "./db.js";
|
|
3
|
-
import { splitAt, writeAt, readAt, deleteAt } from "./collection-io.js";
|
|
3
|
+
import { splitAt, writeAt, readAt, readAtMany, deleteAt } from "./collection-io.js";
|
|
4
|
+
import { chunk } from "./batch.js";
|
|
4
5
|
export function extendEntityName(e) {
|
|
5
6
|
return `extend__${e.id}`;
|
|
6
7
|
}
|
|
@@ -14,6 +15,26 @@ export async function getExtendRecord(e, baseId, em) {
|
|
|
14
15
|
const collections = await readAt(fork, name, e.fields, baseId);
|
|
15
16
|
return { ...flat, ...collections };
|
|
16
17
|
}
|
|
18
|
+
export async function getExtendRecords(e, baseIds, em) {
|
|
19
|
+
const out = new Map();
|
|
20
|
+
if (baseIds.length === 0)
|
|
21
|
+
return out;
|
|
22
|
+
const fork = em ?? getEm().fork();
|
|
23
|
+
const name = extendEntityName(e);
|
|
24
|
+
const flats = [];
|
|
25
|
+
for (const ids of chunk(baseIds)) {
|
|
26
|
+
flats.push(...serialize(await fork.find(name, { base_id: { $in: ids } })));
|
|
27
|
+
}
|
|
28
|
+
if (flats.length === 0)
|
|
29
|
+
return out;
|
|
30
|
+
const presentIds = flats.map((r) => Number(r.base_id));
|
|
31
|
+
const collectionsById = await readAtMany(fork, name, e.fields, presentIds);
|
|
32
|
+
for (const flat of flats) {
|
|
33
|
+
const id = Number(flat.base_id);
|
|
34
|
+
out.set(id, { ...flat, ...(collectionsById.get(id) ?? {}) });
|
|
35
|
+
}
|
|
36
|
+
return out;
|
|
37
|
+
}
|
|
17
38
|
export async function upsertExtendRecord(em, e, baseId, data) {
|
|
18
39
|
const name = extendEntityName(e);
|
|
19
40
|
const { base, collections } = splitAt(e.fields, data);
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { randomUUID } from 'node:crypto';
|
|
1
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
2
2
|
import { createWriteStream, existsSync } from 'node:fs';
|
|
3
3
|
import { realpath } from 'node:fs/promises';
|
|
4
4
|
import { pipeline } from 'node:stream/promises';
|
|
@@ -9,7 +9,7 @@ import cors from '@fastify/cors';
|
|
|
9
9
|
import multipart from '@fastify/multipart';
|
|
10
10
|
import fastifyStatic from '@fastify/static';
|
|
11
11
|
import { serialize } from '@mikro-orm/core';
|
|
12
|
-
import { textSearchKeys, recordTitle } from '@coffer-org/sdk/shelf';
|
|
12
|
+
import { textSearchKeys, recordTitle, titleKey, storageColumnsFor } from '@coffer-org/sdk/shelf';
|
|
13
13
|
import { tokenize } from '@coffer-org/core/search';
|
|
14
14
|
import { shelfTableName } from "./entity-schema.js";
|
|
15
15
|
import { getEm, closeDb } from "./db.js";
|
|
@@ -31,6 +31,7 @@ import { maskTree, preserveTree } from "./field-masking.js";
|
|
|
31
31
|
import { writePluginSettings } from "./settings-write.js";
|
|
32
32
|
import { rootLogger, getLogger } from "./log.js";
|
|
33
33
|
import { uploadsDir } from "./uploads.js";
|
|
34
|
+
import { verifyUploadTicket } from "./upload-ticket.js";
|
|
34
35
|
const ENV_FILE = join(process.cwd(), '.env');
|
|
35
36
|
if (existsSync(ENV_FILE))
|
|
36
37
|
process.loadEnvFile(ENV_FILE);
|
|
@@ -75,6 +76,11 @@ app.addHook('onRequest', async (req, reply) => {
|
|
|
75
76
|
return;
|
|
76
77
|
if (PUBLIC_API_PATHS.some((p) => req.url.startsWith(p)))
|
|
77
78
|
return;
|
|
79
|
+
if (req.method === 'POST' && req.url === '/api/upload') {
|
|
80
|
+
const auth = req.headers.authorization;
|
|
81
|
+
if (auth?.startsWith('Bearer ') && verifyUploadTicket(auth.slice('Bearer '.length)))
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
78
84
|
const user = await resolveRequestUser(req);
|
|
79
85
|
if (!user) {
|
|
80
86
|
if (isMcp) {
|
|
@@ -127,7 +133,18 @@ app.get('/health', async (_req, reply) => {
|
|
|
127
133
|
return reply.code(503).send({ status: 'db_unavailable' });
|
|
128
134
|
}
|
|
129
135
|
});
|
|
130
|
-
|
|
136
|
+
let librariesPayload = null;
|
|
137
|
+
app.get('/api/libraries', async (req, reply) => {
|
|
138
|
+
if (!librariesPayload) {
|
|
139
|
+
const body = JSON.stringify(buildClientSchema());
|
|
140
|
+
librariesPayload = { body, etag: `"${createHash('sha1').update(body).digest('base64url')}"` };
|
|
141
|
+
}
|
|
142
|
+
reply.header('ETag', librariesPayload.etag);
|
|
143
|
+
reply.header('Cache-Control', 'no-cache');
|
|
144
|
+
if (req.headers['if-none-match'] === librariesPayload.etag)
|
|
145
|
+
return reply.code(304).send();
|
|
146
|
+
return reply.type('application/json').send(librariesPayload.body);
|
|
147
|
+
});
|
|
131
148
|
app.get('/api/counts', async () => {
|
|
132
149
|
const fork = getEm().fork();
|
|
133
150
|
const out = {};
|
|
@@ -141,6 +158,8 @@ app.get('/api/counts', async () => {
|
|
|
141
158
|
}
|
|
142
159
|
return out;
|
|
143
160
|
});
|
|
161
|
+
const SEARCH_SCAN_LIMIT = 1000;
|
|
162
|
+
const searchLog = getLogger('search');
|
|
144
163
|
app.get('/api/search', async (req) => {
|
|
145
164
|
const { q = '', limit = '20' } = req.query;
|
|
146
165
|
const tokens = tokenize(q);
|
|
@@ -158,7 +177,11 @@ app.get('/api/search', async (req) => {
|
|
|
158
177
|
if (!textSearchKeys(mdef).length)
|
|
159
178
|
continue;
|
|
160
179
|
const [library, type] = k.split('/');
|
|
161
|
-
const
|
|
180
|
+
const cols = storageColumnsFor(mdef, [...textSearchKeys(mdef), titleKey(mdef)]);
|
|
181
|
+
const rows = (await fork.find(ename, {}, { limit: SEARCH_SCAN_LIMIT, fields: cols })).map((r) => serialize(r));
|
|
182
|
+
if (rows.length === SEARCH_SCAN_LIMIT) {
|
|
183
|
+
searchLog.warn('scan cap reached', { shelf: k, limit: SEARCH_SCAN_LIMIT });
|
|
184
|
+
}
|
|
162
185
|
for (const row of rows) {
|
|
163
186
|
const m = rowMatch(mdef, row, tokens);
|
|
164
187
|
if (!m)
|
|
@@ -357,9 +380,12 @@ function maskRecordRow(library, type, row) {
|
|
|
357
380
|
}
|
|
358
381
|
app.get('/api/:library/:type', async (req, reply) => {
|
|
359
382
|
const { library, type } = req.params;
|
|
360
|
-
const query = req.query;
|
|
383
|
+
const { _fields, _extends, ...query } = req.query;
|
|
361
384
|
try {
|
|
362
|
-
const rows = await recordList(library, type, query
|
|
385
|
+
const rows = await recordList(library, type, query, {
|
|
386
|
+
view: _fields === 'full' ? 'full' : 'list',
|
|
387
|
+
extends: _extends === '1',
|
|
388
|
+
});
|
|
363
389
|
return rows.map((r) => maskRecordRow(library, type, r));
|
|
364
390
|
}
|
|
365
391
|
catch (e) {
|
|
@@ -450,12 +476,31 @@ await registerAuthApi(app);
|
|
|
450
476
|
registerOAuthApi(app);
|
|
451
477
|
await registerMcpHttp(app);
|
|
452
478
|
await registerPluginsApi(app);
|
|
479
|
+
const IMMUTABLE = 'public, max-age=31536000, immutable';
|
|
480
|
+
const isHashedAsset = (p) => /-[A-Za-z0-9_-]{8,}\.(js|css)$/.test(p);
|
|
453
481
|
const WEB_DIST = process.env.WEB_DIST;
|
|
454
482
|
if (WEB_DIST && existsSync(join(WEB_DIST, 'index.html'))) {
|
|
455
|
-
await app.register(fastifyStatic, {
|
|
483
|
+
await app.register(fastifyStatic, {
|
|
484
|
+
root: WEB_DIST,
|
|
485
|
+
prefix: '/',
|
|
486
|
+
decorateReply: false,
|
|
487
|
+
cacheControl: false,
|
|
488
|
+
setHeaders(res, path) {
|
|
489
|
+
const hashed = path.includes(`${sep}assets${sep}`) || isHashedAsset(path);
|
|
490
|
+
res.setHeader('Cache-Control', hashed ? IMMUTABLE : 'no-cache');
|
|
491
|
+
},
|
|
492
|
+
});
|
|
456
493
|
const VENDOR_DIST = join(WEB_DIST, '..', 'dist-vendor');
|
|
457
494
|
if (existsSync(VENDOR_DIST)) {
|
|
458
|
-
await app.register(fastifyStatic, {
|
|
495
|
+
await app.register(fastifyStatic, {
|
|
496
|
+
root: VENDOR_DIST,
|
|
497
|
+
prefix: '/vendor/',
|
|
498
|
+
decorateReply: false,
|
|
499
|
+
cacheControl: false,
|
|
500
|
+
setHeaders(res, path) {
|
|
501
|
+
res.setHeader('Cache-Control', isHashedAsset(path) ? IMMUTABLE : 'no-cache');
|
|
502
|
+
},
|
|
503
|
+
});
|
|
459
504
|
}
|
|
460
505
|
app.setNotFoundHandler((req, reply) => {
|
|
461
506
|
if (req.method === 'GET' &&
|
package/dist/mcp-tools.d.ts
CHANGED
|
@@ -10,7 +10,7 @@ export interface McpToolDef {
|
|
|
10
10
|
httpName: string;
|
|
11
11
|
description: string;
|
|
12
12
|
inputSchema: z.ZodRawShape;
|
|
13
|
-
scope: 'crud' | 'plugin' | 'rag' | 'settings';
|
|
13
|
+
scope: 'crud' | 'plugin' | 'rag' | 'settings' | 'upload';
|
|
14
14
|
role: AuthRole;
|
|
15
15
|
handler: (args: Record<string, unknown>) => Promise<ToolResult>;
|
|
16
16
|
}
|
package/dist/mcp-tools.js
CHANGED
|
@@ -2,6 +2,7 @@ import { z } from 'zod';
|
|
|
2
2
|
import { buildTools } from '@coffer-org/mcp';
|
|
3
3
|
import { SchemaCache } from '@coffer-org/mcp/schema';
|
|
4
4
|
import { LocalClient } from "./mcp-local.js";
|
|
5
|
+
import { mintUploadTicket } from "./upload-ticket.js";
|
|
5
6
|
import { pluginHooks, pluginCtx } from "./plugin-hooks.js";
|
|
6
7
|
import { getActiveRegistry } from "./registry-context.js";
|
|
7
8
|
import { describeCondition } from '@coffer-org/sdk/condition';
|
|
@@ -49,6 +50,27 @@ export async function collectMcpTools(opts = {}) {
|
|
|
49
50
|
handler: t.handler,
|
|
50
51
|
});
|
|
51
52
|
}
|
|
53
|
+
{
|
|
54
|
+
const actor = opts.actor ?? 'mcp';
|
|
55
|
+
out.push({
|
|
56
|
+
server: 'coffer',
|
|
57
|
+
bareName: 'create_upload_ticket',
|
|
58
|
+
httpName: 'create_upload_ticket',
|
|
59
|
+
description: 'Mint a short-lived (60 min) upload-only token for POST /api/upload. Use it as a Bearer header to stream local files to the server without sending their bytes through this conversation. The token cannot read or modify records. Response of each upload is {"filename":"<name>"} — store it in a file/image field as {"name":"<name>"}.',
|
|
60
|
+
inputSchema: {},
|
|
61
|
+
scope: 'upload',
|
|
62
|
+
role: 'member',
|
|
63
|
+
handler: async () => {
|
|
64
|
+
const { token, expiresInSec } = mintUploadTicket(actor);
|
|
65
|
+
return ok({
|
|
66
|
+
token,
|
|
67
|
+
upload_url: '/api/upload',
|
|
68
|
+
expires_in: expiresInSec,
|
|
69
|
+
how_to: 'curl -H "Authorization: Bearer <token>" -F "file=@<path>" <base-url>/api/upload → {"filename":"<name>"}. Then set a file field to {"name":"<name>"}.',
|
|
70
|
+
});
|
|
71
|
+
},
|
|
72
|
+
});
|
|
73
|
+
}
|
|
52
74
|
for (const [id, h] of Object.entries(pluginHooks)) {
|
|
53
75
|
for (const t of h.agent?.tools ?? []) {
|
|
54
76
|
out.push({
|
|
@@ -10,7 +10,7 @@ export interface PluginAssetUrls {
|
|
|
10
10
|
web?: string;
|
|
11
11
|
css?: string;
|
|
12
12
|
}
|
|
13
|
-
export declare function resolveAssetUrls(id: string, coffer: PluginAssetPaths): PluginAssetUrls;
|
|
13
|
+
export declare function resolveAssetUrls(id: string, coffer: PluginAssetPaths, version?: string): PluginAssetUrls;
|
|
14
14
|
export interface PluginAssetRecord {
|
|
15
15
|
id: string;
|
|
16
16
|
version: string;
|
|
@@ -21,6 +21,7 @@ export interface PluginAssetRecord {
|
|
|
21
21
|
css?: string;
|
|
22
22
|
local?: boolean;
|
|
23
23
|
}
|
|
24
|
+
export declare function invalidatePluginAssets(): void;
|
|
24
25
|
export declare function discoverPluginAssets(): Promise<PluginAssetRecord[]>;
|
|
25
26
|
export interface RuntimeRecord {
|
|
26
27
|
packageName: string;
|
package/dist/plugin-discovery.js
CHANGED
|
@@ -31,13 +31,14 @@ async function pkgNames(nm) {
|
|
|
31
31
|
}
|
|
32
32
|
return names;
|
|
33
33
|
}
|
|
34
|
-
export function resolveAssetUrls(id, coffer) {
|
|
34
|
+
export function resolveAssetUrls(id, coffer, version) {
|
|
35
35
|
const base = `/plugins/${id}`;
|
|
36
|
-
const
|
|
36
|
+
const v = version ? `?v=${encodeURIComponent(version)}` : '';
|
|
37
|
+
const out = { schema: `${base}/schema.js${v}` };
|
|
37
38
|
if (coffer.web)
|
|
38
|
-
out.web = `${base}/web.js`;
|
|
39
|
+
out.web = `${base}/web.js${v}`;
|
|
39
40
|
if (coffer.css)
|
|
40
|
-
out.css = `${base}/web.css`;
|
|
41
|
+
out.css = `${base}/web.css${v}`;
|
|
41
42
|
return out;
|
|
42
43
|
}
|
|
43
44
|
async function isLocalPackage(dir) {
|
|
@@ -49,7 +50,13 @@ async function isLocalPackage(dir) {
|
|
|
49
50
|
return false;
|
|
50
51
|
}
|
|
51
52
|
}
|
|
53
|
+
let assetsCache = null;
|
|
54
|
+
export function invalidatePluginAssets() {
|
|
55
|
+
assetsCache = null;
|
|
56
|
+
}
|
|
52
57
|
export async function discoverPluginAssets() {
|
|
58
|
+
if (assetsCache)
|
|
59
|
+
return assetsCache;
|
|
53
60
|
const nm = join(process.cwd(), 'node_modules');
|
|
54
61
|
const names = await pkgNames(nm);
|
|
55
62
|
const out = [];
|
|
@@ -62,10 +69,11 @@ export async function discoverPluginAssets() {
|
|
|
62
69
|
const manifest = (await import(__rewriteRelativeImportExtension(spec))).default;
|
|
63
70
|
if (!manifest?.id)
|
|
64
71
|
continue;
|
|
65
|
-
const
|
|
72
|
+
const version = pkg.version ?? '0.0.0';
|
|
73
|
+
const urls = resolveAssetUrls(manifest.id, pkg.coffer, version);
|
|
66
74
|
out.push({
|
|
67
75
|
id: manifest.id,
|
|
68
|
-
version
|
|
76
|
+
version,
|
|
69
77
|
dependsOn: manifest.dependsOn ?? [],
|
|
70
78
|
packageName: name,
|
|
71
79
|
schema: urls.schema,
|
|
@@ -78,6 +86,7 @@ export async function discoverPluginAssets() {
|
|
|
78
86
|
log.error(`${name}: asset scan skip — ${e.message}`);
|
|
79
87
|
}
|
|
80
88
|
}
|
|
89
|
+
assetsCache = out;
|
|
81
90
|
return out;
|
|
82
91
|
}
|
|
83
92
|
const RUNTIME_PACKAGE = '@coffer-org/meta';
|
package/dist/plugins-api.d.ts
CHANGED
|
@@ -15,5 +15,5 @@ export interface PluginListEntry {
|
|
|
15
15
|
web?: string;
|
|
16
16
|
css?: string;
|
|
17
17
|
}
|
|
18
|
-
export declare function buildPluginListResponse(plugins: PluginManifest[], assets: PluginAssetRecord[], disabled: Set<string
|
|
18
|
+
export declare function buildPluginListResponse(plugins: PluginManifest[], assets: PluginAssetRecord[], disabled: Set<string>, withUpdates?: boolean): Promise<PluginListEntry[]>;
|
|
19
19
|
export declare function registerPluginsApi(app: FastifyInstance): Promise<void>;
|
package/dist/plugins-api.js
CHANGED
|
@@ -5,14 +5,14 @@ import { getPlugins, readDisabled } from "./plugin-runtime.js";
|
|
|
5
5
|
import { checkLatestVersion } from "./plugin-updates.js";
|
|
6
6
|
const nmRoot = () => join(process.cwd(), 'node_modules');
|
|
7
7
|
const ASSET_KEY = { 'schema.js': 'schema', 'web.js': 'web', 'web.css': 'css' };
|
|
8
|
-
export async function buildPluginListResponse(plugins, assets, disabled) {
|
|
8
|
+
export async function buildPluginListResponse(plugins, assets, disabled, withUpdates = true) {
|
|
9
9
|
const assetById = new Map(assets.map((a) => [a.id, a]));
|
|
10
10
|
return Promise.all(plugins.map(async (p) => {
|
|
11
11
|
const a = assetById.get(p.id);
|
|
12
12
|
return {
|
|
13
13
|
id: p.id,
|
|
14
14
|
installedVersion: a?.version ?? p.version,
|
|
15
|
-
latestVersion: a && !a.local ? await checkLatestVersion(a.packageName) : null,
|
|
15
|
+
latestVersion: withUpdates && a && !a.local ? await checkLatestVersion(a.packageName) : null,
|
|
16
16
|
packageName: a?.packageName ?? null,
|
|
17
17
|
dependsOn: p.dependsOn,
|
|
18
18
|
enabled: !disabled.has(p.id),
|
|
@@ -26,9 +26,10 @@ export async function buildPluginListResponse(plugins, assets, disabled) {
|
|
|
26
26
|
}));
|
|
27
27
|
}
|
|
28
28
|
export async function registerPluginsApi(app) {
|
|
29
|
-
app.get('/api/plugins', async () => {
|
|
29
|
+
app.get('/api/plugins', async (req) => {
|
|
30
|
+
const withUpdates = req.query.updates === '1';
|
|
30
31
|
const [plugins, assets, disabled] = await Promise.all([getPlugins(), discoverPluginAssets(), readDisabled()]);
|
|
31
|
-
return buildPluginListResponse(plugins, assets, disabled);
|
|
32
|
+
return buildPluginListResponse(plugins, assets, disabled, withUpdates);
|
|
32
33
|
});
|
|
33
34
|
app.get('/api/runtime', async () => {
|
|
34
35
|
const runtime = await discoverRuntime();
|
|
@@ -63,6 +64,8 @@ export async function registerPluginsApi(app) {
|
|
|
63
64
|
if (!existsSync(abs))
|
|
64
65
|
return reply.code(404).send({ error: 'asset_missing' });
|
|
65
66
|
reply.type(abs.endsWith('.css') ? 'text/css' : 'text/javascript');
|
|
67
|
+
const pinned = Boolean(req.query.v) || !key;
|
|
68
|
+
reply.header('Cache-Control', pinned ? 'public, max-age=31536000, immutable' : 'no-cache');
|
|
66
69
|
return reply.send(createReadStream(abs));
|
|
67
70
|
});
|
|
68
71
|
}
|
package/dist/records-api.d.ts
CHANGED
|
@@ -6,13 +6,18 @@ export type RecordListQuery = {
|
|
|
6
6
|
id?: string;
|
|
7
7
|
[field: string]: string | number | undefined;
|
|
8
8
|
};
|
|
9
|
+
export type RecordListOpts = {
|
|
10
|
+
view?: 'list' | 'full';
|
|
11
|
+
extends?: boolean;
|
|
12
|
+
};
|
|
9
13
|
export declare function coerceFilter(v: string, column: string): unknown;
|
|
10
14
|
export declare function withExtends(record: Record<string, unknown>, library: string, shelf: string): Promise<Record<string, unknown>>;
|
|
15
|
+
export declare function withExtendsMany(rows: Record<string, unknown>[], library: string, shelf: string): Promise<Record<string, unknown>[]>;
|
|
11
16
|
export declare function rowMatch(mdef: ShelfDef, row: Record<string, unknown>, tokens: string[]): {
|
|
12
17
|
score: number;
|
|
13
18
|
snippet: string;
|
|
14
19
|
} | null;
|
|
15
|
-
export declare function recordList(library: string, type: string, query?: RecordListQuery): Promise<Record<string, unknown>[]>;
|
|
20
|
+
export declare function recordList(library: string, type: string, query?: RecordListQuery, opts?: RecordListOpts): Promise<Record<string, unknown>[]>;
|
|
16
21
|
export declare function recordGet(library: string, type: string, id: number): Promise<Record<string, unknown> | null>;
|
|
17
22
|
export declare function recordCreate(library: string, type: string, body: unknown, actor?: string): Promise<Record<string, unknown>>;
|
|
18
23
|
export declare function recordUpdate(library: string, type: string, id: number, body: unknown, actor?: string): Promise<Record<string, unknown>>;
|
package/dist/records-api.js
CHANGED
|
@@ -1,15 +1,22 @@
|
|
|
1
|
-
import { fieldMap, textSearchKeys, titleKey, recordTitle } from '@coffer-org/sdk/shelf';
|
|
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
4
|
import { getShelf, getExtendsFor } from "./registry-context.js";
|
|
5
5
|
import { shelfTableName } from "./entity-schema.js";
|
|
6
|
-
import { getExtendRecord } from "./extend-table.js";
|
|
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
10
|
import { createRecord, updateRecord, getRecord, deleteRecord } from "./mutate.js";
|
|
11
11
|
export class UnknownTypeError extends Error {
|
|
12
12
|
}
|
|
13
|
+
function pickCols(row, cols) {
|
|
14
|
+
const out = {};
|
|
15
|
+
for (const c of cols)
|
|
16
|
+
if (c in row)
|
|
17
|
+
out[c] = row[c];
|
|
18
|
+
return out;
|
|
19
|
+
}
|
|
13
20
|
function resolve(library, type) {
|
|
14
21
|
const m = getShelf(library, type);
|
|
15
22
|
if (!m)
|
|
@@ -37,6 +44,22 @@ export async function withExtends(record, library, shelf) {
|
|
|
37
44
|
}));
|
|
38
45
|
return { ...record, _extends };
|
|
39
46
|
}
|
|
47
|
+
export async function withExtendsMany(rows, library, shelf) {
|
|
48
|
+
const matchedExtends = getExtendsFor(library, shelf);
|
|
49
|
+
if (!matchedExtends.length || rows.length === 0)
|
|
50
|
+
return rows;
|
|
51
|
+
const ids = rows.map((r) => Number(r.id));
|
|
52
|
+
const byExtend = new Map();
|
|
53
|
+
await Promise.all(matchedExtends.map(async (e) => {
|
|
54
|
+
byExtend.set(e.id, await getExtendRecords(e, ids));
|
|
55
|
+
}));
|
|
56
|
+
return rows.map((row) => {
|
|
57
|
+
const _extends = {};
|
|
58
|
+
for (const e of matchedExtends)
|
|
59
|
+
_extends[e.id] = byExtend.get(e.id)?.get(Number(row.id)) ?? null;
|
|
60
|
+
return { ...row, _extends };
|
|
61
|
+
});
|
|
62
|
+
}
|
|
40
63
|
export function rowMatch(mdef, row, tokens) {
|
|
41
64
|
const textKeys = textSearchKeys(mdef);
|
|
42
65
|
if (!textKeys.length)
|
|
@@ -54,7 +77,8 @@ export function rowMatch(mdef, row, tokens) {
|
|
|
54
77
|
const snippet = raw.length > 120 ? raw.slice(0, 120) + '…' : raw;
|
|
55
78
|
return { score, snippet };
|
|
56
79
|
}
|
|
57
|
-
export async function recordList(library, type, query = {}) {
|
|
80
|
+
export async function recordList(library, type, query = {}, opts = {}) {
|
|
81
|
+
const { view = 'full', extends: withExt = true } = opts;
|
|
58
82
|
const { m, ename } = resolve(library, type);
|
|
59
83
|
const { q, ...filterParams } = query;
|
|
60
84
|
const where = {};
|
|
@@ -77,12 +101,20 @@ export async function recordList(library, type, query = {}) {
|
|
|
77
101
|
if (m.standalone === false && Object.keys(where).length === 0)
|
|
78
102
|
return [];
|
|
79
103
|
const fork = getEm().fork();
|
|
80
|
-
let rows = (await fork.find(ename, where)).map((r) => serialize(r));
|
|
81
104
|
const tokens = typeof q === 'string' && q ? tokenize(q) : [];
|
|
105
|
+
const projected = view === 'list' ? storageColumnsFor(m, listKeys(m)) : null;
|
|
106
|
+
const findOpts = projected
|
|
107
|
+
? { fields: tokens.length ? [...new Set([...projected, ...storageColumnsFor(m, textSearchKeys(m))])] : projected }
|
|
108
|
+
: undefined;
|
|
109
|
+
let rows = (await fork.find(ename, where, findOpts)).map((r) => serialize(r));
|
|
82
110
|
if (tokens.length > 0)
|
|
83
111
|
rows = rows.filter((row) => rowMatch(m, row, tokens) !== null);
|
|
112
|
+
if (projected)
|
|
113
|
+
rows = rows.map((row) => pickCols(row, projected));
|
|
84
114
|
const decoded = rows.map((r) => decodeTemporal(m, r));
|
|
85
|
-
|
|
115
|
+
if (!withExt)
|
|
116
|
+
return decoded;
|
|
117
|
+
return withExtendsMany(decoded, library, type);
|
|
86
118
|
}
|
|
87
119
|
export async function recordGet(library, type, id) {
|
|
88
120
|
const { m, ename } = resolve(library, type);
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export declare function mintUploadTicket(actor: string): {
|
|
2
|
+
token: string;
|
|
3
|
+
expiresInSec: number;
|
|
4
|
+
};
|
|
5
|
+
export declare function verifyUploadTicket(raw: string): boolean;
|
|
6
|
+
export declare function __resetUploadTickets(): void;
|
|
7
|
+
export declare function __injectUploadTicket(raw: string, exp: number): void;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { generateToken, hashToken } from "./auth-crypto.js";
|
|
2
|
+
const TTL_MS = 60 * 60 * 1000;
|
|
3
|
+
const store = new Map();
|
|
4
|
+
export function mintUploadTicket(actor) {
|
|
5
|
+
const raw = generateToken();
|
|
6
|
+
const exp = Date.now() + TTL_MS;
|
|
7
|
+
store.set(hashToken(raw), { actor, exp });
|
|
8
|
+
const t = setTimeout(() => store.delete(hashToken(raw)), TTL_MS);
|
|
9
|
+
if (typeof t === 'object' && 'unref' in t)
|
|
10
|
+
t.unref();
|
|
11
|
+
return { token: raw, expiresInSec: TTL_MS / 1000 };
|
|
12
|
+
}
|
|
13
|
+
export function verifyUploadTicket(raw) {
|
|
14
|
+
const key = hashToken(raw);
|
|
15
|
+
const entry = store.get(key);
|
|
16
|
+
if (!entry)
|
|
17
|
+
return false;
|
|
18
|
+
if (entry.exp <= Date.now()) {
|
|
19
|
+
store.delete(key);
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
return true;
|
|
23
|
+
}
|
|
24
|
+
export function __resetUploadTickets() {
|
|
25
|
+
store.clear();
|
|
26
|
+
}
|
|
27
|
+
export function __injectUploadTicket(raw, exp) {
|
|
28
|
+
store.set(hashToken(raw), { actor: 'test', exp });
|
|
29
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@coffer-org/server",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.12.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": "^1.4.0",
|
|
28
|
-
"@coffer-org/sdk": "^1.
|
|
28
|
+
"@coffer-org/sdk": "^1.7.0",
|
|
29
29
|
"@extractus/oembed-extractor": "^4.1.0",
|
|
30
30
|
"@fastify/cors": "^11.2.0",
|
|
31
31
|
"@fastify/multipart": "^10.0.0",
|