@coffer-org/server 1.11.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 +47 -8
- 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/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";
|
|
@@ -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)
|
|
@@ -363,9 +380,12 @@ function maskRecordRow(library, type, row) {
|
|
|
363
380
|
}
|
|
364
381
|
app.get('/api/:library/:type', async (req, reply) => {
|
|
365
382
|
const { library, type } = req.params;
|
|
366
|
-
const query = req.query;
|
|
383
|
+
const { _fields, _extends, ...query } = req.query;
|
|
367
384
|
try {
|
|
368
|
-
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
|
+
});
|
|
369
389
|
return rows.map((r) => maskRecordRow(library, type, r));
|
|
370
390
|
}
|
|
371
391
|
catch (e) {
|
|
@@ -456,12 +476,31 @@ await registerAuthApi(app);
|
|
|
456
476
|
registerOAuthApi(app);
|
|
457
477
|
await registerMcpHttp(app);
|
|
458
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);
|
|
459
481
|
const WEB_DIST = process.env.WEB_DIST;
|
|
460
482
|
if (WEB_DIST && existsSync(join(WEB_DIST, 'index.html'))) {
|
|
461
|
-
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
|
+
});
|
|
462
493
|
const VENDOR_DIST = join(WEB_DIST, '..', 'dist-vendor');
|
|
463
494
|
if (existsSync(VENDOR_DIST)) {
|
|
464
|
-
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
|
+
});
|
|
465
504
|
}
|
|
466
505
|
app.setNotFoundHandler((req, reply) => {
|
|
467
506
|
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/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.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",
|