@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.
- 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 +42 -28
- 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/db.js +2 -0
- package/dist/entity-schema.d.ts +1 -0
- package/dist/entity-schema.js +25 -2
- package/dist/extend-io.d.ts +3 -2
- package/dist/extend-io.js +4 -4
- package/dist/extend-table.d.ts +3 -2
- package/dist/extend-table.js +48 -11
- package/dist/file-fields.js +50 -23
- package/dist/global-search.js +1 -1
- package/dist/http-errors.d.ts +7 -0
- package/dist/http-errors.js +24 -0
- package/dist/index.js +31 -35
- 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-local.d.ts +1 -1
- package/dist/mcp-local.js +4 -2
- package/dist/mcp-tools.d.ts +12 -1
- package/dist/mcp-tools.js +41 -8
- package/dist/migrations.d.ts +30 -2
- package/dist/migrations.js +368 -19
- package/dist/mutate.js +86 -57
- 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 +3 -0
- package/dist/orchestrator/environment.js +30 -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 +7 -0
- package/dist/orchestrator/system-assembly.js +13 -0
- package/dist/orchestrator/system-capabilities.d.ts +2 -0
- package/dist/orchestrator/system-capabilities.js +63 -0
- package/dist/orchestrator/types.d.ts +144 -0
- package/dist/orchestrator/types.js +1 -0
- package/dist/part-errors.d.ts +3 -0
- package/dist/part-errors.js +6 -0
- package/dist/part-injection.d.ts +11 -3
- package/dist/part-injection.js +72 -92
- package/dist/part-ref.d.ts +13 -0
- package/dist/part-ref.js +109 -0
- 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 +21 -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 +123 -26
- package/dist/records-api.d.ts +5 -1
- package/dist/records-api.js +47 -15
- package/dist/registry-context.d.ts +1 -1
- package/dist/registry-context.js +3 -0
- package/dist/rename-guard.d.ts +1 -0
- package/dist/rename-guard.js +9 -0
- package/dist/schema-sync.js +38 -1
- 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];
|
|
@@ -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)
|
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,40 +1,137 @@
|
|
|
1
|
-
import {
|
|
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,
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
}
|
package/dist/records-api.d.ts
CHANGED
|
@@ -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;
|
package/dist/records-api.js
CHANGED
|
@@ -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/
|
|
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
|
|
95
|
-
if (
|
|
96
|
-
where[
|
|
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
|
-
|
|
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/
|
|
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
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function disableColumnRenameDetection(): void;
|
package/dist/schema-sync.js
CHANGED
|
@@ -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
|
}
|
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>;
|