@coffer-org/server 3.4.0 → 5.0.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.
Files changed (101) hide show
  1. package/dist/auth-api.js +4 -5
  2. package/dist/cache-clock.d.ts +8 -0
  3. package/dist/cache-clock.js +103 -0
  4. package/dist/cache-refresh.d.ts +7 -0
  5. package/dist/cache-refresh.js +322 -0
  6. package/dist/collection-io.d.ts +13 -0
  7. package/dist/collection-io.js +40 -29
  8. package/dist/compute-unit.d.ts +66 -0
  9. package/dist/compute-unit.js +369 -0
  10. package/dist/condition-where.js +1 -3
  11. package/dist/data-dir.d.ts +3 -0
  12. package/dist/data-dir.js +39 -0
  13. package/dist/entity-schema.js +5 -2
  14. package/dist/extend-table.d.ts +1 -1
  15. package/dist/extend-table.js +22 -8
  16. package/dist/file-fields.js +50 -23
  17. package/dist/frontend-agent.d.ts +1 -1
  18. package/dist/frontend-agent.js +2 -3
  19. package/dist/global-search.js +1 -1
  20. package/dist/index.js +20 -26
  21. package/dist/mcp-contract/client.d.ts +24 -0
  22. package/dist/mcp-contract/client.js +12 -0
  23. package/dist/mcp-contract/schema.d.ts +46 -0
  24. package/dist/mcp-contract/schema.js +90 -0
  25. package/dist/mcp-contract/tools.d.ts +19 -0
  26. package/dist/mcp-contract/tools.js +160 -0
  27. package/dist/mcp-http.js +4 -2
  28. package/dist/mcp-local.d.ts +1 -1
  29. package/dist/mcp-local.js +1 -1
  30. package/dist/mcp-tools.d.ts +13 -1
  31. package/dist/mcp-tools.js +46 -16
  32. package/dist/migrations.d.ts +5 -0
  33. package/dist/migrations.js +122 -0
  34. package/dist/mutate.js +77 -36
  35. package/dist/orchestrator/agent-capabilities.d.ts +2 -0
  36. package/dist/orchestrator/agent-capabilities.js +201 -0
  37. package/dist/orchestrator/allow.d.ts +16 -0
  38. package/dist/orchestrator/allow.js +65 -0
  39. package/dist/orchestrator/attachments.d.ts +2 -0
  40. package/dist/orchestrator/attachments.js +11 -0
  41. package/dist/orchestrator/config.d.ts +4 -0
  42. package/dist/orchestrator/config.js +19 -0
  43. package/dist/orchestrator/db.d.ts +17 -0
  44. package/dist/orchestrator/db.js +12 -0
  45. package/dist/orchestrator/diagnostics.d.ts +9 -0
  46. package/dist/orchestrator/diagnostics.js +13 -0
  47. package/dist/orchestrator/environment.d.ts +2 -0
  48. package/dist/orchestrator/environment.js +14 -0
  49. package/dist/orchestrator/file-inspection.d.ts +3 -0
  50. package/dist/orchestrator/file-inspection.js +61 -0
  51. package/dist/orchestrator/format.d.ts +1 -0
  52. package/dist/orchestrator/format.js +6 -0
  53. package/dist/orchestrator/index.d.ts +30 -0
  54. package/dist/orchestrator/index.js +56 -0
  55. package/dist/orchestrator/live-message.d.ts +14 -0
  56. package/dist/orchestrator/live-message.js +100 -0
  57. package/dist/orchestrator/pipeline.d.ts +11 -0
  58. package/dist/orchestrator/pipeline.js +198 -0
  59. package/dist/orchestrator/registry.d.ts +13 -0
  60. package/dist/orchestrator/registry.js +74 -0
  61. package/dist/orchestrator/starters.d.ts +19 -0
  62. package/dist/orchestrator/starters.js +81 -0
  63. package/dist/orchestrator/suggestion-capabilities.d.ts +5 -0
  64. package/dist/orchestrator/suggestion-capabilities.js +33 -0
  65. package/dist/orchestrator/system-assembly.d.ts +4 -0
  66. package/dist/orchestrator/system-assembly.js +3 -0
  67. package/dist/orchestrator/system-capabilities.d.ts +2 -0
  68. package/dist/orchestrator/system-capabilities.js +63 -0
  69. package/dist/orchestrator/types.d.ts +141 -0
  70. package/dist/orchestrator/types.js +1 -0
  71. package/dist/part-injection.d.ts +3 -4
  72. package/dist/part-injection.js +25 -368
  73. package/dist/plugin-hooks.d.ts +1 -0
  74. package/dist/plugin-i18n.js +2 -1
  75. package/dist/plugin-runtime.d.ts +2 -2
  76. package/dist/plugin-runtime.js +20 -11
  77. package/dist/plugin-updates.d.ts +2 -2
  78. package/dist/plugin-updates.js +6 -12
  79. package/dist/public-url.js +2 -1
  80. package/dist/recompute-derived.d.ts +2 -1
  81. package/dist/recompute-derived.js +111 -34
  82. package/dist/records-api.js +14 -16
  83. package/dist/registry-context.d.ts +1 -1
  84. package/dist/registry-context.js +3 -0
  85. package/dist/search-index.js +1 -1
  86. package/dist/search-indexer.d.ts +1 -1
  87. package/dist/search-indexer.js +3 -3
  88. package/dist/settings-groups.d.ts +14 -0
  89. package/dist/settings-groups.js +25 -0
  90. package/dist/settings-write.d.ts +4 -4
  91. package/dist/settings-write.js +23 -26
  92. package/dist/system-settings.d.ts +3 -0
  93. package/dist/system-settings.js +27 -0
  94. package/dist/thread-store.d.ts +3 -2
  95. package/dist/thread-store.js +6 -5
  96. package/dist/trigger-queue.d.ts +33 -0
  97. package/dist/trigger-queue.js +432 -0
  98. package/dist/uploads.js +6 -3
  99. package/dist/value-compare.d.ts +1 -0
  100. package/dist/value-compare.js +7 -0
  101. package/package.json +17 -3
@@ -1,4 +1,4 @@
1
- import { composeRegistry } from '@coffer-org/core/compose';
1
+ import { composeRegistry } from '@coffer-org/sdk/compose';
2
2
  import { getLogger } from '@coffer-org/sdk/logger';
3
3
  import { initDb, getOrm, getEm } from "./db.js";
4
4
  import { syncSchema } from "./schema-sync.js";
@@ -6,13 +6,15 @@ import { systemEntities, buildPluginEntities, shelfTableName } from "./entity-sc
6
6
  import { pluginHooks, pluginCtx, HttpError } from "./plugin-hooks.js";
7
7
  import { discoverPlugins, loadServerHooks } from "./plugin-discovery.js";
8
8
  import { selectRows } from "./read-rows.js";
9
- import { runMigrations, assertSafeRequired, renameSystemShelfKey } from "./migrations.js";
9
+ import { runMigrations, assertSafeRequired, runSystemMigrations } from "./migrations.js";
10
10
  import { runSeeds } from "./seeds.js";
11
11
  import { setActiveRegistry } from "./registry-context.js";
12
12
  import { migrateEmbeddingVectorsToBlob } from "./embeddings.js";
13
13
  import { ensureSearchTable } from "./search-index.js";
14
14
  import { startScheduler, stopScheduler } from "./background-scheduler.js";
15
15
  import { startSearchIndexer, indexSearchOnce } from "./search-indexer.js";
16
+ import { startOrchestrator, stopOrchestrator, orchestratorStartersTask } from "./orchestrator/index.js";
17
+ import { SYSTEM_SETTINGS_ID, SYSTEM_SETTINGS } from "./system-settings.js";
16
18
  const log = getLogger('plugins');
17
19
  let stopSearchIndexer;
18
20
  let _plugins = null;
@@ -22,9 +24,7 @@ export async function getPlugins() {
22
24
  export async function readDisabled() {
23
25
  const fork = getEm().fork();
24
26
  const rows = await fork.find('_Plugin', { enabled: 0 });
25
- const disabled = new Set(rows.map((r) => r.id));
26
- disabled.delete('core');
27
- return disabled;
27
+ return new Set(rows.map((r) => r.id));
28
28
  }
29
29
  let _disabled = null;
30
30
  export function getDisabled() {
@@ -33,10 +33,10 @@ export function getDisabled() {
33
33
  throw e;
34
34
  }));
35
35
  }
36
- export async function getPluginSettings(pluginId) {
36
+ export async function getPluginSettings(groupId) {
37
37
  try {
38
38
  const fork = getEm().fork();
39
- const [row] = await selectRows(fork, `_settings__${pluginId}`, { plugin_id: pluginId }, { limit: 1 });
39
+ const [row] = await selectRows(fork, `_settings__${groupId}`, { plugin_id: groupId }, { limit: 1 });
40
40
  if (!row)
41
41
  return {};
42
42
  return row;
@@ -61,6 +61,12 @@ export async function requireSettings(pluginId, keys) {
61
61
  }
62
62
  return out;
63
63
  }
64
+ function systemSettingsGroupInput() {
65
+ return { id: SYSTEM_SETTINGS_ID, fields: SYSTEM_SETTINGS.fields };
66
+ }
67
+ async function composeAll(disabled) {
68
+ return composeRegistry(await getPlugins(), { disabled, settingsGroups: [systemSettingsGroupInput()] });
69
+ }
64
70
  async function seedPluginRows() {
65
71
  const fork = getEm().fork();
66
72
  const existing = new Set((await fork.find('_Plugin', {})).map((r) => r.id));
@@ -74,8 +80,9 @@ async function seedPluginRows() {
74
80
  }
75
81
  export async function initStorage() {
76
82
  await initDb(systemEntities);
77
- await renameSystemShelfKey(getEm().fork());
83
+ await runSystemMigrations(getEm().fork());
78
84
  await syncSchema();
85
+ await runSystemMigrations(getEm().fork());
79
86
  await migrateEmbeddingVectorsToBlob();
80
87
  await getEm().fork().getConnection().execute('DROP TABLE IF EXISTS "_folders"');
81
88
  await seedPluginRows();
@@ -98,11 +105,12 @@ export async function initStorage() {
98
105
  }
99
106
  export async function initPlugins() {
100
107
  const disabled = await initStorage();
101
- const reg = composeRegistry(await getPlugins(), { disabled });
108
+ const reg = await composeAll(disabled);
102
109
  setActiveRegistry(reg);
103
110
  Object.assign(pluginHooks, await loadServerHooks());
104
111
  await runSeeds({ em: getEm().fork(), plugins: reg.order, hooks: pluginHooks });
105
- const bgTasks = [];
112
+ startOrchestrator();
113
+ const bgTasks = [orchestratorStartersTask];
106
114
  for (const p of reg.order) {
107
115
  const h = pluginHooks[p.id];
108
116
  try {
@@ -139,7 +147,7 @@ export async function teardownPlugins() {
139
147
  stopScheduler();
140
148
  Object.assign(pluginHooks, await loadServerHooks());
141
149
  const disabled = await readDisabled();
142
- const reg = composeRegistry(await getPlugins(), { disabled });
150
+ const reg = await composeAll(disabled);
143
151
  for (const p of [...reg.order].reverse()) {
144
152
  const h = pluginHooks[p.id];
145
153
  if (!h?.teardown)
@@ -152,6 +160,7 @@ export async function teardownPlugins() {
152
160
  log.warn(`${p.id}: teardown ✗ ${e.message}`);
153
161
  }
154
162
  }
163
+ stopOrchestrator();
155
164
  }
156
165
  export async function teardownPlugin(id) {
157
166
  const h = pluginHooks[id];
@@ -18,11 +18,11 @@ export interface AllUpdateTarget {
18
18
  to: string;
19
19
  }
20
20
  export declare function resolveAllUpdateTargets(assets: PluginAssetRecord[], latestById: Map<string, string | null>): AllUpdateTarget[];
21
- export declare function resolveBaseTargets(assets: PluginAssetRecord[], runtime: {
21
+ export declare function resolveBaseTargets(runtime: {
22
22
  packageName: string;
23
23
  installedVersion: string;
24
24
  local: boolean;
25
- } | null, coreLatest: string | null, runtimeLatest: string | null): AllUpdateTarget[];
25
+ } | null, runtimeLatest: string | null): AllUpdateTarget[];
26
26
  export interface RuntimeUpdateTarget {
27
27
  packageName: string;
28
28
  from: string;
@@ -1,8 +1,8 @@
1
1
  import { execFile } from 'node:child_process';
2
- import { isNewerVersion } from '@coffer-org/core/version';
2
+ import { isNewerVersion } from '@coffer-org/sdk/version';
3
3
  import { getPluginState, setPluginState } from "./plugin-state.js";
4
4
  const TTL_MS = 60 * 60 * 1000;
5
- const STATE_PLUGIN = 'core';
5
+ const STATE_NAMESPACE = 'system';
6
6
  const stateKey = (pkgName) => `update-check:${pkgName}`;
7
7
  function readCache(raw) {
8
8
  if (!raw)
@@ -18,7 +18,7 @@ export async function checkLatestVersion(pkgName, opts = {}) {
18
18
  const key = stateKey(pkgName);
19
19
  let cached = null;
20
20
  try {
21
- cached = readCache(await getPluginState(STATE_PLUGIN, key));
21
+ cached = readCache(await getPluginState(STATE_NAMESPACE, key));
22
22
  if (!opts.force && cached && Date.now() - cached.checkedAt < TTL_MS)
23
23
  return cached.latestVersion;
24
24
  const res = await fetch(`https://registry.npmjs.org/${pkgName}/latest`, { signal: AbortSignal.timeout(5000) });
@@ -27,7 +27,7 @@ export async function checkLatestVersion(pkgName, opts = {}) {
27
27
  const data = (await res.json());
28
28
  if (!data.version)
29
29
  return cached?.latestVersion ?? null;
30
- await setPluginState(STATE_PLUGIN, key, JSON.stringify({ latestVersion: data.version, checkedAt: Date.now() }));
30
+ await setPluginState(STATE_NAMESPACE, key, JSON.stringify({ latestVersion: data.version, checkedAt: Date.now() }));
31
31
  return data.version;
32
32
  }
33
33
  catch {
@@ -51,15 +51,9 @@ export function resolveAllUpdateTargets(assets, latestById) {
51
51
  return [{ id: rec.id, packageName: rec.packageName, from: rec.version, to: latest }];
52
52
  });
53
53
  }
54
- export function resolveBaseTargets(assets, runtime, coreLatest, runtimeLatest) {
55
- const core = assets.find((a) => a.id === 'core');
54
+ export function resolveBaseTargets(runtime, runtimeLatest) {
56
55
  const rt = resolveRuntimeTarget(runtime, runtimeLatest);
57
- return [
58
- ...(core && !core.local && coreLatest && isNewerVersion(coreLatest, core.version)
59
- ? [{ id: 'core', packageName: core.packageName, from: core.version, to: coreLatest }]
60
- : []),
61
- ...(rt ? [{ id: 'runtime', packageName: rt.packageName, from: rt.from, to: rt.to }] : []),
62
- ];
56
+ return rt ? [{ id: 'runtime', packageName: rt.packageName, from: rt.from, to: rt.to }] : [];
63
57
  }
64
58
  export function resolveRuntimeTarget(runtime, latestVersion) {
65
59
  if (!runtime || runtime.local)
@@ -1,4 +1,5 @@
1
1
  import { getPluginSettings } from "./plugin-runtime.js";
2
+ import { SYSTEM_SETTINGS_ID } from "./system-settings.js";
2
3
  const CACHE_TTL_MS = 30_000;
3
4
  let cached = null;
4
5
  export function invalidatePublicUrlCache() {
@@ -12,7 +13,7 @@ async function configuredBase() {
12
13
  return cached.value;
13
14
  let value = '';
14
15
  try {
15
- value = normalize((await getPluginSettings('core'))['publicUrl']);
16
+ value = normalize((await getPluginSettings(SYSTEM_SETTINGS_ID))['publicUrl']);
16
17
  }
17
18
  catch {
18
19
  value = '';
@@ -1,6 +1,7 @@
1
1
  import type { ShelfDef } from '@coffer-org/sdk/shelf';
2
+ import type { ExtendDef } from '@coffer-org/sdk/extend';
2
3
  export interface RecomputeResult {
3
4
  scanned: number;
4
5
  changed: number;
5
6
  }
6
- export declare function recomputeDerivedFields(m: ShelfDef, entityName: string): Promise<RecomputeResult>;
7
+ export declare function recomputeDerivedFields(m: ShelfDef, entityName: string, extends_?: ExtendDef[]): Promise<RecomputeResult>;
@@ -1,60 +1,137 @@
1
1
  import { collectionGroups } from '@coffer-org/sdk/shelf';
2
- import { derivedEntries, applyDerived } from '@coffer-org/sdk/derive';
3
2
  import { applyStoredParts, hasStoredPartWork } from "./part-injection.js";
3
+ import { extendEntityName } from "./extend-table.js";
4
4
  import { getEm } from "./db.js";
5
5
  import { selectRows } from "./read-rows.js";
6
- import { encodeJson } from "./mutate.js";
7
- import { encodeTemporal, decodeTemporal } from "./temporal.js";
8
- import { flattenEmbedded, nestEmbedded, readCollections } from "./collection-io.js";
9
- function sameValue(a, b) {
10
- if (a instanceof Date && b instanceof Date)
11
- return a.getTime() === b.getTime();
12
- if (a instanceof Date || b instanceof Date)
13
- return false;
14
- return a === b;
15
- }
6
+ import { encodeJson, encodeJsonAt } from "./mutate.js";
7
+ import { encodeTemporal, decodeTemporal, encodeTemporalAt, decodeTemporalAt } from "./temporal.js";
8
+ import { flattenEmbedded, nestEmbedded, readCollectionsWithMeta, flattenEmbeddedAt, nestEmbeddedAt, readAtWithMeta, } from "./collection-io.js";
9
+ import { levelClockColumnsByOwner } from "./cache-clock.js";
10
+ import { sameValue } from "./value-compare.js";
16
11
  function partCtx(m) {
17
12
  return { settings: {}, global: {}, meta: { library: m.library, shelf: m.shelf } };
18
13
  }
19
- function changedTopLevel(m, before, after) {
20
- const collKeys = new Set(collectionGroups(m.fields).map((c) => c.key));
14
+ function extendCtx(e) {
15
+ return { settings: {}, global: {}, meta: { library: 'extend', shelf: e.id } };
16
+ }
17
+ function changedKeys(fields, before, after) {
18
+ const collKeys = new Set(collectionGroups(fields).map((c) => c.key));
19
+ const clocksByOwner = levelClockColumnsByOwner(fields);
20
+ const clockKeys = new Set([...clocksByOwner.values()].flat());
21
21
  const out = {};
22
22
  for (const [k, v] of Object.entries(after)) {
23
- if (collKeys.has(k))
23
+ if (collKeys.has(k) || clockKeys.has(k))
24
24
  continue;
25
25
  if (JSON.stringify(before[k] ?? null) !== JSON.stringify(v ?? null))
26
26
  out[k] = v;
27
27
  }
28
+ for (const [owner, cols] of clocksByOwner) {
29
+ if (!(owner in out))
30
+ continue;
31
+ for (const col of cols) {
32
+ if (after[col] !== undefined)
33
+ out[col] = after[col];
34
+ }
35
+ }
28
36
  return out;
29
37
  }
30
- export async function recomputeDerivedFields(m, entityName) {
31
- const storedParts = hasStoredPartWork(m.fields);
32
- if (derivedEntries(m.fields).length === 0 && !storedParts)
38
+ async function recomputeCollectionRows(em, tablePrefix, fields, before, after, meta) {
39
+ let anyChanged = false;
40
+ for (const c of collectionGroups(fields)) {
41
+ const table = `${tablePrefix}__${c.key}`;
42
+ const childFields = c.group.fields;
43
+ const beforeRows = before[c.key] ?? [];
44
+ const afterRows = after[c.key] ?? [];
45
+ const metas = meta[c.key] ?? [];
46
+ for (let i = 0; i < beforeRows.length; i++) {
47
+ const rm = metas[i];
48
+ const b = beforeRows[i];
49
+ const a = afterRows[i];
50
+ if (!rm || !b || !a)
51
+ continue;
52
+ const patch = changedKeys(childFields, b, a);
53
+ if (Object.keys(patch).length > 0) {
54
+ const encodedPatch = encodeJsonAt(childFields, flattenEmbeddedAt(childFields, patch));
55
+ const encodedBefore = encodeJsonAt(childFields, flattenEmbeddedAt(childFields, b));
56
+ const dirty = {};
57
+ for (const [k, v] of Object.entries(encodedPatch)) {
58
+ if (!sameValue(encodedBefore[k], v))
59
+ dirty[k] = v;
60
+ }
61
+ if (Object.keys(dirty).length > 0) {
62
+ await em.nativeUpdate(table, { id: rm.id }, dirty);
63
+ anyChanged = true;
64
+ }
65
+ }
66
+ if (await recomputeCollectionRows(em, table, childFields, b, a, rm.nested))
67
+ anyChanged = true;
68
+ }
69
+ }
70
+ return anyChanged;
71
+ }
72
+ async function recomputeExtendRow(em, e, baseId) {
73
+ const table = extendEntityName(e);
74
+ const [flat] = (await selectRows(em, table, { base_id: baseId }, { limit: 1 }));
75
+ if (!flat)
76
+ return false;
77
+ const decoded = nestEmbeddedAt(e.fields, decodeTemporalAt(e.fields, flat));
78
+ const { collections, meta } = await readAtWithMeta(em, table, e.fields, baseId);
79
+ const hydrated = { ...decoded, ...collections };
80
+ const withParts = await applyStoredParts(e.fields, hydrated, extendCtx(e));
81
+ let changed = false;
82
+ const patch = changedKeys(e.fields, hydrated, withParts);
83
+ if (Object.keys(patch).length > 0) {
84
+ const encoded = encodeJsonAt(e.fields, encodeTemporalAt(e.fields, flattenEmbeddedAt(e.fields, patch)));
85
+ const dirty = {};
86
+ for (const [k, v] of Object.entries(encoded)) {
87
+ if (!sameValue(flat[k], v))
88
+ dirty[k] = v;
89
+ }
90
+ if (Object.keys(dirty).length > 0) {
91
+ await em.nativeUpdate(table, { base_id: baseId }, dirty);
92
+ changed = true;
93
+ }
94
+ }
95
+ if (await recomputeCollectionRows(em, table, e.fields, collections, withParts, meta))
96
+ changed = true;
97
+ return changed;
98
+ }
99
+ export async function recomputeDerivedFields(m, entityName, extends_ = []) {
100
+ const hasExtendWork = extends_.some((e) => hasStoredPartWork(e.fields));
101
+ if (!hasStoredPartWork(m.fields) && !hasExtendWork)
33
102
  return { scanned: 0, changed: 0 };
34
103
  const fork = getEm().fork();
35
104
  const rows = await selectRows(fork, entityName, {}, { orderBy: { id: 'asc' } });
36
- let changed = 0;
105
+ const changedIds = new Set();
37
106
  for (const row of rows) {
38
107
  const id = row['id'];
39
108
  const flat = decodeTemporal(m, row);
40
109
  const nested = nestEmbedded(m, flat);
41
- const collections = await readCollections(fork, m, id);
110
+ const { collections, meta } = await readCollectionsWithMeta(fork, m, id);
42
111
  const hydrated = { ...nested, ...collections };
43
- const withParts = storedParts ? await applyStoredParts(m.fields, hydrated, partCtx(m)) : hydrated;
44
- const partPatch = storedParts ? changedTopLevel(m, hydrated, withParts) : {};
45
- const derived = await applyDerived(m.fields, withParts);
46
- if (Object.keys(derived).length === 0 && Object.keys(partPatch).length === 0)
47
- continue;
48
- const encoded = encodeJson(m, encodeTemporal(m, flattenEmbedded(m, { ...partPatch, ...derived })));
49
- const dirty = {};
50
- for (const [k, v] of Object.entries(encoded)) {
51
- if (!sameValue(row[k], v))
52
- dirty[k] = v;
112
+ const withParts = await applyStoredParts(m.fields, hydrated, partCtx(m));
113
+ const partPatch = changedKeys(m.fields, hydrated, withParts);
114
+ if (Object.keys(partPatch).length > 0) {
115
+ const encoded = encodeJson(m, encodeTemporal(m, flattenEmbedded(m, partPatch)));
116
+ const dirty = {};
117
+ for (const [k, v] of Object.entries(encoded)) {
118
+ if (!sameValue(row[k], v))
119
+ dirty[k] = v;
120
+ }
121
+ if (Object.keys(dirty).length > 0) {
122
+ await fork.nativeUpdate(entityName, { id }, dirty);
123
+ changedIds.add(id);
124
+ }
125
+ }
126
+ if (await recomputeCollectionRows(fork, entityName, m.fields, collections, withParts, meta)) {
127
+ changedIds.add(id);
128
+ }
129
+ for (const e of extends_) {
130
+ if (!hasStoredPartWork(e.fields))
131
+ continue;
132
+ if (await recomputeExtendRow(fork, e, id))
133
+ changedIds.add(id);
53
134
  }
54
- if (Object.keys(dirty).length === 0)
55
- continue;
56
- await fork.nativeUpdate(entityName, { id }, dirty);
57
- changed += 1;
58
135
  }
59
- return { scanned: rows.length, changed };
136
+ return { scanned: rows.length, changed: changedIds.size };
60
137
  }
@@ -1,5 +1,5 @@
1
- import { fieldMap, textSearchKeys, titleKey, recordTitle, listKeys, storageColumnsFor, magnitudeSub, } from '@coffer-org/sdk/shelf';
2
- import { tokenize, matchScoreFolded, foldText } from '@coffer-org/core/search';
1
+ import { fieldMap, textSearchKeys, titleKey, recordTitle, listKeys, storageColumnsFor, magnitudeSub, resolveColumnKey, } from '@coffer-org/sdk/shelf';
2
+ import { tokenize, matchScoreFolded, foldText } from '@coffer-org/sdk/search';
3
3
  import { getActiveRegistry, getShelf, getExtendsFor } from "./registry-context.js";
4
4
  import { selectRows } from "./read-rows.js";
5
5
  import { shelfTableName } from "./entity-schema.js";
@@ -10,6 +10,7 @@ import { splitBody, saveExtends, deleteExtends, validateExtends, readExtends } f
10
10
  import { createRecord, updateRecord, getRecord, deleteRecord, restoreRecord, purgeRecord } from "./mutate.js";
11
11
  import { newPartMemo } from "./part-injection.js";
12
12
  import { deleteRecordActivity } from "./record-activity.js";
13
+ import { stripClockKeys } from "./cache-clock.js";
13
14
  export class UnknownShelfError extends Error {
14
15
  }
15
16
  export class SingleShelfError extends Error {
@@ -95,26 +96,19 @@ export class FilterError extends Error {
95
96
  function resolveFilterKey(m, fm, k) {
96
97
  const direct = fm[k];
97
98
  if (direct) {
98
- if (direct.virtual)
99
- return undefined;
100
99
  if (!direct.columns)
101
100
  return { column: k, type: direct.column };
102
101
  const sub = magnitudeSub(direct);
103
102
  if (sub === undefined)
104
- throw new FilterError(`${m.library}/${m.shelf}: '${k}' is a composite whose parts are all computed or pinned — it owns no column to filter on`);
103
+ throw new FilterError(`${m.library}/${m.shelf}: '${k}' is a composite with no stored part — it owns no column to filter on`);
105
104
  return { column: `${k}__${sub}`, type: direct.columns[sub] };
106
105
  }
107
- const cut = k.lastIndexOf('__');
108
- if (cut <= 0)
106
+ const owner = resolveColumnKey(fm, k);
107
+ if (!owner)
109
108
  return undefined;
110
- const base = fm[k.slice(0, cut)];
111
- if (!base?.columns)
112
- return undefined;
113
- const part = k.slice(cut + 2);
114
- const type = base.columns[part];
115
- if (type === undefined)
116
- throw new FilterError(`${m.library}/${m.shelf}: '${k}' names no part of composite '${k.slice(0, cut)}' (parts: ${Object.keys(base.columns).join(', ')})`);
117
- return { column: k, type };
109
+ if (owner.kind === 'unknown-sub')
110
+ throw new FilterError(`${m.library}/${m.shelf}: '${k}' names no part of composite '${owner.key}' (parts: ${Object.keys(owner.field.columns).join(', ')})`);
111
+ return { column: k, type: owner.field.columns[owner.sub] };
118
112
  }
119
113
  function buildWhere(m, filterParams) {
120
114
  const where = {};
@@ -179,8 +173,12 @@ async function listRecords(library, shelf, query = {}, opts = {}) {
179
173
  let rows = await selectRows(fork, ename, where, findOpts);
180
174
  if (tokens.length > 0)
181
175
  rows = rows.filter((row) => rowMatch(m, row, tokens) !== null);
182
- if (projected)
176
+ if (projected) {
183
177
  rows = rows.map((row) => pickCols(row, projected));
178
+ }
179
+ else {
180
+ rows = rows.map((row) => stripClockKeys(row));
181
+ }
184
182
  const decoded = rows.map((r) => decodeTemporal(m, r));
185
183
  if (!withExt)
186
184
  return decoded;
@@ -1,4 +1,4 @@
1
- import type { Registry } from '@coffer-org/core/compose';
1
+ import type { Registry } from '@coffer-org/sdk/compose';
2
2
  export declare function setActiveRegistry(reg: Registry): void;
3
3
  export declare function getActiveRegistry(): Registry;
4
4
  export declare const getShelf: Registry['getShelf'];
@@ -1,5 +1,8 @@
1
+ import { buildMutateGraph } from '@coffer-org/sdk/mutate-graph';
1
2
  let active = null;
2
3
  export function setActiveRegistry(reg) {
4
+ if (!reg.mutateGraph)
5
+ reg.mutateGraph = buildMutateGraph(reg);
3
6
  active = reg;
4
7
  }
5
8
  function current() {
@@ -1,5 +1,5 @@
1
1
  import { textSearchKeys, titleKey } from '@coffer-org/sdk/shelf';
2
- import { foldText } from '@coffer-org/core/search';
2
+ import { foldText } from '@coffer-org/sdk/search';
3
3
  import { getLogger } from '@coffer-org/sdk/logger';
4
4
  import { getEm } from "./db.js";
5
5
  import { getDialect } from "./dialect.js";
@@ -1,4 +1,4 @@
1
- export declare const SEARCH_STATE_PLUGIN = "core";
1
+ export declare const SEARCH_STATE_NAMESPACE = "system";
2
2
  export declare const SEARCH_CURSOR_KEY = "fts_last_event_id";
3
3
  export declare function indexSearchOnce(batch?: number): Promise<number>;
4
4
  export declare function startSearchIndexer(batch?: number): () => void;
@@ -5,13 +5,13 @@ import { getShelf } from "./registry-context.js";
5
5
  import { onRecordsChanged } from "./index-signal.js";
6
6
  import { ftsAvailable, foldedTextFor, upsertSearchRow, deleteSearchRow } from "./search-index.js";
7
7
  const log = getLogger('search-indexer');
8
- export const SEARCH_STATE_PLUGIN = 'core';
8
+ export const SEARCH_STATE_NAMESPACE = 'system';
9
9
  export const SEARCH_CURSOR_KEY = 'fts_last_event_id';
10
10
  const DEFAULT_BATCH = 5000;
11
11
  export async function indexSearchOnce(batch = DEFAULT_BATCH) {
12
12
  if (!ftsAvailable())
13
13
  return 0;
14
- const last = Number((await getPluginState(SEARCH_STATE_PLUGIN, SEARCH_CURSOR_KEY)) ?? '0');
14
+ const last = Number((await getPluginState(SEARCH_STATE_NAMESPACE, SEARCH_CURSOR_KEY)) ?? '0');
15
15
  const rows = await listEventsSince(last, batch);
16
16
  if (rows.length === 0)
17
17
  return 0;
@@ -52,7 +52,7 @@ export async function indexSearchOnce(batch = DEFAULT_BATCH) {
52
52
  await upsertSearchRow(p.shelf, p.recordId, p.folded);
53
53
  }
54
54
  const lastId = rows[rows.length - 1].id;
55
- await setPluginState(SEARCH_STATE_PLUGIN, SEARCH_CURSOR_KEY, String(lastId));
55
+ await setPluginState(SEARCH_STATE_NAMESPACE, SEARCH_CURSOR_KEY, String(lastId));
56
56
  log.debug(`indexed ${byRef.size} record(s) from ${rows.length} event(s), cursor → ${lastId}`);
57
57
  return rows.length;
58
58
  }
@@ -0,0 +1,14 @@
1
+ import type { LayoutEl } from '@coffer-org/sdk/fields';
2
+ import type { PluginManifest } from '@coffer-org/sdk/plugin';
3
+ import { SYSTEM_SETTINGS_ID } from './system-settings.ts';
4
+ export interface SettingsGroup {
5
+ id: string;
6
+ label: string;
7
+ fields: LayoutEl[];
8
+ }
9
+ export { SYSTEM_SETTINGS_ID };
10
+ export declare function groupsFromPlugins(plugins: PluginManifest[]): SettingsGroup[];
11
+ export declare function systemSettingsGroup(): SettingsGroup;
12
+ export declare function groupsWithSystem(plugins: PluginManifest[]): SettingsGroup[];
13
+ export declare function settingsGroups(): Promise<SettingsGroup[]>;
14
+ export declare function findSettingsGroup(id: string): Promise<SettingsGroup | undefined>;
@@ -0,0 +1,25 @@
1
+ import { getPlugins } from "./plugin-runtime.js";
2
+ import { SYSTEM_SETTINGS_ID, SYSTEM_SETTINGS } from "./system-settings.js";
3
+ export { SYSTEM_SETTINGS_ID };
4
+ export function groupsFromPlugins(plugins) {
5
+ return plugins
6
+ .filter((p) => p.settings && p.settings.fields.length > 0)
7
+ .map((p) => ({ id: p.id, label: p.settings.label, fields: p.settings.fields }));
8
+ }
9
+ export function systemSettingsGroup() {
10
+ return { id: SYSTEM_SETTINGS_ID, label: SYSTEM_SETTINGS.label, fields: SYSTEM_SETTINGS.fields };
11
+ }
12
+ export function groupsWithSystem(plugins) {
13
+ const fromPlugins = groupsFromPlugins(plugins);
14
+ const clash = fromPlugins.find((g) => g.id === SYSTEM_SETTINGS_ID);
15
+ if (clash) {
16
+ throw new Error(`[settings] a plugin declares settings under the reserved group id '${SYSTEM_SETTINGS_ID}'`);
17
+ }
18
+ return [systemSettingsGroup(), ...fromPlugins];
19
+ }
20
+ export async function settingsGroups() {
21
+ return groupsWithSystem(await getPlugins());
22
+ }
23
+ export async function findSettingsGroup(id) {
24
+ return (await settingsGroups()).find((g) => g.id === id);
25
+ }
@@ -9,10 +9,10 @@ export interface SettingsFieldInfo {
9
9
  options?: string[];
10
10
  }
11
11
  export declare function describeSettingsFields(fields: LayoutEl[]): SettingsFieldInfo[];
12
- export declare function buildSettingsBody(pluginId: string, fields: LayoutEl[], incoming: Record<string, unknown>, existing: Record<string, unknown>): Record<string, unknown>;
13
- export declare function writePluginSettings(em: EntityManager, pluginId: string, incoming: Record<string, unknown>, actor: string, plugins?: PluginManifest[]): Promise<Record<string, unknown>>;
14
- export declare function listSettings(plugins?: PluginManifest[]): Promise<{
15
- plugin: string;
12
+ export declare function buildSettingsBody(groupId: string, fields: LayoutEl[], incoming: Record<string, unknown>, existing: Record<string, unknown>, label?: string): Record<string, unknown>;
13
+ export declare function writePluginSettings(em: EntityManager, groupId: string, incoming: Record<string, unknown>, actor: string, pluginsOverride?: PluginManifest[]): Promise<Record<string, unknown>>;
14
+ export declare function listSettings(pluginsOverride?: PluginManifest[]): Promise<{
15
+ group: string;
16
16
  label: string;
17
17
  fields: SettingsFieldInfo[];
18
18
  values: Record<string, unknown>;
@@ -1,8 +1,9 @@
1
1
  import { fieldEntries, buildZodObject } from '@coffer-org/sdk/shelf';
2
2
  import { preserveTree, maskTree } from "./field-masking.js";
3
3
  import { ValidationError, NotFoundError, toIssue } from "./mutate.js";
4
- import { getPlugins, getPluginSettings } from "./plugin-runtime.js";
4
+ import { getPluginSettings } from "./plugin-runtime.js";
5
5
  import { invalidatePublicUrlCache } from "./public-url.js";
6
+ import { groupsFromPlugins, settingsGroups, findSettingsGroup, SYSTEM_SETTINGS_ID } from "./settings-groups.js";
6
7
  export function describeSettingsFields(fields) {
7
8
  return fieldEntries(fields).map(([key, f]) => ({
8
9
  key,
@@ -12,38 +13,35 @@ export function describeSettingsFields(fields) {
12
13
  ...(f.options ? { options: f.options.map((o) => o.value) } : {}),
13
14
  }));
14
15
  }
15
- function settingsShelf(pluginId, fields) {
16
- return { library: '_settings', shelf: pluginId, label: `${pluginId}.plugin.label`, fields };
16
+ function settingsShelf(groupId, fields, label = `${groupId}.plugin.label`) {
17
+ return { library: '_settings', shelf: groupId, label, fields };
17
18
  }
18
- export function buildSettingsBody(pluginId, fields, incoming, existing) {
19
+ export function buildSettingsBody(groupId, fields, incoming, existing, label) {
19
20
  const body = preserveTree(fields, incoming, existing);
20
- const parsed = buildZodObject(settingsShelf(pluginId, fields)).safeParse(body);
21
+ const parsed = buildZodObject(settingsShelf(groupId, fields, label)).safeParse(body);
21
22
  if (!parsed.success)
22
23
  throw new ValidationError(parsed.error.issues.map(toIssue));
23
24
  return parsed.data;
24
25
  }
25
- function withSettings(plugins) {
26
- return plugins.filter((p) => p.settings && p.settings.fields.length > 0);
27
- }
28
- export async function writePluginSettings(em, pluginId, incoming, actor, plugins) {
29
- const all = plugins ?? (await getPlugins());
30
- const p = all.find((x) => x.id === pluginId);
31
- if (!p || !p.settings || p.settings.fields.length === 0) {
26
+ export async function writePluginSettings(em, groupId, incoming, actor, pluginsOverride) {
27
+ const group = pluginsOverride
28
+ ? groupsFromPlugins(pluginsOverride).find((g) => g.id === groupId)
29
+ : await findSettingsGroup(groupId);
30
+ if (!group)
32
31
  throw new NotFoundError();
33
- }
34
- const fields = p.settings.fields;
35
- const existing = await getPluginSettings(pluginId);
36
- const data = buildSettingsBody(pluginId, fields, incoming, existing);
37
- const row = { plugin_id: pluginId, ...data };
38
- await em.upsert(`_settings__${pluginId}`, row);
39
- if (pluginId === 'core')
32
+ const fields = group.fields;
33
+ const existing = await getPluginSettings(groupId);
34
+ const data = buildSettingsBody(groupId, fields, incoming, existing, group.label);
35
+ const row = { plugin_id: groupId, ...data };
36
+ await em.upsert(`_settings__${groupId}`, row);
37
+ if (groupId === SYSTEM_SETTINGS_ID)
40
38
  invalidatePublicUrlCache();
41
39
  const masked = maskTree(fields, row);
42
40
  em.create('_Event', {
43
41
  ts: new Date().toISOString(),
44
42
  actor,
45
43
  op: 'settings',
46
- type: pluginId,
44
+ type: groupId,
47
45
  record_id: null,
48
46
  before: JSON.stringify(maskTree(fields, existing)),
49
47
  after: JSON.stringify(masked),
@@ -51,13 +49,12 @@ export async function writePluginSettings(em, pluginId, incoming, actor, plugins
51
49
  await em.flush();
52
50
  return masked;
53
51
  }
54
- export async function listSettings(plugins) {
55
- const all = withSettings(plugins ?? (await getPlugins()));
52
+ export async function listSettings(pluginsOverride) {
53
+ const groups = pluginsOverride ? groupsFromPlugins(pluginsOverride) : await settingsGroups();
56
54
  const out = [];
57
- for (const p of all) {
58
- const fields = p.settings.fields;
59
- const values = maskTree(fields, await getPluginSettings(p.id));
60
- out.push({ plugin: p.id, label: p.settings.label, fields: describeSettingsFields(fields), values });
55
+ for (const g of groups) {
56
+ const values = maskTree(g.fields, await getPluginSettings(g.id));
57
+ out.push({ group: g.id, label: g.label, fields: describeSettingsFields(g.fields), values });
61
58
  }
62
59
  return out;
63
60
  }
@@ -0,0 +1,3 @@
1
+ import { type SettingsDef } from '@coffer-org/sdk/settings';
2
+ export declare const SYSTEM_SETTINGS_ID = "system";
3
+ export declare const SYSTEM_SETTINGS: SettingsDef;