@coffer-org/server 1.11.0 → 1.13.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 +68 -9
- package/dist/plugin-discovery.d.ts +2 -1
- package/dist/plugin-discovery.js +15 -6
- package/dist/plugin-updates.d.ts +5 -0
- package/dist/plugin-updates.js +10 -0
- 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/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";
|
|
@@ -24,7 +24,7 @@ import { registerMcpHttp } from "./mcp-http.js";
|
|
|
24
24
|
import { registerOAuthApi } from "./oauth-api.js";
|
|
25
25
|
import { baseUrl } from "./public-url.js";
|
|
26
26
|
import { discoverPluginAssets, discoverRuntime } from "./plugin-discovery.js";
|
|
27
|
-
import { checkLatestVersion, resolveUpdateTarget, resolveAllUpdateTargets, resolveRuntimeTarget, runNpmInstall, } from "./plugin-updates.js";
|
|
27
|
+
import { checkLatestVersion, resolveUpdateTarget, resolveAllUpdateTargets, resolveBaseTargets, resolveRuntimeTarget, runNpmInstall, } from "./plugin-updates.js";
|
|
28
28
|
import { buildClientSchema } from "./schema-api.js";
|
|
29
29
|
import { recordList, recordGet, recordCreate, recordUpdate, recordDelete, rowMatch, UnknownTypeError } from "./records-api.js";
|
|
30
30
|
import { maskTree, preserveTree } from "./field-masking.js";
|
|
@@ -133,7 +133,18 @@ app.get('/health', async (_req, reply) => {
|
|
|
133
133
|
return reply.code(503).send({ status: 'db_unavailable' });
|
|
134
134
|
}
|
|
135
135
|
});
|
|
136
|
-
|
|
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
|
+
});
|
|
137
148
|
app.get('/api/counts', async () => {
|
|
138
149
|
const fork = getEm().fork();
|
|
139
150
|
const out = {};
|
|
@@ -147,6 +158,8 @@ app.get('/api/counts', async () => {
|
|
|
147
158
|
}
|
|
148
159
|
return out;
|
|
149
160
|
});
|
|
161
|
+
const SEARCH_SCAN_LIMIT = 1000;
|
|
162
|
+
const searchLog = getLogger('search');
|
|
150
163
|
app.get('/api/search', async (req) => {
|
|
151
164
|
const { q = '', limit = '20' } = req.query;
|
|
152
165
|
const tokens = tokenize(q);
|
|
@@ -164,7 +177,11 @@ app.get('/api/search', async (req) => {
|
|
|
164
177
|
if (!textSearchKeys(mdef).length)
|
|
165
178
|
continue;
|
|
166
179
|
const [library, type] = k.split('/');
|
|
167
|
-
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
|
+
}
|
|
168
185
|
for (const row of rows) {
|
|
169
186
|
const m = rowMatch(mdef, row, tokens);
|
|
170
187
|
if (!m)
|
|
@@ -277,6 +294,26 @@ app.post('/api/plugins/update-all', async (req, reply) => {
|
|
|
277
294
|
reply.send({ ok: true, updated });
|
|
278
295
|
setTimeout(() => process.exit(0), 500);
|
|
279
296
|
});
|
|
297
|
+
app.post('/api/system/update', async (req, reply) => {
|
|
298
|
+
if (!requireAdmin(req, reply))
|
|
299
|
+
return;
|
|
300
|
+
const assets = await discoverPluginAssets();
|
|
301
|
+
const runtime = await discoverRuntime();
|
|
302
|
+
const core = assets.find((a) => a.id === 'core');
|
|
303
|
+
const [coreLatest, runtimeLatest] = await Promise.all([
|
|
304
|
+
core && !core.local ? checkLatestVersion(core.packageName) : Promise.resolve(null),
|
|
305
|
+
runtime && !runtime.local ? checkLatestVersion(runtime.packageName) : Promise.resolve(null),
|
|
306
|
+
]);
|
|
307
|
+
const targets = resolveBaseTargets(assets, runtime, coreLatest, runtimeLatest);
|
|
308
|
+
if (targets.length === 0)
|
|
309
|
+
return reply.code(400).send({ error: 'no_update_available' });
|
|
310
|
+
const result = await runNpmInstall(targets.map((t) => `${t.packageName}@${t.to}`), process.cwd());
|
|
311
|
+
if (!result.ok) {
|
|
312
|
+
return reply.code(500).send({ error: 'install_failed', stderr: result.stderr });
|
|
313
|
+
}
|
|
314
|
+
reply.send({ ok: true, updated: targets.map(({ id, from, to }) => ({ id, from, to })) });
|
|
315
|
+
setTimeout(() => process.exit(0), 500);
|
|
316
|
+
});
|
|
280
317
|
app.get('/api/plugins/:id/settings', async (req, reply) => {
|
|
281
318
|
if (!requireAdmin(req, reply))
|
|
282
319
|
return;
|
|
@@ -363,9 +400,12 @@ function maskRecordRow(library, type, row) {
|
|
|
363
400
|
}
|
|
364
401
|
app.get('/api/:library/:type', async (req, reply) => {
|
|
365
402
|
const { library, type } = req.params;
|
|
366
|
-
const query = req.query;
|
|
403
|
+
const { _fields, _extends, ...query } = req.query;
|
|
367
404
|
try {
|
|
368
|
-
const rows = await recordList(library, type, query
|
|
405
|
+
const rows = await recordList(library, type, query, {
|
|
406
|
+
view: _fields === 'full' ? 'full' : 'list',
|
|
407
|
+
extends: _extends === '1',
|
|
408
|
+
});
|
|
369
409
|
return rows.map((r) => maskRecordRow(library, type, r));
|
|
370
410
|
}
|
|
371
411
|
catch (e) {
|
|
@@ -456,12 +496,31 @@ await registerAuthApi(app);
|
|
|
456
496
|
registerOAuthApi(app);
|
|
457
497
|
await registerMcpHttp(app);
|
|
458
498
|
await registerPluginsApi(app);
|
|
499
|
+
const IMMUTABLE = 'public, max-age=31536000, immutable';
|
|
500
|
+
const isHashedAsset = (p) => /-[A-Za-z0-9_-]{8,}\.(js|css)$/.test(p);
|
|
459
501
|
const WEB_DIST = process.env.WEB_DIST;
|
|
460
502
|
if (WEB_DIST && existsSync(join(WEB_DIST, 'index.html'))) {
|
|
461
|
-
await app.register(fastifyStatic, {
|
|
503
|
+
await app.register(fastifyStatic, {
|
|
504
|
+
root: WEB_DIST,
|
|
505
|
+
prefix: '/',
|
|
506
|
+
decorateReply: false,
|
|
507
|
+
cacheControl: false,
|
|
508
|
+
setHeaders(res, path) {
|
|
509
|
+
const hashed = path.includes(`${sep}assets${sep}`) || isHashedAsset(path);
|
|
510
|
+
res.setHeader('Cache-Control', hashed ? IMMUTABLE : 'no-cache');
|
|
511
|
+
},
|
|
512
|
+
});
|
|
462
513
|
const VENDOR_DIST = join(WEB_DIST, '..', 'dist-vendor');
|
|
463
514
|
if (existsSync(VENDOR_DIST)) {
|
|
464
|
-
await app.register(fastifyStatic, {
|
|
515
|
+
await app.register(fastifyStatic, {
|
|
516
|
+
root: VENDOR_DIST,
|
|
517
|
+
prefix: '/vendor/',
|
|
518
|
+
decorateReply: false,
|
|
519
|
+
cacheControl: false,
|
|
520
|
+
setHeaders(res, path) {
|
|
521
|
+
res.setHeader('Cache-Control', isHashedAsset(path) ? IMMUTABLE : 'no-cache');
|
|
522
|
+
},
|
|
523
|
+
});
|
|
465
524
|
}
|
|
466
525
|
app.setNotFoundHandler((req, reply) => {
|
|
467
526
|
if (req.method === 'GET' &&
|
|
@@ -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/plugin-updates.d.ts
CHANGED
|
@@ -16,6 +16,11 @@ export interface AllUpdateTarget {
|
|
|
16
16
|
to: string;
|
|
17
17
|
}
|
|
18
18
|
export declare function resolveAllUpdateTargets(assets: PluginAssetRecord[], latestById: Map<string, string | null>): AllUpdateTarget[];
|
|
19
|
+
export declare function resolveBaseTargets(assets: PluginAssetRecord[], runtime: {
|
|
20
|
+
packageName: string;
|
|
21
|
+
installedVersion: string;
|
|
22
|
+
local: boolean;
|
|
23
|
+
} | null, coreLatest: string | null, runtimeLatest: string | null): AllUpdateTarget[];
|
|
19
24
|
export interface RuntimeUpdateTarget {
|
|
20
25
|
packageName: string;
|
|
21
26
|
from: string;
|
package/dist/plugin-updates.js
CHANGED
|
@@ -49,6 +49,16 @@ export function resolveAllUpdateTargets(assets, latestById) {
|
|
|
49
49
|
return [{ id: rec.id, packageName: rec.packageName, from: rec.version, to: latest }];
|
|
50
50
|
});
|
|
51
51
|
}
|
|
52
|
+
export function resolveBaseTargets(assets, runtime, coreLatest, runtimeLatest) {
|
|
53
|
+
const core = assets.find((a) => a.id === 'core');
|
|
54
|
+
const rt = resolveRuntimeTarget(runtime, runtimeLatest);
|
|
55
|
+
return [
|
|
56
|
+
...(core && !core.local && coreLatest && coreLatest !== core.version
|
|
57
|
+
? [{ id: 'core', packageName: core.packageName, from: core.version, to: coreLatest }]
|
|
58
|
+
: []),
|
|
59
|
+
...(rt ? [{ id: 'runtime', packageName: rt.packageName, from: rt.from, to: rt.to }] : []),
|
|
60
|
+
];
|
|
61
|
+
}
|
|
52
62
|
export function resolveRuntimeTarget(runtime, latestVersion) {
|
|
53
63
|
if (!runtime || runtime.local)
|
|
54
64
|
return null;
|
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);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@coffer-org/server",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.13.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",
|