@coffer-org/server 3.3.0 → 4.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 (112) 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 +42 -28
  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/db.js +2 -0
  14. package/dist/entity-schema.d.ts +1 -0
  15. package/dist/entity-schema.js +25 -2
  16. package/dist/extend-io.d.ts +3 -2
  17. package/dist/extend-io.js +4 -4
  18. package/dist/extend-table.d.ts +3 -2
  19. package/dist/extend-table.js +48 -11
  20. package/dist/file-fields.js +50 -23
  21. package/dist/global-search.js +1 -1
  22. package/dist/http-errors.d.ts +7 -0
  23. package/dist/http-errors.js +24 -0
  24. package/dist/index.js +31 -35
  25. package/dist/mcp-contract/client.d.ts +24 -0
  26. package/dist/mcp-contract/client.js +12 -0
  27. package/dist/mcp-contract/schema.d.ts +46 -0
  28. package/dist/mcp-contract/schema.js +90 -0
  29. package/dist/mcp-contract/tools.d.ts +19 -0
  30. package/dist/mcp-contract/tools.js +160 -0
  31. package/dist/mcp-local.d.ts +1 -1
  32. package/dist/mcp-local.js +4 -2
  33. package/dist/mcp-tools.d.ts +12 -1
  34. package/dist/mcp-tools.js +41 -8
  35. package/dist/migrations.d.ts +30 -2
  36. package/dist/migrations.js +368 -19
  37. package/dist/mutate.js +86 -57
  38. package/dist/orchestrator/agent-capabilities.d.ts +2 -0
  39. package/dist/orchestrator/agent-capabilities.js +201 -0
  40. package/dist/orchestrator/allow.d.ts +16 -0
  41. package/dist/orchestrator/allow.js +65 -0
  42. package/dist/orchestrator/attachments.d.ts +2 -0
  43. package/dist/orchestrator/attachments.js +11 -0
  44. package/dist/orchestrator/config.d.ts +4 -0
  45. package/dist/orchestrator/config.js +19 -0
  46. package/dist/orchestrator/db.d.ts +17 -0
  47. package/dist/orchestrator/db.js +12 -0
  48. package/dist/orchestrator/diagnostics.d.ts +9 -0
  49. package/dist/orchestrator/diagnostics.js +13 -0
  50. package/dist/orchestrator/environment.d.ts +3 -0
  51. package/dist/orchestrator/environment.js +30 -0
  52. package/dist/orchestrator/file-inspection.d.ts +3 -0
  53. package/dist/orchestrator/file-inspection.js +61 -0
  54. package/dist/orchestrator/format.d.ts +1 -0
  55. package/dist/orchestrator/format.js +6 -0
  56. package/dist/orchestrator/index.d.ts +30 -0
  57. package/dist/orchestrator/index.js +56 -0
  58. package/dist/orchestrator/live-message.d.ts +14 -0
  59. package/dist/orchestrator/live-message.js +100 -0
  60. package/dist/orchestrator/pipeline.d.ts +11 -0
  61. package/dist/orchestrator/pipeline.js +198 -0
  62. package/dist/orchestrator/registry.d.ts +13 -0
  63. package/dist/orchestrator/registry.js +74 -0
  64. package/dist/orchestrator/starters.d.ts +19 -0
  65. package/dist/orchestrator/starters.js +81 -0
  66. package/dist/orchestrator/suggestion-capabilities.d.ts +5 -0
  67. package/dist/orchestrator/suggestion-capabilities.js +33 -0
  68. package/dist/orchestrator/system-assembly.d.ts +7 -0
  69. package/dist/orchestrator/system-assembly.js +13 -0
  70. package/dist/orchestrator/system-capabilities.d.ts +2 -0
  71. package/dist/orchestrator/system-capabilities.js +63 -0
  72. package/dist/orchestrator/types.d.ts +144 -0
  73. package/dist/orchestrator/types.js +1 -0
  74. package/dist/part-errors.d.ts +3 -0
  75. package/dist/part-errors.js +6 -0
  76. package/dist/part-injection.d.ts +11 -3
  77. package/dist/part-injection.js +72 -92
  78. package/dist/part-ref.d.ts +13 -0
  79. package/dist/part-ref.js +109 -0
  80. package/dist/plugin-hooks.d.ts +1 -0
  81. package/dist/plugin-i18n.js +2 -1
  82. package/dist/plugin-runtime.d.ts +2 -2
  83. package/dist/plugin-runtime.js +21 -11
  84. package/dist/plugin-updates.d.ts +2 -2
  85. package/dist/plugin-updates.js +6 -12
  86. package/dist/public-url.js +2 -1
  87. package/dist/recompute-derived.d.ts +2 -1
  88. package/dist/recompute-derived.js +123 -26
  89. package/dist/records-api.d.ts +5 -1
  90. package/dist/records-api.js +47 -15
  91. package/dist/registry-context.d.ts +1 -1
  92. package/dist/registry-context.js +3 -0
  93. package/dist/rename-guard.d.ts +1 -0
  94. package/dist/rename-guard.js +9 -0
  95. package/dist/schema-sync.js +38 -1
  96. package/dist/search-index.js +1 -1
  97. package/dist/search-indexer.d.ts +1 -1
  98. package/dist/search-indexer.js +3 -3
  99. package/dist/settings-groups.d.ts +14 -0
  100. package/dist/settings-groups.js +25 -0
  101. package/dist/settings-write.d.ts +4 -4
  102. package/dist/settings-write.js +23 -26
  103. package/dist/system-settings.d.ts +3 -0
  104. package/dist/system-settings.js +27 -0
  105. package/dist/thread-store.d.ts +3 -2
  106. package/dist/thread-store.js +6 -5
  107. package/dist/trigger-queue.d.ts +33 -0
  108. package/dist/trigger-queue.js +432 -0
  109. package/dist/uploads.js +6 -3
  110. package/dist/value-compare.d.ts +1 -0
  111. package/dist/value-compare.js +7 -0
  112. 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];
@@ -185,6 +194,7 @@ export async function purgePluginData(p, actor) {
185
194
  .transactional(async (tx) => {
186
195
  const now = new Date().toISOString();
187
196
  await tx.nativeDelete('_Migration', { plugin_id: p.id });
197
+ await tx.nativeDelete('_MigrationOp', { plugin_id: p.id });
188
198
  await tx.nativeDelete('_Seed', { plugin_id: p.id });
189
199
  const row = await tx.findOne('_Plugin', { id: p.id });
190
200
  if (row)
@@ -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,40 +1,137 @@
1
- import { derivedEntries, applyDerived } from '@coffer-org/sdk/derive';
1
+ import { collectionGroups } from '@coffer-org/sdk/shelf';
2
+ import { applyStoredParts, hasStoredPartWork } from "./part-injection.js";
3
+ import { extendEntityName } from "./extend-table.js";
2
4
  import { getEm } from "./db.js";
3
5
  import { selectRows } from "./read-rows.js";
4
- import { encodeJson } from "./mutate.js";
5
- import { encodeTemporal, decodeTemporal } from "./temporal.js";
6
- import { flattenEmbedded, nestEmbedded, readCollections } from "./collection-io.js";
7
- function sameValue(a, b) {
8
- if (a instanceof Date && b instanceof Date)
9
- return a.getTime() === b.getTime();
10
- if (a instanceof Date || b instanceof Date)
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";
11
+ function partCtx(m) {
12
+ return { settings: {}, global: {}, meta: { library: m.library, shelf: m.shelf } };
13
+ }
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
+ const out = {};
22
+ for (const [k, v] of Object.entries(after)) {
23
+ if (collKeys.has(k) || clockKeys.has(k))
24
+ continue;
25
+ if (JSON.stringify(before[k] ?? null) !== JSON.stringify(v ?? null))
26
+ out[k] = v;
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
+ }
36
+ return out;
37
+ }
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)
11
76
  return false;
12
- return a === b;
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;
13
98
  }
14
- export async function recomputeDerivedFields(m, entityName) {
15
- if (derivedEntries(m.fields).length === 0)
99
+ export async function recomputeDerivedFields(m, entityName, extends_ = []) {
100
+ const hasExtendWork = extends_.some((e) => hasStoredPartWork(e.fields));
101
+ if (!hasStoredPartWork(m.fields) && !hasExtendWork)
16
102
  return { scanned: 0, changed: 0 };
17
103
  const fork = getEm().fork();
18
104
  const rows = await selectRows(fork, entityName, {}, { orderBy: { id: 'asc' } });
19
- let changed = 0;
105
+ const changedIds = new Set();
20
106
  for (const row of rows) {
21
107
  const id = row['id'];
22
108
  const flat = decodeTemporal(m, row);
23
109
  const nested = nestEmbedded(m, flat);
24
- const collections = await readCollections(fork, m, id);
25
- const derived = await applyDerived(m.fields, { ...nested, ...collections });
26
- if (Object.keys(derived).length === 0)
27
- continue;
28
- const encoded = encodeJson(m, encodeTemporal(m, flattenEmbedded(m, derived)));
29
- const dirty = {};
30
- for (const [k, v] of Object.entries(encoded)) {
31
- if (!sameValue(row[k], v))
32
- dirty[k] = v;
110
+ const { collections, meta } = await readCollectionsWithMeta(fork, m, id);
111
+ const hydrated = { ...nested, ...collections };
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);
33
134
  }
34
- if (Object.keys(dirty).length === 0)
35
- continue;
36
- await fork.nativeUpdate(entityName, { id }, dirty);
37
- changed += 1;
38
135
  }
39
- return { scanned: rows.length, changed };
136
+ return { scanned: rows.length, changed: changedIds.size };
40
137
  }
@@ -1,4 +1,5 @@
1
1
  import type { ShelfDef } from '@coffer-org/sdk/shelf';
2
+ import { type PartMemo } from './part-injection.ts';
2
3
  export declare class UnknownShelfError extends Error {
3
4
  }
4
5
  export declare class SingleShelfError extends Error {
@@ -20,12 +21,15 @@ export type RecordListOpts = {
20
21
  };
21
22
  export declare const MAX_PAGE = 500;
22
23
  export declare function coerceFilter(v: string, column: string): unknown;
23
- export declare function withExtends(record: Record<string, unknown>, library: string, shelf: string): Promise<Record<string, unknown>>;
24
+ export declare function withExtends(record: Record<string, unknown>, library: string, shelf: string, memo?: PartMemo): Promise<Record<string, unknown>>;
24
25
  export declare function withExtendsMany(rows: Record<string, unknown>[], library: string, shelf: string): Promise<Record<string, unknown>[]>;
25
26
  export declare function rowMatch(mdef: ShelfDef, row: Record<string, unknown>, tokens: string[]): {
26
27
  score: number;
27
28
  snippet: string;
28
29
  } | null;
30
+ export declare class FilterError extends Error {
31
+ constructor(message: string);
32
+ }
29
33
  export declare function recordCount(library: string, shelf: string, query?: RecordListQuery, opts?: Pick<RecordListOpts, 'deleted' | 'where'>): Promise<number>;
30
34
  export declare function recordList(library: string, shelf: string, query?: RecordListQuery, opts?: RecordListOpts): Promise<Record<string, unknown>[]>;
31
35
  export declare function isPagedRequest(limit?: string, offset?: string): boolean;
@@ -1,5 +1,5 @@
1
- import { fieldMap, textSearchKeys, titleKey, recordTitle, listKeys, storageColumnsFor } 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";
@@ -8,7 +8,9 @@ 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, restoreRecord, purgeRecord } from "./mutate.js";
11
+ import { newPartMemo } from "./part-injection.js";
11
12
  import { deleteRecordActivity } from "./record-activity.js";
13
+ import { stripClockKeys } from "./cache-clock.js";
12
14
  export class UnknownShelfError extends Error {
13
15
  }
14
16
  export class SingleShelfError extends Error {
@@ -42,13 +44,13 @@ export function coerceFilter(v, column) {
42
44
  return dtStringToDate(v);
43
45
  return v;
44
46
  }
45
- export async function withExtends(record, library, shelf) {
47
+ export async function withExtends(record, library, shelf, memo) {
46
48
  const matchedExtends = getExtendsFor(library, shelf);
47
49
  if (!matchedExtends.length)
48
50
  return record;
49
51
  const _extends = {};
50
52
  await Promise.all(matchedExtends.map(async (e) => {
51
- _extends[e.id] = (await getExtendRecord(e, record.id)) ?? null;
53
+ _extends[e.id] = (await getExtendRecord(e, record.id, undefined, memo)) ?? null;
52
54
  }));
53
55
  return { ...record, _extends };
54
56
  }
@@ -85,15 +87,38 @@ export function rowMatch(mdef, row, tokens) {
85
87
  const snippet = raw.length > 120 ? raw.slice(0, 120) + '…' : raw;
86
88
  return { score, snippet };
87
89
  }
90
+ export class FilterError extends Error {
91
+ constructor(message) {
92
+ super(message);
93
+ this.name = 'FilterError';
94
+ }
95
+ }
96
+ function resolveFilterKey(m, fm, k) {
97
+ const direct = fm[k];
98
+ if (direct) {
99
+ if (!direct.columns)
100
+ return { column: k, type: direct.column };
101
+ const sub = magnitudeSub(direct);
102
+ if (sub === undefined)
103
+ throw new FilterError(`${m.library}/${m.shelf}: '${k}' is a composite with no stored part — it owns no column to filter on`);
104
+ return { column: `${k}__${sub}`, type: direct.columns[sub] };
105
+ }
106
+ const owner = resolveColumnKey(fm, k);
107
+ if (!owner)
108
+ return undefined;
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] };
112
+ }
88
113
  function buildWhere(m, filterParams) {
89
114
  const where = {};
90
115
  const fm = fieldMap(m.fields);
91
116
  for (const [k, v] of Object.entries(filterParams)) {
92
117
  if (k === 'id' || v === undefined)
93
118
  continue;
94
- const fieldDef = fm[k];
95
- if (fieldDef && !fieldDef.virtual)
96
- where[k] = coerceFilter(String(v), fieldDef.column);
119
+ const target = resolveFilterKey(m, fm, k);
120
+ if (target)
121
+ where[target.column] = coerceFilter(String(v), target.type);
97
122
  }
98
123
  if (filterParams['id']) {
99
124
  const ids = String(filterParams['id'])
@@ -148,8 +173,12 @@ async function listRecords(library, shelf, query = {}, opts = {}) {
148
173
  let rows = await selectRows(fork, ename, where, findOpts);
149
174
  if (tokens.length > 0)
150
175
  rows = rows.filter((row) => rowMatch(m, row, tokens) !== null);
151
- if (projected)
176
+ if (projected) {
152
177
  rows = rows.map((row) => pickCols(row, projected));
178
+ }
179
+ else {
180
+ rows = rows.map((row) => stripClockKeys(row));
181
+ }
153
182
  const decoded = rows.map((r) => decodeTemporal(m, r));
154
183
  if (!withExt)
155
184
  return decoded;
@@ -197,11 +226,12 @@ async function createOne(library, shelf, body, actor = 'gui') {
197
226
  const { m, ename } = resolve(library, shelf);
198
227
  const { base, extData } = splitBody(body);
199
228
  validateExtends(library, shelf, extData);
229
+ const memo = newPartMemo();
200
230
  const row = await createRecord(m, ename, base, { actor }, async (tx, id) => {
201
- await saveExtends(tx, library, shelf, id, extData);
202
- return readExtends(tx, library, shelf, id);
231
+ await saveExtends(tx, library, shelf, id, extData, memo);
232
+ return readExtends(tx, library, shelf, id, memo);
203
233
  });
204
- return withExtends(row, library, shelf);
234
+ return withExtends(row, library, shelf, memo);
205
235
  }
206
236
  export async function recordCreate(library, shelf, body, actor = 'gui') {
207
237
  const { m } = resolve(library, shelf);
@@ -238,11 +268,12 @@ export async function recordUpdate(library, shelf, id, body, actor = 'gui') {
238
268
  const { m, ename } = resolve(library, shelf);
239
269
  const { base, extData } = splitBody(body);
240
270
  validateExtends(library, shelf, extData);
271
+ const memo = newPartMemo();
241
272
  const row = await updateRecord(m, ename, id, base, { actor }, async (tx) => {
242
- await saveExtends(tx, library, shelf, id, extData);
243
- return readExtends(tx, library, shelf, id);
273
+ await saveExtends(tx, library, shelf, id, extData, memo);
274
+ return readExtends(tx, library, shelf, id, memo);
244
275
  });
245
- return withExtends(row, library, shelf);
276
+ return withExtends(row, library, shelf, memo);
246
277
  }
247
278
  export async function recordDelete(library, shelf, id, actor = 'gui') {
248
279
  const { m, ename } = resolve(library, shelf);
@@ -252,7 +283,8 @@ export async function recordDelete(library, shelf, id, actor = 'gui') {
252
283
  }
253
284
  export async function recordRestore(library, shelf, id, actor = 'gui') {
254
285
  const { m, ename } = resolve(library, shelf);
255
- await restoreRecord(m, ename, id, { actor }, (tx, recordId) => readExtends(tx, library, shelf, recordId));
286
+ const memo = newPartMemo();
287
+ await restoreRecord(m, ename, id, { actor }, (tx, recordId) => readExtends(tx, library, shelf, recordId, memo));
256
288
  }
257
289
  export async function recordPurge(library, shelf, id, actor = 'gui') {
258
290
  const { m, ename } = resolve(library, shelf);
@@ -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() {
@@ -0,0 +1 @@
1
+ export declare function disableColumnRenameDetection(): void;
@@ -0,0 +1,9 @@
1
+ import { SchemaComparator } from '@mikro-orm/sqlite';
2
+ let applied = false;
3
+ export function disableColumnRenameDetection() {
4
+ if (applied)
5
+ return;
6
+ SchemaComparator.prototype['detectColumnRenamings'] = function noop() {
7
+ };
8
+ applied = true;
9
+ }
@@ -1,4 +1,41 @@
1
- import { getOrm } from "./db.js";
1
+ import { getOrm, getEm } from "./db.js";
2
+ import { introspectAllColumns, isColumnHandedOver } from "./migrations.js";
3
+ import { getLogger } from '@coffer-org/sdk/logger';
4
+ const log = getLogger('schema-sync');
5
+ const q = (id) => `"${id.replace(/"/g, '""')}"`;
6
+ async function warnAboutColumnsToBeDropped() {
7
+ try {
8
+ const orm = getOrm();
9
+ const em = getEm().fork();
10
+ const metas = [...orm.getMetadata().getAll().values()].filter((m) => !m.virtual && !m.pivotTable && m.tableName);
11
+ const live = await introspectAllColumns(em, [...new Set(metas.map((m) => m.tableName))]);
12
+ for (const meta of metas) {
13
+ const tableName = meta.tableName;
14
+ const columns = live.get(tableName);
15
+ if (!columns)
16
+ continue;
17
+ const known = new Set();
18
+ for (const prop of meta.props)
19
+ for (const f of prop.fieldNames ?? [])
20
+ known.add(f);
21
+ const droppable = [...columns].filter((c) => !known.has(c) && !isColumnHandedOver(tableName, c));
22
+ if (!droppable.length)
23
+ continue;
24
+ const conn = em.getConnection();
25
+ for (const col of droppable) {
26
+ const rows = (await conn.execute(`SELECT COUNT(*) AS n FROM ${q(tableName)} WHERE ${q(col)} IS NOT NULL`));
27
+ const n = rows[0]?.n ?? 0;
28
+ if (n > 0) {
29
+ log.warn(`schema sync is about to DROP ${tableName}.${col} — it still holds ${n} row(s) of data. Add a migration (TableOps.convert/renameColumn/changeType) before the next start, or the data is lost.`);
30
+ }
31
+ }
32
+ }
33
+ }
34
+ catch (e) {
35
+ log.error('drop-warning check failed (diagnostic only, sync continues)', e);
36
+ }
37
+ }
2
38
  export async function syncSchema() {
39
+ await warnAboutColumnsToBeDropped();
3
40
  await getOrm().schema.update({ safe: false, dropTables: false });
4
41
  }
@@ -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>;