@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.
@@ -0,0 +1,2 @@
1
+ export declare const IN_CHUNK = 500;
2
+ export declare function chunk<T>(items: T[], size?: number): T[][];
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
+ }
@@ -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;
@@ -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}`;
@@ -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 {};
@@ -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
- app.get('/api/libraries', () => buildClientSchema());
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 rows = (await fork.find(ename, {}, { limit: 1000 })).map((r) => serialize(r));
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, { root: WEB_DIST, prefix: '/', decorateReply: false });
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, { root: VENDOR_DIST, prefix: '/vendor/', decorateReply: false });
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;
@@ -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 out = { schema: `${base}/schema.js` };
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 urls = resolveAssetUrls(manifest.id, pkg.coffer);
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: pkg.version ?? '0.0.0',
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';
@@ -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;
@@ -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;
@@ -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>): Promise<PluginListEntry[]>;
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>;
@@ -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
  }
@@ -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>>;
@@ -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
- return Promise.all(decoded.map((row) => withExtends(row, library, type)));
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.11.0",
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.6.0",
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",