@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
|
@@ -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
|
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { field } from '@coffer-org/sdk/fields';
|
|
2
|
+
import { defineSettings } from '@coffer-org/sdk/settings';
|
|
3
|
+
export const SYSTEM_SETTINGS_ID = 'system';
|
|
4
|
+
export const SYSTEM_SETTINGS = defineSettings({
|
|
5
|
+
label: 'core.settings.label',
|
|
6
|
+
fields: {
|
|
7
|
+
publicUrl: field.string({
|
|
8
|
+
label: 'core.settings.publicUrl',
|
|
9
|
+
rules: { pattern: '^https?://[^/\\s]+/?$', messageKey: 'core.settings.publicUrlHint' },
|
|
10
|
+
}),
|
|
11
|
+
language: field.string({
|
|
12
|
+
label: 'core.settings.language',
|
|
13
|
+
default: 'uk',
|
|
14
|
+
strict: true,
|
|
15
|
+
view: { noSearch: true },
|
|
16
|
+
options: [
|
|
17
|
+
{ value: 'uk', title: 'Українська' },
|
|
18
|
+
{ value: 'ru', title: 'Русский' },
|
|
19
|
+
{ value: 'en', title: 'English' },
|
|
20
|
+
],
|
|
21
|
+
}),
|
|
22
|
+
agent_id: field.string({ label: 'core.settings.agent_id', strict: true, view: { noSearch: true } }),
|
|
23
|
+
access_password: field.password({ label: 'core.settings.access_password' }),
|
|
24
|
+
trigger_prefix: field.string({ label: 'core.settings.trigger_prefix' }),
|
|
25
|
+
reply_window: field.int({ label: 'core.settings.reply_window', default: 1800 }),
|
|
26
|
+
},
|
|
27
|
+
});
|
package/dist/thread-store.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
export declare const SIDECAR_ROLES: readonly ["reasoning", "suggestions"];
|
|
1
2
|
export interface StoredMsg {
|
|
2
3
|
msgId: string;
|
|
3
|
-
role: 'user' | 'assistant' | 'reasoning';
|
|
4
|
+
role: 'user' | 'assistant' | 'reasoning' | 'suggestions';
|
|
4
5
|
sender: string | null;
|
|
5
6
|
text: string;
|
|
6
7
|
attachments?: StoredAttachment[];
|
|
@@ -24,7 +25,7 @@ export declare function putThreadMessage(m: {
|
|
|
24
25
|
connector: string;
|
|
25
26
|
chatId: string;
|
|
26
27
|
msgId: string;
|
|
27
|
-
role: 'user' | 'assistant' | 'reasoning';
|
|
28
|
+
role: 'user' | 'assistant' | 'reasoning' | 'suggestions';
|
|
28
29
|
sender?: string | null;
|
|
29
30
|
attachments?: StoredAttachment[];
|
|
30
31
|
text: string;
|
package/dist/thread-store.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { getEm } from "./db.js";
|
|
2
|
+
export const SIDECAR_ROLES = ['reasoning', 'suggestions'];
|
|
2
3
|
function toStored(r) {
|
|
3
4
|
let attachments;
|
|
4
5
|
if (r.attachments) {
|
|
@@ -49,12 +50,12 @@ export async function pruneThreadMessages(connector, cutoffTs) {
|
|
|
49
50
|
}
|
|
50
51
|
export async function listThreadMessages(connector, chatId, limit = 200) {
|
|
51
52
|
const em = getEm().fork();
|
|
52
|
-
const visible = (await em.find('_ThreadMessage', { connector, chat_id: chatId, role: { $
|
|
53
|
+
const visible = (await em.find('_ThreadMessage', { connector, chat_id: chatId, role: { $nin: SIDECAR_ROLES } }, { orderBy: { ts: 'desc', msg_id: 'desc' }, limit }));
|
|
53
54
|
const oldest = visible.at(-1)?.ts;
|
|
54
|
-
const
|
|
55
|
+
const sidecars = oldest === undefined
|
|
55
56
|
? []
|
|
56
|
-
: (await em.find('_ThreadMessage', { connector, chat_id: chatId, role:
|
|
57
|
-
return [...visible, ...
|
|
57
|
+
: (await em.find('_ThreadMessage', { connector, chat_id: chatId, role: { $in: SIDECAR_ROLES }, ts: { $gte: oldest - 1 } }, { orderBy: { ts: 'desc', msg_id: 'desc' } }));
|
|
58
|
+
return [...visible, ...sidecars]
|
|
58
59
|
.sort((a, b) => b.ts - a.ts || (a.msg_id < b.msg_id ? 1 : a.msg_id > b.msg_id ? -1 : 0))
|
|
59
60
|
.reverse()
|
|
60
61
|
.map(toStored);
|
|
@@ -69,7 +70,7 @@ export async function listThreadChats(connector, chatIdPrefix) {
|
|
|
69
70
|
const rows = (await em.find('_ThreadMessage', { connector, chat_id: { $gte: chatIdPrefix, $lt: upper } }, { orderBy: { ts: 'asc' } }));
|
|
70
71
|
const byChat = new Map();
|
|
71
72
|
for (const r of rows) {
|
|
72
|
-
if (r.role
|
|
73
|
+
if (SIDECAR_ROLES.includes(r.role))
|
|
73
74
|
continue;
|
|
74
75
|
const cur = byChat.get(r.chat_id);
|
|
75
76
|
if (!cur) {
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { type LayoutEl } from '@coffer-org/sdk/fields';
|
|
2
|
+
import type { MutateTarget } from '@coffer-org/sdk/mutate-path';
|
|
3
|
+
import { type MutateGraph } from '@coffer-org/sdk/mutate-graph';
|
|
4
|
+
import { type Row } from './compute-unit.ts';
|
|
5
|
+
import { type PartCtx } from './part-injection.ts';
|
|
6
|
+
export declare class TriggerQueueOverflowError extends Error {
|
|
7
|
+
}
|
|
8
|
+
export declare class TriggerRecordCapError extends Error {
|
|
9
|
+
}
|
|
10
|
+
export interface TouchedRecord {
|
|
11
|
+
library: string;
|
|
12
|
+
shelf: string;
|
|
13
|
+
id: number;
|
|
14
|
+
changed: string[];
|
|
15
|
+
}
|
|
16
|
+
interface CrossRecordState {
|
|
17
|
+
visited: Set<string>;
|
|
18
|
+
cap: number;
|
|
19
|
+
touched?: TouchedRecord[];
|
|
20
|
+
}
|
|
21
|
+
export interface ChangedField {
|
|
22
|
+
path: string[];
|
|
23
|
+
}
|
|
24
|
+
export declare function changedFields(before: Row, after: Row, fields: LayoutEl[]): ChangedField[];
|
|
25
|
+
export declare function allUnitPaths(fields: LayoutEl[], scope?: string[]): string[][];
|
|
26
|
+
export declare function runTriggers(fields: LayoutEl[], row: Row, changed: ChangedField[], graph: MutateGraph, shelfKey: string, ctx: PartCtx, opts?: {
|
|
27
|
+
seedAll?: boolean;
|
|
28
|
+
seedTargets?: MutateTarget[];
|
|
29
|
+
crossRecordState?: CrossRecordState;
|
|
30
|
+
recordCap?: number;
|
|
31
|
+
touched?: TouchedRecord[];
|
|
32
|
+
}): Promise<Row>;
|
|
33
|
+
export {};
|
|
@@ -0,0 +1,432 @@
|
|
|
1
|
+
import { hasChildren, isNamedField, isCollectionGroup, isJsonStored, } from '@coffer-org/sdk/fields';
|
|
2
|
+
import { fieldEntries } from '@coffer-org/sdk/shelf';
|
|
3
|
+
import { newScope, childScope, runFieldUnit, isPlainObject, isComputeUnit, isGroupUnit, } from "./compute-unit.js";
|
|
4
|
+
import { newPartMemo } from "./part-injection.js";
|
|
5
|
+
import { sameValue } from "./value-compare.js";
|
|
6
|
+
import { getShelf } from "./registry-context.js";
|
|
7
|
+
import { shelfTableName } from "./entity-schema.js";
|
|
8
|
+
import { selectRows } from "./read-rows.js";
|
|
9
|
+
import { decodeTemporal, encodeTemporal } from "./temporal.js";
|
|
10
|
+
import { nestEmbedded, flattenEmbedded, readCollections, writeCollections, splitCollections } from "./collection-io.js";
|
|
11
|
+
const MAX_UNIT_COMPUTATIONS = 10_000;
|
|
12
|
+
export class TriggerQueueOverflowError extends Error {
|
|
13
|
+
}
|
|
14
|
+
const MAX_TOUCHED_RECORDS = 1000;
|
|
15
|
+
export class TriggerRecordCapError extends Error {
|
|
16
|
+
}
|
|
17
|
+
function readHop(row, fields, hop, i) {
|
|
18
|
+
const seg = hop[i];
|
|
19
|
+
for (const el of fields) {
|
|
20
|
+
if (isNamedField(el)) {
|
|
21
|
+
if (el.key !== seg)
|
|
22
|
+
continue;
|
|
23
|
+
return el.type.relation ? [{ value: row[seg], relation: el.type.relation }] : [];
|
|
24
|
+
}
|
|
25
|
+
if (hasChildren(el)) {
|
|
26
|
+
const sub = el.fields;
|
|
27
|
+
if (!el.key) {
|
|
28
|
+
const found = readHop(row, sub, hop, i);
|
|
29
|
+
if (found.length)
|
|
30
|
+
return found;
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
if (el.key !== seg)
|
|
34
|
+
continue;
|
|
35
|
+
if (isCollectionGroup(el)) {
|
|
36
|
+
const rows = Array.isArray(row[seg]) ? row[seg] : [];
|
|
37
|
+
return rows.flatMap((r) => (isPlainObject(r) ? readHop(r, sub, hop, i + 1) : []));
|
|
38
|
+
}
|
|
39
|
+
const child = row[seg];
|
|
40
|
+
return isPlainObject(child) ? readHop(child, sub, hop, i + 1) : [];
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return [];
|
|
44
|
+
}
|
|
45
|
+
function idsOf(value) {
|
|
46
|
+
if (Array.isArray(value))
|
|
47
|
+
return value.filter((v) => Number.isInteger(v) && v > 0);
|
|
48
|
+
return Number.isInteger(value) && value > 0 ? [value] : [];
|
|
49
|
+
}
|
|
50
|
+
async function loadRecords(em, m, ids) {
|
|
51
|
+
const table = shelfTableName(m.library, m.shelf);
|
|
52
|
+
const flats = (await selectRows(em, table, { id: { $in: ids }, deleted_at: null }));
|
|
53
|
+
const out = new Map();
|
|
54
|
+
for (const flat of flats) {
|
|
55
|
+
const id = Number(flat['id']);
|
|
56
|
+
const nested = nestEmbedded(m, decodeTemporal(m, flat));
|
|
57
|
+
const collections = await readCollections(em, m, id);
|
|
58
|
+
out.set(id, { ...nested, ...collections });
|
|
59
|
+
}
|
|
60
|
+
return out;
|
|
61
|
+
}
|
|
62
|
+
async function resolveHopChain(em, startRow, startFields, hops) {
|
|
63
|
+
let current = [{ row: startRow, fields: startFields }];
|
|
64
|
+
let loaded = [];
|
|
65
|
+
for (const hop of hops) {
|
|
66
|
+
const byShelf = new Map();
|
|
67
|
+
for (const cur of current) {
|
|
68
|
+
for (const match of readHop(cur.row, cur.fields, hop, 0)) {
|
|
69
|
+
const m = getShelf(match.relation.library, match.relation.shelf);
|
|
70
|
+
if (!m)
|
|
71
|
+
continue;
|
|
72
|
+
const key = `${match.relation.library}/${match.relation.shelf}`;
|
|
73
|
+
let entry = byShelf.get(key);
|
|
74
|
+
if (!entry) {
|
|
75
|
+
entry = { m, ids: new Set() };
|
|
76
|
+
byShelf.set(key, entry);
|
|
77
|
+
}
|
|
78
|
+
for (const id of idsOf(match.value))
|
|
79
|
+
entry.ids.add(id);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
loaded = [];
|
|
83
|
+
for (const { m, ids } of byShelf.values()) {
|
|
84
|
+
if (!ids.size)
|
|
85
|
+
continue;
|
|
86
|
+
const rows = await loadRecords(em, m, [...ids]);
|
|
87
|
+
for (const [id, row] of rows)
|
|
88
|
+
loaded.push({ m, row, id });
|
|
89
|
+
}
|
|
90
|
+
current = loaded.map((l) => ({ row: l.row, fields: l.m.fields }));
|
|
91
|
+
}
|
|
92
|
+
return loaded;
|
|
93
|
+
}
|
|
94
|
+
function encodeJsonForWrite(fields, data) {
|
|
95
|
+
const out = { ...data };
|
|
96
|
+
for (const [k, fm] of fieldEntries(fields)) {
|
|
97
|
+
if (isJsonStored(fm) && out[k] !== null && out[k] !== undefined && typeof out[k] === 'object') {
|
|
98
|
+
out[k] = JSON.stringify(out[k]);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return out;
|
|
102
|
+
}
|
|
103
|
+
async function runCrossRecordTarget(target, rootRow, rootFields, graph, ctx, cross) {
|
|
104
|
+
const em = ctx.em;
|
|
105
|
+
if (!em)
|
|
106
|
+
return;
|
|
107
|
+
const endpoints = await resolveHopChain(em, rootRow, rootFields, target.hops);
|
|
108
|
+
for (const { m, row, id } of endpoints) {
|
|
109
|
+
const key = `${m.library}/${m.shelf}#${id}`;
|
|
110
|
+
if (cross.visited.has(key))
|
|
111
|
+
continue;
|
|
112
|
+
cross.visited.add(key);
|
|
113
|
+
if (cross.visited.size > cross.cap) {
|
|
114
|
+
throw new TriggerRecordCapError(`[trigger-queue] a cross-record write touched more than ${cross.cap} distinct records in one ` +
|
|
115
|
+
`transaction (reaching ${m.library}/${m.shelf}#${id}) — a chain of relation hops is not ` +
|
|
116
|
+
`converging (this guards the runtime cascade; a cycle in the DECLARED graph is rejected at ` +
|
|
117
|
+
`compose time by buildMutateGraph, so this is not that).`);
|
|
118
|
+
}
|
|
119
|
+
const before = row;
|
|
120
|
+
const after = { ...row };
|
|
121
|
+
const shelfKeyB = `${m.library}/${m.shelf}`;
|
|
122
|
+
const triggered = await runTriggers(m.fields, after, [], graph, shelfKeyB, { ...ctx, meta: { library: m.library, shelf: m.shelf }, memo: undefined }, { seedTargets: [{ hops: [], path: target.path, relative: false }], crossRecordState: cross });
|
|
123
|
+
const changedB = changedFields(before, triggered, m.fields);
|
|
124
|
+
if (!changedB.length)
|
|
125
|
+
continue;
|
|
126
|
+
cross.touched?.push({
|
|
127
|
+
library: m.library,
|
|
128
|
+
shelf: m.shelf,
|
|
129
|
+
id,
|
|
130
|
+
changed: changedB.map((c) => c.path.join('.')),
|
|
131
|
+
});
|
|
132
|
+
const { base, collections } = splitCollections(m, triggered);
|
|
133
|
+
const dbRow = encodeJsonForWrite(m.fields, encodeTemporal(m, flattenEmbedded(m, base)));
|
|
134
|
+
await em.upsert(shelfTableName(m.library, m.shelf), { ...dbRow, id });
|
|
135
|
+
const changedTop = new Set(changedB.map((c) => c.path[0]));
|
|
136
|
+
const toWrite = {};
|
|
137
|
+
for (const [k, rows] of Object.entries(collections))
|
|
138
|
+
if (changedTop.has(k))
|
|
139
|
+
toWrite[k] = rows;
|
|
140
|
+
if (Object.keys(toWrite).length)
|
|
141
|
+
await writeCollections(em, m, id, toWrite);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
function valuesDiffer(a, b) {
|
|
145
|
+
if (a instanceof Date || b instanceof Date)
|
|
146
|
+
return !sameValue(a, b);
|
|
147
|
+
if (Array.isArray(a) || Array.isArray(b)) {
|
|
148
|
+
if (!Array.isArray(a) || !Array.isArray(b) || a.length !== b.length)
|
|
149
|
+
return true;
|
|
150
|
+
return a.some((v, i) => valuesDiffer(v, b[i]));
|
|
151
|
+
}
|
|
152
|
+
if (isPlainObject(a) && isPlainObject(b)) {
|
|
153
|
+
const av = a;
|
|
154
|
+
const bv = b;
|
|
155
|
+
const keys = new Set([...Object.keys(av), ...Object.keys(bv)]);
|
|
156
|
+
for (const k of keys)
|
|
157
|
+
if (valuesDiffer(av[k], bv[k]))
|
|
158
|
+
return true;
|
|
159
|
+
return false;
|
|
160
|
+
}
|
|
161
|
+
return !sameValue(a, b);
|
|
162
|
+
}
|
|
163
|
+
export function changedFields(before, after, fields) {
|
|
164
|
+
const out = [];
|
|
165
|
+
collectChanges(before, after, fields, out);
|
|
166
|
+
return out;
|
|
167
|
+
}
|
|
168
|
+
function asRow(v) {
|
|
169
|
+
return isPlainObject(v) ? v : {};
|
|
170
|
+
}
|
|
171
|
+
function collectChanges(before, after, fields, out) {
|
|
172
|
+
for (const el of fields) {
|
|
173
|
+
if (isNamedField(el)) {
|
|
174
|
+
const key = el.key;
|
|
175
|
+
if (el.type.parts?.length) {
|
|
176
|
+
const bObj = asRow(before[key]);
|
|
177
|
+
const aObj = asRow(after[key]);
|
|
178
|
+
for (const p of el.type.parts) {
|
|
179
|
+
if (valuesDiffer(bObj[p.key], aObj[p.key]))
|
|
180
|
+
out.push({ path: [key, p.key] });
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
else if (valuesDiffer(before[key], after[key])) {
|
|
184
|
+
out.push({ path: [key] });
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
else if (hasChildren(el)) {
|
|
188
|
+
const sub = el.fields;
|
|
189
|
+
if (!el.key) {
|
|
190
|
+
collectChanges(before, after, sub, out);
|
|
191
|
+
}
|
|
192
|
+
else if (isCollectionGroup(el)) {
|
|
193
|
+
const bRows = Array.isArray(before[el.key]) ? before[el.key] : [];
|
|
194
|
+
const aRows = Array.isArray(after[el.key]) ? after[el.key] : [];
|
|
195
|
+
if (bRows.length !== aRows.length)
|
|
196
|
+
out.push({ path: [el.key] });
|
|
197
|
+
const rowKeys = new Set();
|
|
198
|
+
const n = Math.min(bRows.length, aRows.length);
|
|
199
|
+
for (let i = 0; i < n; i++) {
|
|
200
|
+
const rowOut = [];
|
|
201
|
+
collectChanges(asRow(bRows[i]), asRow(aRows[i]), sub, rowOut);
|
|
202
|
+
for (const c of rowOut)
|
|
203
|
+
rowKeys.add(c.path.join(''));
|
|
204
|
+
}
|
|
205
|
+
for (const s of rowKeys)
|
|
206
|
+
out.push({ path: [el.key, ...s.split('')] });
|
|
207
|
+
}
|
|
208
|
+
else {
|
|
209
|
+
const subOut = [];
|
|
210
|
+
collectChanges(asRow(before[el.key]), asRow(after[el.key]), sub, subOut);
|
|
211
|
+
for (const c of subOut)
|
|
212
|
+
out.push({ path: [el.key, ...c.path] });
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
function markDone(scope) {
|
|
218
|
+
for (const key of scope.units.keys())
|
|
219
|
+
scope.status.set(key, 'done');
|
|
220
|
+
return scope;
|
|
221
|
+
}
|
|
222
|
+
const rowScopesCache = new WeakMap();
|
|
223
|
+
function rowScopesOf(scope, key, sub) {
|
|
224
|
+
let byKey = rowScopesCache.get(scope);
|
|
225
|
+
if (!byKey) {
|
|
226
|
+
byKey = new Map();
|
|
227
|
+
rowScopesCache.set(scope, byKey);
|
|
228
|
+
}
|
|
229
|
+
const cached = byKey.get(key);
|
|
230
|
+
if (cached)
|
|
231
|
+
return cached;
|
|
232
|
+
const rows = scope.obj[key];
|
|
233
|
+
const arr = Array.isArray(rows) ? rows : [];
|
|
234
|
+
const copies = arr.map((r) => (isPlainObject(r) ? { ...r } : r));
|
|
235
|
+
scope.obj[key] = copies;
|
|
236
|
+
const scopes = [];
|
|
237
|
+
copies.forEach((r, i) => {
|
|
238
|
+
if (!isPlainObject(r))
|
|
239
|
+
return;
|
|
240
|
+
scopes.push(markDone(newScope(scope.st, r, sub, [...scope.path, key, i], null, `collection row '${key}'`)));
|
|
241
|
+
});
|
|
242
|
+
byKey.set(key, scopes);
|
|
243
|
+
return scopes;
|
|
244
|
+
}
|
|
245
|
+
function locateUnits(scope, fields, path, i, materialized) {
|
|
246
|
+
const seg = path[i];
|
|
247
|
+
const last = i === path.length - 1;
|
|
248
|
+
for (const el of fields) {
|
|
249
|
+
if (isNamedField(el)) {
|
|
250
|
+
if (el.key !== seg)
|
|
251
|
+
continue;
|
|
252
|
+
return [{ scope, key: el.key, fm: el.type }];
|
|
253
|
+
}
|
|
254
|
+
if (hasChildren(el)) {
|
|
255
|
+
const sub = el.fields;
|
|
256
|
+
if (!el.key) {
|
|
257
|
+
const found = locateUnits(scope, sub, path, i, materialized);
|
|
258
|
+
if (found.length)
|
|
259
|
+
return found;
|
|
260
|
+
continue;
|
|
261
|
+
}
|
|
262
|
+
if (el.key !== seg)
|
|
263
|
+
continue;
|
|
264
|
+
if (last)
|
|
265
|
+
return el.compute !== undefined ? [{ scope, key: el.key, fm: el }] : [];
|
|
266
|
+
if (isCollectionGroup(el)) {
|
|
267
|
+
const rows = rowScopesOf(scope, el.key, sub);
|
|
268
|
+
return rows.flatMap((row) => locateUnits(row, sub, path, i + 1, materialized));
|
|
269
|
+
}
|
|
270
|
+
let child = childScope(scope, el.key);
|
|
271
|
+
if (!child) {
|
|
272
|
+
child = newScope(scope.st, {}, sub, [...scope.path, el.key], scope.level, scope.level.label);
|
|
273
|
+
scope.obj[el.key] = child.obj;
|
|
274
|
+
scope.groups.set(el.key, child);
|
|
275
|
+
materialized.push({ parent: scope, key: el.key });
|
|
276
|
+
}
|
|
277
|
+
markDone(child);
|
|
278
|
+
return locateUnits(child, sub, path, i + 1, materialized);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
return [];
|
|
282
|
+
}
|
|
283
|
+
function isPathPrefix(prefix, path) {
|
|
284
|
+
return prefix.length <= path.length && prefix.every((seg, i) => path[i] === seg);
|
|
285
|
+
}
|
|
286
|
+
function edgesFrom(graph, shelfKey, path) {
|
|
287
|
+
return (graph.get(shelfKey) ?? []).filter((e) => isPathPrefix(path, e.from));
|
|
288
|
+
}
|
|
289
|
+
function movedNodes(item, before, after) {
|
|
290
|
+
if (isGroupUnit(item.fm)) {
|
|
291
|
+
const base = item.path;
|
|
292
|
+
const out = [];
|
|
293
|
+
if (valuesDiffer(before, after))
|
|
294
|
+
out.push(base);
|
|
295
|
+
if (!isCollectionGroup(item.fm)) {
|
|
296
|
+
const b = isPlainObject(before) ? before : {};
|
|
297
|
+
const a = isPlainObject(after) ? after : {};
|
|
298
|
+
for (const child of item.fm.fields) {
|
|
299
|
+
if (isNamedField(child) && valuesDiffer(b[child.key], a[child.key]))
|
|
300
|
+
out.push([...base, child.key]);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
return out;
|
|
304
|
+
}
|
|
305
|
+
const parts = item.fm.parts;
|
|
306
|
+
const lastSeg = item.path[item.path.length - 1];
|
|
307
|
+
const base = parts?.some((p) => p.key === lastSeg) ? item.path.slice(0, -1) : item.path;
|
|
308
|
+
const out = [];
|
|
309
|
+
if (valuesDiffer(before, after))
|
|
310
|
+
out.push(base);
|
|
311
|
+
if (parts?.length) {
|
|
312
|
+
const b = isPlainObject(before) ? before : {};
|
|
313
|
+
const a = isPlainObject(after) ? after : {};
|
|
314
|
+
for (const p of parts)
|
|
315
|
+
if (valuesDiffer(b[p.key], a[p.key]))
|
|
316
|
+
out.push([...base, p.key]);
|
|
317
|
+
}
|
|
318
|
+
return out;
|
|
319
|
+
}
|
|
320
|
+
export function allUnitPaths(fields, scope = []) {
|
|
321
|
+
const out = [];
|
|
322
|
+
for (const el of fields) {
|
|
323
|
+
if (isNamedField(el)) {
|
|
324
|
+
if (isComputeUnit(el.type))
|
|
325
|
+
out.push([...scope, el.key]);
|
|
326
|
+
}
|
|
327
|
+
else if (hasChildren(el)) {
|
|
328
|
+
const p = el.key ? [...scope, el.key] : scope;
|
|
329
|
+
if (el.key && el.compute !== undefined)
|
|
330
|
+
out.push(p);
|
|
331
|
+
out.push(...allUnitPaths(el.fields, p));
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
return out;
|
|
335
|
+
}
|
|
336
|
+
function declaresCache(fm) {
|
|
337
|
+
return fm.cache !== undefined || (fm.parts?.some((p) => p.meta.cache !== undefined) ?? false);
|
|
338
|
+
}
|
|
339
|
+
function cacheUnitPaths(fields, scope = []) {
|
|
340
|
+
const out = [];
|
|
341
|
+
for (const el of fields) {
|
|
342
|
+
if (isNamedField(el)) {
|
|
343
|
+
if (isComputeUnit(el.type) && declaresCache(el.type))
|
|
344
|
+
out.push([...scope, el.key]);
|
|
345
|
+
}
|
|
346
|
+
else if (hasChildren(el)) {
|
|
347
|
+
out.push(...cacheUnitPaths(el.fields, el.key ? [...scope, el.key] : scope));
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
return out;
|
|
351
|
+
}
|
|
352
|
+
export async function runTriggers(fields, row, changed, graph, shelfKey, ctx, opts = {}) {
|
|
353
|
+
const id = row['id'] ?? row['base_id'];
|
|
354
|
+
const st = {
|
|
355
|
+
ctx,
|
|
356
|
+
mode: 'write',
|
|
357
|
+
memo: ctx.memo ?? newPartMemo(),
|
|
358
|
+
refs: ctx.refs ?? null,
|
|
359
|
+
selfKey: typeof id === 'number' ? `${ctx.meta.library}/${ctx.meta.shelf}#${id}` : null,
|
|
360
|
+
recordLevel: null,
|
|
361
|
+
now: ctx.now ?? new Date().toISOString(),
|
|
362
|
+
};
|
|
363
|
+
const root = markDone(newScope(st, { ...row }, fields, [], null, shelfKey));
|
|
364
|
+
st.recordLevel = root.level;
|
|
365
|
+
const queue = [];
|
|
366
|
+
const pending = new Set();
|
|
367
|
+
const materialized = [];
|
|
368
|
+
const cross = opts.crossRecordState ?? {
|
|
369
|
+
visited: new Set(),
|
|
370
|
+
cap: opts.recordCap ?? MAX_TOUCHED_RECORDS,
|
|
371
|
+
...(opts.touched ? { touched: opts.touched } : {}),
|
|
372
|
+
};
|
|
373
|
+
const enqueue = async (target) => {
|
|
374
|
+
if (target.hops.length > 0) {
|
|
375
|
+
await runCrossRecordTarget(target, root.obj, fields, graph, ctx, cross);
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
for (const unit of locateUnits(root, fields, target.path, 0, materialized)) {
|
|
379
|
+
const dedupeKey = `${unit.scope.uid}:${unit.key}`;
|
|
380
|
+
if (pending.has(dedupeKey))
|
|
381
|
+
continue;
|
|
382
|
+
pending.add(dedupeKey);
|
|
383
|
+
queue.push({ scope: unit.scope, key: unit.key, fm: unit.fm, path: target.path, dedupeKey });
|
|
384
|
+
}
|
|
385
|
+
};
|
|
386
|
+
if (opts.seedAll) {
|
|
387
|
+
for (const path of allUnitPaths(fields))
|
|
388
|
+
await enqueue({ hops: [], path, relative: false });
|
|
389
|
+
}
|
|
390
|
+
else if (opts.seedTargets) {
|
|
391
|
+
for (const target of opts.seedTargets)
|
|
392
|
+
await enqueue(target);
|
|
393
|
+
}
|
|
394
|
+
else {
|
|
395
|
+
for (const path of cacheUnitPaths(fields))
|
|
396
|
+
await enqueue({ hops: [], path, relative: false });
|
|
397
|
+
}
|
|
398
|
+
for (const c of changed) {
|
|
399
|
+
await enqueue({ hops: [], path: c.path, relative: false });
|
|
400
|
+
for (const edge of edgesFrom(graph, shelfKey, c.path))
|
|
401
|
+
await enqueue(edge.to);
|
|
402
|
+
}
|
|
403
|
+
let computations = 0;
|
|
404
|
+
while (queue.length > 0) {
|
|
405
|
+
const wave = queue.splice(0, queue.length);
|
|
406
|
+
for (const item of wave)
|
|
407
|
+
pending.delete(item.dedupeKey);
|
|
408
|
+
computations += wave.length;
|
|
409
|
+
if (computations > MAX_UNIT_COMPUTATIONS) {
|
|
410
|
+
throw new TriggerQueueOverflowError(`[trigger-queue] ${shelfKey}: more than ${MAX_UNIT_COMPUTATIONS} unit computations in one drain — ` +
|
|
411
|
+
`a compute function is not converging (this guards the runtime queue; a cycle in the DECLARED ` +
|
|
412
|
+
`graph is rejected at compose time by buildMutateGraph, so this is not that).`);
|
|
413
|
+
}
|
|
414
|
+
const movedPerItem = await Promise.all(wave.map(async (item) => {
|
|
415
|
+
const before = item.scope.obj[item.key];
|
|
416
|
+
await runFieldUnit(item.scope, item.key, item.fm);
|
|
417
|
+
return movedNodes(item, before, item.scope.obj[item.key]);
|
|
418
|
+
}));
|
|
419
|
+
for (const nodes of movedPerItem) {
|
|
420
|
+
for (const node of nodes) {
|
|
421
|
+
for (const edge of edgesFrom(graph, shelfKey, node))
|
|
422
|
+
await enqueue(edge.to);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
for (const g of materialized) {
|
|
427
|
+
const obj = g.parent.obj[g.key];
|
|
428
|
+
if (isPlainObject(obj) && Object.keys(obj).length === 0)
|
|
429
|
+
delete g.parent.obj[g.key];
|
|
430
|
+
}
|
|
431
|
+
return root.obj;
|
|
432
|
+
}
|