@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.
- package/dist/auth-api.js +4 -5
- package/dist/cache-clock.d.ts +8 -0
- package/dist/cache-clock.js +103 -0
- package/dist/cache-refresh.d.ts +7 -0
- package/dist/cache-refresh.js +322 -0
- package/dist/collection-io.d.ts +13 -0
- package/dist/collection-io.js +40 -29
- package/dist/compute-unit.d.ts +66 -0
- package/dist/compute-unit.js +369 -0
- package/dist/condition-where.js +1 -3
- package/dist/data-dir.d.ts +3 -0
- package/dist/data-dir.js +39 -0
- package/dist/entity-schema.js +5 -2
- package/dist/extend-table.d.ts +1 -1
- package/dist/extend-table.js +22 -8
- package/dist/file-fields.js +50 -23
- package/dist/frontend-agent.d.ts +1 -1
- package/dist/frontend-agent.js +2 -3
- package/dist/global-search.js +1 -1
- package/dist/index.js +20 -26
- package/dist/mcp-contract/client.d.ts +24 -0
- package/dist/mcp-contract/client.js +12 -0
- package/dist/mcp-contract/schema.d.ts +46 -0
- package/dist/mcp-contract/schema.js +90 -0
- package/dist/mcp-contract/tools.d.ts +19 -0
- package/dist/mcp-contract/tools.js +160 -0
- package/dist/mcp-http.js +4 -2
- package/dist/mcp-local.d.ts +1 -1
- package/dist/mcp-local.js +1 -1
- package/dist/mcp-tools.d.ts +13 -1
- package/dist/mcp-tools.js +46 -16
- package/dist/migrations.d.ts +5 -0
- package/dist/migrations.js +122 -0
- package/dist/mutate.js +77 -36
- package/dist/orchestrator/agent-capabilities.d.ts +2 -0
- package/dist/orchestrator/agent-capabilities.js +201 -0
- package/dist/orchestrator/allow.d.ts +16 -0
- package/dist/orchestrator/allow.js +65 -0
- package/dist/orchestrator/attachments.d.ts +2 -0
- package/dist/orchestrator/attachments.js +11 -0
- package/dist/orchestrator/config.d.ts +4 -0
- package/dist/orchestrator/config.js +19 -0
- package/dist/orchestrator/db.d.ts +17 -0
- package/dist/orchestrator/db.js +12 -0
- package/dist/orchestrator/diagnostics.d.ts +9 -0
- package/dist/orchestrator/diagnostics.js +13 -0
- package/dist/orchestrator/environment.d.ts +2 -0
- package/dist/orchestrator/environment.js +14 -0
- package/dist/orchestrator/file-inspection.d.ts +3 -0
- package/dist/orchestrator/file-inspection.js +61 -0
- package/dist/orchestrator/format.d.ts +1 -0
- package/dist/orchestrator/format.js +6 -0
- package/dist/orchestrator/index.d.ts +30 -0
- package/dist/orchestrator/index.js +56 -0
- package/dist/orchestrator/live-message.d.ts +14 -0
- package/dist/orchestrator/live-message.js +100 -0
- package/dist/orchestrator/pipeline.d.ts +11 -0
- package/dist/orchestrator/pipeline.js +198 -0
- package/dist/orchestrator/registry.d.ts +13 -0
- package/dist/orchestrator/registry.js +74 -0
- package/dist/orchestrator/starters.d.ts +19 -0
- package/dist/orchestrator/starters.js +81 -0
- package/dist/orchestrator/suggestion-capabilities.d.ts +5 -0
- package/dist/orchestrator/suggestion-capabilities.js +33 -0
- package/dist/orchestrator/system-assembly.d.ts +4 -0
- package/dist/orchestrator/system-assembly.js +3 -0
- package/dist/orchestrator/system-capabilities.d.ts +2 -0
- package/dist/orchestrator/system-capabilities.js +63 -0
- package/dist/orchestrator/types.d.ts +141 -0
- package/dist/orchestrator/types.js +1 -0
- package/dist/part-injection.d.ts +3 -4
- package/dist/part-injection.js +25 -368
- package/dist/plugin-hooks.d.ts +1 -0
- package/dist/plugin-i18n.js +2 -1
- package/dist/plugin-runtime.d.ts +2 -2
- package/dist/plugin-runtime.js +20 -11
- package/dist/plugin-updates.d.ts +2 -2
- package/dist/plugin-updates.js +6 -12
- package/dist/public-url.js +2 -1
- package/dist/recompute-derived.d.ts +2 -1
- package/dist/recompute-derived.js +111 -34
- package/dist/records-api.js +14 -16
- package/dist/registry-context.d.ts +1 -1
- package/dist/registry-context.js +3 -0
- package/dist/search-index.js +1 -1
- package/dist/search-indexer.d.ts +1 -1
- package/dist/search-indexer.js +3 -3
- package/dist/settings-groups.d.ts +14 -0
- package/dist/settings-groups.js +25 -0
- package/dist/settings-write.d.ts +4 -4
- package/dist/settings-write.js +23 -26
- package/dist/system-settings.d.ts +3 -0
- package/dist/system-settings.js +27 -0
- package/dist/thread-store.d.ts +3 -2
- package/dist/thread-store.js +6 -5
- package/dist/trigger-queue.d.ts +33 -0
- package/dist/trigger-queue.js +432 -0
- package/dist/uploads.js +6 -3
- package/dist/value-compare.d.ts +1 -0
- package/dist/value-compare.js +7 -0
- package/package.json +17 -3
package/dist/plugin-runtime.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { composeRegistry } from '@coffer-org/
|
|
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,
|
|
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
|
-
|
|
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(
|
|
36
|
+
export async function getPluginSettings(groupId) {
|
|
37
37
|
try {
|
|
38
38
|
const fork = getEm().fork();
|
|
39
|
-
const [row] = await selectRows(fork, `_settings__${
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
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];
|
package/dist/plugin-updates.d.ts
CHANGED
|
@@ -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(
|
|
21
|
+
export declare function resolveBaseTargets(runtime: {
|
|
22
22
|
packageName: string;
|
|
23
23
|
installedVersion: string;
|
|
24
24
|
local: boolean;
|
|
25
|
-
} | null,
|
|
25
|
+
} | null, runtimeLatest: string | null): AllUpdateTarget[];
|
|
26
26
|
export interface RuntimeUpdateTarget {
|
|
27
27
|
packageName: string;
|
|
28
28
|
from: string;
|
package/dist/plugin-updates.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { execFile } from 'node:child_process';
|
|
2
|
-
import { isNewerVersion } from '@coffer-org/
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
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)
|
package/dist/public-url.js
CHANGED
|
@@ -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(
|
|
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,
|
|
9
|
-
|
|
10
|
-
|
|
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
|
|
20
|
-
|
|
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
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
-
|
|
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
|
|
110
|
+
const { collections, meta } = await readCollectionsWithMeta(fork, m, id);
|
|
42
111
|
const hydrated = { ...nested, ...collections };
|
|
43
|
-
const withParts =
|
|
44
|
-
const partPatch =
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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
|
}
|
package/dist/records-api.js
CHANGED
|
@@ -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/
|
|
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
|
|
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
|
|
108
|
-
if (
|
|
106
|
+
const owner = resolveColumnKey(fm, k);
|
|
107
|
+
if (!owner)
|
|
109
108
|
return undefined;
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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/
|
|
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'];
|
package/dist/registry-context.js
CHANGED
package/dist/search-index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { textSearchKeys, titleKey } from '@coffer-org/sdk/shelf';
|
|
2
|
-
import { foldText } from '@coffer-org/
|
|
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";
|
package/dist/search-indexer.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export declare const
|
|
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;
|
package/dist/search-indexer.js
CHANGED
|
@@ -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
|
|
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(
|
|
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(
|
|
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
|
+
}
|
package/dist/settings-write.d.ts
CHANGED
|
@@ -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(
|
|
13
|
-
export declare function writePluginSettings(em: EntityManager,
|
|
14
|
-
export declare function listSettings(
|
|
15
|
-
|
|
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>;
|
package/dist/settings-write.js
CHANGED
|
@@ -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 {
|
|
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(
|
|
16
|
-
return { library: '_settings', shelf:
|
|
16
|
+
function settingsShelf(groupId, fields, label = `${groupId}.plugin.label`) {
|
|
17
|
+
return { library: '_settings', shelf: groupId, label, fields };
|
|
17
18
|
}
|
|
18
|
-
export function buildSettingsBody(
|
|
19
|
+
export function buildSettingsBody(groupId, fields, incoming, existing, label) {
|
|
19
20
|
const body = preserveTree(fields, incoming, existing);
|
|
20
|
-
const parsed = buildZodObject(settingsShelf(
|
|
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
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
|
35
|
-
const
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
|
|
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:
|
|
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(
|
|
55
|
-
const
|
|
52
|
+
export async function listSettings(pluginsOverride) {
|
|
53
|
+
const groups = pluginsOverride ? groupsFromPlugins(pluginsOverride) : await settingsGroups();
|
|
56
54
|
const out = [];
|
|
57
|
-
for (const
|
|
58
|
-
const
|
|
59
|
-
|
|
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
|
}
|