@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/auth-api.js
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import { field } from '@coffer-org/sdk/fields';
|
|
2
|
+
import { materializeTree } from '@coffer-org/sdk/materialize/pipeline';
|
|
2
3
|
import { countUsers, createUser, findUserByLogin, findUserById, getPasswordHash, listUsers, updateUser, deleteUser, countAdmins, createSession, resolveSession, deleteSession, createApiToken, resolveApiToken, listApiTokens, revokeApiToken, } from "./auth-store.js";
|
|
3
4
|
import { resolveAccessToken } from "./oauth-store.js";
|
|
4
5
|
import { mcpResource } from "./public-url.js";
|
|
5
6
|
import { verifyPassword } from "./auth-crypto.js";
|
|
6
7
|
import { maskSecrets, preserveSecrets } from "./field-masking.js";
|
|
7
8
|
import { rowMatch } from "./records-api.js";
|
|
8
|
-
import { tokenize } from '@coffer-org/
|
|
9
|
+
import { tokenize } from '@coffer-org/sdk/search';
|
|
9
10
|
import { USERS_SHELF } from '@coffer-org/sdk/users-shelf';
|
|
10
|
-
const PASSWORD_FIELDS =
|
|
11
|
+
const PASSWORD_FIELDS = materializeTree({ password: field.password({}) });
|
|
11
12
|
const SESSION_TTL_MS = 30 * 24 * 60 * 60 * 1000;
|
|
12
13
|
const COOKIE_NAME = 'sid';
|
|
13
14
|
export const PUBLIC_API_PATHS = ['/api/auth/status', '/api/auth/login', '/api/auth/setup'];
|
|
@@ -75,9 +76,7 @@ export async function registerAuthApi(app) {
|
|
|
75
76
|
return reply.code(409).send({ error: 'already_setup' });
|
|
76
77
|
const body = (req.body ?? {});
|
|
77
78
|
if (!body.login || !body.password || !body.displayName) {
|
|
78
|
-
return reply
|
|
79
|
-
.code(422)
|
|
80
|
-
.send({
|
|
79
|
+
return reply.code(422).send({
|
|
81
80
|
issues: [{ field: !body.login ? 'login' : !body.password ? 'password' : 'displayName', code: 'required' }],
|
|
82
81
|
});
|
|
83
82
|
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { LayoutEl } from '@coffer-org/sdk/fields';
|
|
2
|
+
export declare function wholeClockColumn(key: string): string;
|
|
3
|
+
export declare function partClockColumn(key: string, partKey: string): string;
|
|
4
|
+
export declare function cacheClockColumns(fields: LayoutEl[]): string[];
|
|
5
|
+
export declare function levelClockColumnsByOwner(fields: LayoutEl[]): Map<string, string[]>;
|
|
6
|
+
export declare function stripClockKeys(row: Record<string, unknown>): Record<string, unknown>;
|
|
7
|
+
export declare function stripClockKeysDeep(fields: LayoutEl[], row: Record<string, unknown>): Record<string, unknown>;
|
|
8
|
+
export declare function clockValuesOf(row: Record<string, unknown>): Record<string, number>;
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { fieldEntries } from '@coffer-org/sdk/shelf';
|
|
2
|
+
import { hasChildren, isNamedField, isCollectionGroup, isEmbeddedGroup, isStorageGroup } from '@coffer-org/sdk/fields';
|
|
3
|
+
export function wholeClockColumn(key) {
|
|
4
|
+
return `${key}__cachedAt`;
|
|
5
|
+
}
|
|
6
|
+
export function partClockColumn(key, partKey) {
|
|
7
|
+
return `${key}__${partKey}__cachedAt`;
|
|
8
|
+
}
|
|
9
|
+
export function cacheClockColumns(fields) {
|
|
10
|
+
const out = [];
|
|
11
|
+
for (const [key, fm] of fieldEntries(fields)) {
|
|
12
|
+
if (fm.cache !== undefined)
|
|
13
|
+
out.push(wholeClockColumn(key));
|
|
14
|
+
for (const p of fm.parts ?? []) {
|
|
15
|
+
if (p.key != null && p.meta?.cache !== undefined)
|
|
16
|
+
out.push(partClockColumn(key, p.key));
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return out;
|
|
20
|
+
}
|
|
21
|
+
export function levelClockColumnsByOwner(fields) {
|
|
22
|
+
const out = new Map();
|
|
23
|
+
const add = (owner, col) => {
|
|
24
|
+
const list = out.get(owner);
|
|
25
|
+
if (list)
|
|
26
|
+
list.push(col);
|
|
27
|
+
else
|
|
28
|
+
out.set(owner, [col]);
|
|
29
|
+
};
|
|
30
|
+
const scan = (list) => {
|
|
31
|
+
for (const it of list) {
|
|
32
|
+
if (isNamedField(it)) {
|
|
33
|
+
const fm = it.type;
|
|
34
|
+
if (fm.cache !== undefined)
|
|
35
|
+
add(it.key, wholeClockColumn(it.key));
|
|
36
|
+
for (const p of fm.parts ?? []) {
|
|
37
|
+
if (p.key != null && p.meta?.cache !== undefined)
|
|
38
|
+
add(it.key, partClockColumn(it.key, p.key));
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
else if (hasChildren(it) && it.scope === 'hoist') {
|
|
42
|
+
scan(it.fields);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
scan(fields);
|
|
47
|
+
return out;
|
|
48
|
+
}
|
|
49
|
+
function levelClockColumns(fields) {
|
|
50
|
+
return [...levelClockColumnsByOwner(fields).values()].flat();
|
|
51
|
+
}
|
|
52
|
+
export function stripClockKeys(row) {
|
|
53
|
+
for (const k of Object.keys(row))
|
|
54
|
+
if (k.endsWith('__cachedAt'))
|
|
55
|
+
delete row[k];
|
|
56
|
+
return row;
|
|
57
|
+
}
|
|
58
|
+
function levelGroups(fields) {
|
|
59
|
+
const embedded = [];
|
|
60
|
+
const collections = [];
|
|
61
|
+
const scan = (list) => {
|
|
62
|
+
for (const it of list) {
|
|
63
|
+
if (!hasChildren(it))
|
|
64
|
+
continue;
|
|
65
|
+
if (!isStorageGroup(it))
|
|
66
|
+
scan(it.fields);
|
|
67
|
+
else if (isCollectionGroup(it))
|
|
68
|
+
collections.push([it.key, it.fields]);
|
|
69
|
+
else if (isEmbeddedGroup(it))
|
|
70
|
+
embedded.push([it.key, it.fields]);
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
scan(fields);
|
|
74
|
+
return { embedded, collections };
|
|
75
|
+
}
|
|
76
|
+
export function stripClockKeysDeep(fields, row) {
|
|
77
|
+
for (const col of levelClockColumns(fields))
|
|
78
|
+
delete row[col];
|
|
79
|
+
const { embedded, collections } = levelGroups(fields);
|
|
80
|
+
for (const [key, sub] of embedded) {
|
|
81
|
+
const nested = row[key];
|
|
82
|
+
if (nested != null && typeof nested === 'object' && !Array.isArray(nested)) {
|
|
83
|
+
stripClockKeysDeep(sub, nested);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
for (const [key, sub] of collections) {
|
|
87
|
+
const rows = row[key];
|
|
88
|
+
if (Array.isArray(rows)) {
|
|
89
|
+
for (const r of rows)
|
|
90
|
+
if (r != null && typeof r === 'object')
|
|
91
|
+
stripClockKeysDeep(sub, r);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return row;
|
|
95
|
+
}
|
|
96
|
+
export function clockValuesOf(row) {
|
|
97
|
+
const out = {};
|
|
98
|
+
for (const [k, v] of Object.entries(row)) {
|
|
99
|
+
if (k.endsWith('__cachedAt') && typeof v === 'number')
|
|
100
|
+
out[k] = v;
|
|
101
|
+
}
|
|
102
|
+
return out;
|
|
103
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { ShelfDef } from '@coffer-org/sdk/shelf';
|
|
2
|
+
import { type RowMeta } from './collection-io.ts';
|
|
3
|
+
import { type PartCtx } from './part-injection.ts';
|
|
4
|
+
type Row = Record<string, unknown>;
|
|
5
|
+
export declare function stale(cache: number, cachedAt: number | null | undefined, now: number): boolean;
|
|
6
|
+
export declare function refreshCachedUnits(m: ShelfDef, tableName: string, row: Row, clocks: Record<string, number>, collectionMeta: Record<string, RowMeta[]>, ctx: PartCtx): Promise<Row>;
|
|
7
|
+
export {};
|
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
import { fieldEntries, storageColumns, collectionGroups } from '@coffer-org/sdk/shelf';
|
|
2
|
+
import { hasChildren, isNamedField, isCollectionGroup, isStorageGroup, isJsonStored, } from '@coffer-org/sdk/fields';
|
|
3
|
+
import { getEm } from "./db.js";
|
|
4
|
+
import { flattenEmbeddedAt } from "./collection-io.js";
|
|
5
|
+
import { encodeTemporalAt } from "./temporal.js";
|
|
6
|
+
import { wholeClockColumn, partClockColumn, cacheClockColumns } from "./cache-clock.js";
|
|
7
|
+
import { sameValue } from "./value-compare.js";
|
|
8
|
+
import { runTriggers } from "./trigger-queue.js";
|
|
9
|
+
import { getActiveRegistry } from "./registry-context.js";
|
|
10
|
+
export function stale(cache, cachedAt, now) {
|
|
11
|
+
return cache === 0 || cachedAt == null || now - cachedAt >= cache * 1000;
|
|
12
|
+
}
|
|
13
|
+
function collectCacheUnits(fields, scopePath, flatPrefix) {
|
|
14
|
+
const out = [];
|
|
15
|
+
for (const it of fields) {
|
|
16
|
+
if (isNamedField(it)) {
|
|
17
|
+
const fm = it.type;
|
|
18
|
+
const fullKey = `${flatPrefix}${it.key}`;
|
|
19
|
+
if (fm.cache !== undefined) {
|
|
20
|
+
out.push({
|
|
21
|
+
cache: fm.cache,
|
|
22
|
+
clockColumn: wholeClockColumn(fullKey),
|
|
23
|
+
valueColumns: storageColumns(fullKey, fm).map(([col]) => col),
|
|
24
|
+
scopePath,
|
|
25
|
+
fieldKey: it.key,
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
const allPartKeys = (fm.parts ?? []).filter((p) => p.key != null).map((p) => p.key);
|
|
29
|
+
for (const p of fm.parts ?? []) {
|
|
30
|
+
if (p.key != null && p.meta?.cache !== undefined) {
|
|
31
|
+
out.push({
|
|
32
|
+
cache: p.meta.cache,
|
|
33
|
+
clockColumn: partClockColumn(fullKey, p.key),
|
|
34
|
+
valueColumns: [`${fullKey}__${p.key}`],
|
|
35
|
+
scopePath,
|
|
36
|
+
fieldKey: it.key,
|
|
37
|
+
role: p.key,
|
|
38
|
+
allPartKeys,
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
else if (hasChildren(it) && !isCollectionGroup(it)) {
|
|
44
|
+
const sub = it.fields;
|
|
45
|
+
if (!isStorageGroup(it))
|
|
46
|
+
out.push(...collectCacheUnits(sub, scopePath, flatPrefix));
|
|
47
|
+
else
|
|
48
|
+
out.push(...collectCacheUnits(sub, [...scopePath, it.key], `${flatPrefix}${it.key}__`));
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return out;
|
|
52
|
+
}
|
|
53
|
+
function collectCollectionGroups(fields, tablePrefix) {
|
|
54
|
+
return collectionGroups(fields).map((c) => {
|
|
55
|
+
const table = `${tablePrefix}__${c.key}`;
|
|
56
|
+
const childFields = c.group.fields;
|
|
57
|
+
return {
|
|
58
|
+
key: c.key,
|
|
59
|
+
table,
|
|
60
|
+
fields: childFields,
|
|
61
|
+
units: collectCacheUnits(childFields, [], ''),
|
|
62
|
+
children: collectCollectionGroups(childFields, table),
|
|
63
|
+
};
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
function anyCollectionWork(groups) {
|
|
67
|
+
return groups.some((g) => g.units.length > 0 || anyCollectionWork(g.children));
|
|
68
|
+
}
|
|
69
|
+
const infoCache = new WeakMap();
|
|
70
|
+
function shelfCacheInfoOf(m) {
|
|
71
|
+
let info = infoCache.get(m.fields);
|
|
72
|
+
if (!info) {
|
|
73
|
+
info = {
|
|
74
|
+
base: collectCacheUnits(m.fields, [], ''),
|
|
75
|
+
collections: collectCollectionGroups(m.fields, `${m.library}__${m.shelf}`),
|
|
76
|
+
};
|
|
77
|
+
infoCache.set(m.fields, info);
|
|
78
|
+
}
|
|
79
|
+
return info;
|
|
80
|
+
}
|
|
81
|
+
function isPlainRow(v) {
|
|
82
|
+
return v != null && typeof v === 'object' && !Array.isArray(v);
|
|
83
|
+
}
|
|
84
|
+
function scopeAt(obj, path) {
|
|
85
|
+
let cur = obj;
|
|
86
|
+
for (const seg of path) {
|
|
87
|
+
if (cur == null)
|
|
88
|
+
return undefined;
|
|
89
|
+
if (typeof seg === 'number') {
|
|
90
|
+
if (!Array.isArray(cur))
|
|
91
|
+
return undefined;
|
|
92
|
+
cur = cur[seg];
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
if (typeof cur !== 'object' || Array.isArray(cur))
|
|
96
|
+
return undefined;
|
|
97
|
+
cur = cur[seg];
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return cur;
|
|
101
|
+
}
|
|
102
|
+
function readUnitValue(u, obj, groupChain = []) {
|
|
103
|
+
const scope = scopeAt(obj, [...groupChain, ...u.scopePath]);
|
|
104
|
+
if (!scope)
|
|
105
|
+
return undefined;
|
|
106
|
+
if (u.role === undefined)
|
|
107
|
+
return scope[u.fieldKey];
|
|
108
|
+
const composite = scope[u.fieldKey];
|
|
109
|
+
return composite != null && typeof composite === 'object' ? composite[u.role] : undefined;
|
|
110
|
+
}
|
|
111
|
+
function readUnitClock(u, obj, groupChain = []) {
|
|
112
|
+
const scope = scopeAt(obj, [...groupChain, ...u.scopePath]);
|
|
113
|
+
if (!scope)
|
|
114
|
+
return undefined;
|
|
115
|
+
const localClockKey = u.role === undefined ? wholeClockColumn(u.fieldKey) : partClockColumn(u.fieldKey, u.role);
|
|
116
|
+
const v = scope[localClockKey];
|
|
117
|
+
return typeof v === 'number' ? v : undefined;
|
|
118
|
+
}
|
|
119
|
+
function withValueAt(container, path, key, value) {
|
|
120
|
+
const [head, ...rest] = path;
|
|
121
|
+
if (head === undefined) {
|
|
122
|
+
const base = isPlainRow(container) ? container : {};
|
|
123
|
+
return { ...base, [key]: value };
|
|
124
|
+
}
|
|
125
|
+
if (typeof head === 'number') {
|
|
126
|
+
const arr = Array.isArray(container) ? container : [];
|
|
127
|
+
const next = arr.slice();
|
|
128
|
+
next[head] = withValueAt(arr[head], rest, key, value);
|
|
129
|
+
return next;
|
|
130
|
+
}
|
|
131
|
+
const obj = isPlainRow(container) ? container : {};
|
|
132
|
+
return { ...obj, [head]: withValueAt(obj[head], rest, key, value) };
|
|
133
|
+
}
|
|
134
|
+
function withUnitValue(row, groupChain, u, value) {
|
|
135
|
+
const path = [...groupChain, ...u.scopePath];
|
|
136
|
+
if (u.role === undefined)
|
|
137
|
+
return withValueAt(row, path, u.fieldKey, value);
|
|
138
|
+
const scope = scopeAt(row, path);
|
|
139
|
+
const existing = scope ? scope[u.fieldKey] : undefined;
|
|
140
|
+
const composite = isPlainRow(existing)
|
|
141
|
+
? { ...existing }
|
|
142
|
+
: Object.fromEntries((u.allPartKeys ?? [u.role]).map((k) => [k, null]));
|
|
143
|
+
composite[u.role] = value;
|
|
144
|
+
return withValueAt(row, path, u.fieldKey, composite);
|
|
145
|
+
}
|
|
146
|
+
function encodeJsonForColumns(fields, row) {
|
|
147
|
+
const out = { ...row };
|
|
148
|
+
for (const [k, f] of fieldEntries(fields)) {
|
|
149
|
+
if (isJsonStored(f) && out[k] !== null && out[k] !== undefined && typeof out[k] === 'object') {
|
|
150
|
+
out[k] = JSON.stringify(out[k]);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return out;
|
|
154
|
+
}
|
|
155
|
+
function flatColumnsOf(fields, obj) {
|
|
156
|
+
return encodeJsonForColumns(fields, encodeTemporalAt(fields, flattenEmbeddedAt(fields, obj)));
|
|
157
|
+
}
|
|
158
|
+
function unitValueSame(u, before, after) {
|
|
159
|
+
return u.valueColumns.every((col) => sameValue(before[col] ?? null, after[col] ?? null));
|
|
160
|
+
}
|
|
161
|
+
function nowIso() {
|
|
162
|
+
return new Date().toISOString();
|
|
163
|
+
}
|
|
164
|
+
function planCollectionStale(groups, record, meta, prefixChain, now, out) {
|
|
165
|
+
for (const g of groups) {
|
|
166
|
+
const rows = record[g.key] ?? [];
|
|
167
|
+
const metas = meta[g.key] ?? [];
|
|
168
|
+
rows.forEach((rowObj, i) => {
|
|
169
|
+
const rm = metas[i];
|
|
170
|
+
if (!rm)
|
|
171
|
+
return;
|
|
172
|
+
const chain = [...prefixChain, g.key, i];
|
|
173
|
+
for (const u of g.units) {
|
|
174
|
+
const prevClock = rm.clocks[u.clockColumn] ?? null;
|
|
175
|
+
if (stale(u.cache, prevClock, now)) {
|
|
176
|
+
out.push({ table: g.table, rowId: rm.id, fields: g.fields, groupChain: chain, unit: u, prevClock });
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
if (g.children.length && isPlainRow(rowObj)) {
|
|
180
|
+
planCollectionStale(g.children, rowObj, rm.nested, chain, now, out);
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
function groupByRow(entries) {
|
|
186
|
+
const out = new Map();
|
|
187
|
+
for (const e of entries) {
|
|
188
|
+
const k = `${e.table}#${e.rowId}`;
|
|
189
|
+
let g = out.get(k);
|
|
190
|
+
if (!g) {
|
|
191
|
+
g = { table: e.table, rowId: e.rowId, fields: e.fields, groupChain: e.groupChain, entries: [] };
|
|
192
|
+
out.set(k, g);
|
|
193
|
+
}
|
|
194
|
+
g.entries.push(e);
|
|
195
|
+
}
|
|
196
|
+
return out;
|
|
197
|
+
}
|
|
198
|
+
function cacheUnitPath(u) {
|
|
199
|
+
return u.role === undefined ? [...u.scopePath, u.fieldKey] : [...u.scopePath, u.fieldKey, u.role];
|
|
200
|
+
}
|
|
201
|
+
function chainKeys(chain) {
|
|
202
|
+
return chain.filter((s) => typeof s === 'string');
|
|
203
|
+
}
|
|
204
|
+
export async function refreshCachedUnits(m, tableName, row, clocks, collectionMeta, ctx) {
|
|
205
|
+
const info = shelfCacheInfoOf(m);
|
|
206
|
+
if (info.base.length === 0 && !anyCollectionWork(info.collections))
|
|
207
|
+
return row;
|
|
208
|
+
const now = Date.now();
|
|
209
|
+
const baseStale = info.base.filter((u) => stale(u.cache, clocks[u.clockColumn] ?? null, now));
|
|
210
|
+
const collectionStale = [];
|
|
211
|
+
planCollectionStale(info.collections, row, collectionMeta, [], now, collectionStale);
|
|
212
|
+
if (baseStale.length === 0 && collectionStale.length === 0)
|
|
213
|
+
return row;
|
|
214
|
+
const graph = getActiveRegistry().mutateGraph;
|
|
215
|
+
const shelfKey = `${m.library}/${m.shelf}`;
|
|
216
|
+
const seedTargets = [
|
|
217
|
+
...baseStale.map((u) => ({ hops: [], path: cacheUnitPath(u), relative: false })),
|
|
218
|
+
...collectionStale.map((e) => ({
|
|
219
|
+
hops: [],
|
|
220
|
+
path: [...chainKeys(e.groupChain), ...cacheUnitPath(e.unit)],
|
|
221
|
+
relative: false,
|
|
222
|
+
})),
|
|
223
|
+
];
|
|
224
|
+
const recomputed = await runTriggers(m.fields, row, [], graph, shelfKey, ctx, { seedTargets });
|
|
225
|
+
const baseFinished = baseStale.filter((u) => readUnitClock(u, recomputed) !== undefined);
|
|
226
|
+
let baseChanged = [];
|
|
227
|
+
let flatRecomputedBase;
|
|
228
|
+
if (baseFinished.length > 0) {
|
|
229
|
+
flatRecomputedBase = flatColumnsOf(m.fields, recomputed);
|
|
230
|
+
const flatBeforeBase = flatColumnsOf(m.fields, row);
|
|
231
|
+
baseChanged = baseFinished.filter((u) => !unitValueSame(u, flatBeforeBase, flatRecomputedBase));
|
|
232
|
+
}
|
|
233
|
+
const collectionWrites = [];
|
|
234
|
+
for (const group of groupByRow(collectionStale).values()) {
|
|
235
|
+
const rowRecomputed = scopeAt(recomputed, group.groupChain);
|
|
236
|
+
if (!rowRecomputed)
|
|
237
|
+
continue;
|
|
238
|
+
const finished = group.entries.filter((e) => readUnitClock(e.unit, rowRecomputed) !== undefined);
|
|
239
|
+
if (finished.length === 0)
|
|
240
|
+
continue;
|
|
241
|
+
const rowBefore = scopeAt(row, group.groupChain);
|
|
242
|
+
if (!rowBefore)
|
|
243
|
+
continue;
|
|
244
|
+
const flatRow = flatColumnsOf(group.fields, rowRecomputed);
|
|
245
|
+
const flatBeforeRow = flatColumnsOf(group.fields, rowBefore);
|
|
246
|
+
const changed = new Set(finished.filter((e) => !unitValueSame(e.unit, flatBeforeRow, flatRow)));
|
|
247
|
+
collectionWrites.push({ group, finished, changed, flatRow });
|
|
248
|
+
}
|
|
249
|
+
if (baseFinished.length === 0 && collectionWrites.length === 0)
|
|
250
|
+
return row;
|
|
251
|
+
let out = row;
|
|
252
|
+
const ts = nowIso();
|
|
253
|
+
const persist = async (em) => {
|
|
254
|
+
if (baseFinished.length > 0) {
|
|
255
|
+
const dbPatch = {};
|
|
256
|
+
for (const u of baseFinished)
|
|
257
|
+
dbPatch[u.clockColumn] = readUnitClock(u, recomputed);
|
|
258
|
+
for (const u of baseChanged)
|
|
259
|
+
for (const col of u.valueColumns)
|
|
260
|
+
dbPatch[col] = flatRecomputedBase[col] ?? null;
|
|
261
|
+
if (baseChanged.length > 0)
|
|
262
|
+
dbPatch['updated_at'] = ts;
|
|
263
|
+
const id = row['id'];
|
|
264
|
+
const affected = await em.nativeUpdate(tableName, { id, updated_at: row['updated_at'] }, dbPatch);
|
|
265
|
+
if (affected > 0) {
|
|
266
|
+
for (const u of baseFinished)
|
|
267
|
+
out = withUnitValue(out, [], u, readUnitValue(u, recomputed));
|
|
268
|
+
if (baseChanged.length > 0)
|
|
269
|
+
out = { ...out, updated_at: ts };
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
for (const { group, finished, changed, flatRow } of collectionWrites) {
|
|
273
|
+
const rowRecomputed = scopeAt(recomputed, group.groupChain);
|
|
274
|
+
const dbPatch = {};
|
|
275
|
+
const where = { id: group.rowId };
|
|
276
|
+
for (const e of finished) {
|
|
277
|
+
dbPatch[e.unit.clockColumn] = readUnitClock(e.unit, rowRecomputed);
|
|
278
|
+
where[e.unit.clockColumn] = e.prevClock;
|
|
279
|
+
if (changed.has(e))
|
|
280
|
+
for (const col of e.unit.valueColumns)
|
|
281
|
+
dbPatch[col] = flatRow[col] ?? null;
|
|
282
|
+
}
|
|
283
|
+
const affected = await em.nativeUpdate(group.table, where, dbPatch);
|
|
284
|
+
if (affected > 0) {
|
|
285
|
+
for (const e of finished) {
|
|
286
|
+
out = withUnitValue(out, group.groupChain, e.unit, readUnitValue(e.unit, rowRecomputed));
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
const changed = [
|
|
291
|
+
...baseChanged.map((u) => ({ path: cacheUnitPath(u) })),
|
|
292
|
+
...collectionWrites.flatMap(({ group, changed: rowChanged }) => [...rowChanged].map((e) => ({ path: [...chainKeys(group.groupChain), ...cacheUnitPath(e.unit)] }))),
|
|
293
|
+
];
|
|
294
|
+
if (changed.length > 0) {
|
|
295
|
+
const triggered = await runTriggers(m.fields, recomputed, changed, graph, shelfKey, { ...ctx, em }, {
|
|
296
|
+
seedTargets: [],
|
|
297
|
+
});
|
|
298
|
+
const clockCols = new Set(cacheClockColumns(m.fields));
|
|
299
|
+
const collKeys = new Set(collectionGroups(m.fields).map((c) => c.key));
|
|
300
|
+
const beforeFlat = flatColumnsOf(m.fields, recomputed);
|
|
301
|
+
const afterFlat = flatColumnsOf(m.fields, triggered);
|
|
302
|
+
const cascadePatch = {};
|
|
303
|
+
for (const [k, v] of Object.entries(afterFlat)) {
|
|
304
|
+
if (collKeys.has(k) || clockCols.has(k))
|
|
305
|
+
continue;
|
|
306
|
+
if (!sameValue(beforeFlat[k] ?? null, v ?? null))
|
|
307
|
+
cascadePatch[k] = v ?? null;
|
|
308
|
+
}
|
|
309
|
+
if (Object.keys(cascadePatch).length > 0) {
|
|
310
|
+
const id = row['id'];
|
|
311
|
+
await em.nativeUpdate(tableName, { id }, cascadePatch);
|
|
312
|
+
for (const k of Object.keys(cascadePatch))
|
|
313
|
+
out = { ...out, [k]: triggered[k] };
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
};
|
|
317
|
+
if (ctx.em)
|
|
318
|
+
await persist(ctx.em);
|
|
319
|
+
else
|
|
320
|
+
await getEm().fork().transactional(persist);
|
|
321
|
+
return out;
|
|
322
|
+
}
|
package/dist/collection-io.d.ts
CHANGED
|
@@ -9,6 +9,15 @@ export declare function splitAt(fields: LayoutEl[], input: Row): {
|
|
|
9
9
|
};
|
|
10
10
|
export declare function writeAt(tx: EntityManager, prefix: string, fields: LayoutEl[], parentId: number, collections: Record<string, Row[]>): Promise<void>;
|
|
11
11
|
export declare function readAt(em: EntityManager, prefix: string, fields: LayoutEl[], parentId: number): Promise<Record<string, Row[]>>;
|
|
12
|
+
export interface RowMeta {
|
|
13
|
+
id: number;
|
|
14
|
+
clocks: Record<string, number>;
|
|
15
|
+
nested: Record<string, RowMeta[]>;
|
|
16
|
+
}
|
|
17
|
+
export declare function readAtWithMeta(em: EntityManager, prefix: string, fields: LayoutEl[], parentId: number): Promise<{
|
|
18
|
+
collections: Record<string, Row[]>;
|
|
19
|
+
meta: Record<string, RowMeta[]>;
|
|
20
|
+
}>;
|
|
12
21
|
export declare function readAtMany(em: EntityManager, prefix: string, fields: LayoutEl[], parentIds: number[]): Promise<Map<number, Record<string, Row[]>>>;
|
|
13
22
|
export declare function deleteAt(tx: EntityManager, prefix: string, fields: LayoutEl[], parentId: number): Promise<void>;
|
|
14
23
|
export declare const splitCollections: (m: ShelfDef, input: Row) => {
|
|
@@ -17,6 +26,10 @@ export declare const splitCollections: (m: ShelfDef, input: Row) => {
|
|
|
17
26
|
};
|
|
18
27
|
export declare const writeCollections: (tx: EntityManager, m: ShelfDef, parentId: number, collections: Record<string, Row[]>) => Promise<void>;
|
|
19
28
|
export declare const readCollections: (em: EntityManager, m: ShelfDef, parentId: number) => Promise<Record<string, Row[]>>;
|
|
29
|
+
export declare const readCollectionsWithMeta: (em: EntityManager, m: ShelfDef, parentId: number) => Promise<{
|
|
30
|
+
collections: Record<string, Row[]>;
|
|
31
|
+
meta: Record<string, RowMeta[]>;
|
|
32
|
+
}>;
|
|
20
33
|
export declare const deleteCollections: (tx: EntityManager, m: ShelfDef, parentId: number) => Promise<void>;
|
|
21
34
|
export declare function flattenEmbeddedAt(fields: LayoutEl[], input: Row): Row;
|
|
22
35
|
export declare function nestEmbeddedAt(fields: LayoutEl[], row: Row): Row;
|
package/dist/collection-io.js
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { serialize } from '@mikro-orm/core';
|
|
2
|
-
import { collectionGroups, fieldEntries, storageColumns } from '@coffer-org/sdk/shelf';
|
|
3
|
-
import {
|
|
2
|
+
import { collectionGroups, fieldEntries, flattenComposite, nestComposite, storageColumns } from '@coffer-org/sdk/shelf';
|
|
3
|
+
import { hasChildren, isEmbeddedGroup, isJsonStored } from '@coffer-org/sdk/fields';
|
|
4
4
|
import { chunk } from "./batch.js";
|
|
5
5
|
import { selectRows } from "./read-rows.js";
|
|
6
|
+
import { cacheClockColumns, clockValuesOf } from "./cache-clock.js";
|
|
6
7
|
function flattenLayout(fields) {
|
|
7
8
|
const out = [];
|
|
8
9
|
for (const f of fields) {
|
|
9
|
-
if (
|
|
10
|
+
if (hasChildren(f) && f.scope === 'hoist')
|
|
10
11
|
out.push(...flattenLayout(f.fields));
|
|
11
12
|
else
|
|
12
13
|
out.push(f);
|
|
@@ -17,9 +18,10 @@ function collKeySet(fields) {
|
|
|
17
18
|
return new Set(collectionGroups(flattenLayout(fields)).map((c) => c.key));
|
|
18
19
|
}
|
|
19
20
|
export function scalarKeys(fields) {
|
|
20
|
-
return
|
|
21
|
-
.
|
|
22
|
-
|
|
21
|
+
return [
|
|
22
|
+
...fieldEntries(fields).flatMap(([k, fm]) => storageColumns(k, fm).map(([c]) => c)),
|
|
23
|
+
...cacheClockColumns(fields),
|
|
24
|
+
];
|
|
23
25
|
}
|
|
24
26
|
export function splitAt(fields, input) {
|
|
25
27
|
const keys = collKeySet(fields);
|
|
@@ -83,6 +85,26 @@ export async function readAt(em, prefix, fields, parentId) {
|
|
|
83
85
|
}
|
|
84
86
|
return out;
|
|
85
87
|
}
|
|
88
|
+
export async function readAtWithMeta(em, prefix, fields, parentId) {
|
|
89
|
+
const collections = {};
|
|
90
|
+
const meta = {};
|
|
91
|
+
for (const c of collectionGroups(fields)) {
|
|
92
|
+
const table = `${prefix}__${c.key}`;
|
|
93
|
+
const childFields = c.group.fields;
|
|
94
|
+
const rows = (await selectRows(em, table, { parent_id: parentId }, { orderBy: { position: 'asc' } }));
|
|
95
|
+
const acc = [];
|
|
96
|
+
const metaAcc = [];
|
|
97
|
+
for (const row of rows) {
|
|
98
|
+
const { id, parent_id: _pid, position: _pos, ...rest } = row;
|
|
99
|
+
const { collections: nestedColl, meta: nestedMeta } = await readAtWithMeta(em, table, childFields, id);
|
|
100
|
+
acc.push({ ...nestEmbeddedAt(childFields, rest), ...nestedColl });
|
|
101
|
+
metaAcc.push({ id: id, clocks: clockValuesOf(rest), nested: nestedMeta });
|
|
102
|
+
}
|
|
103
|
+
collections[c.key] = acc;
|
|
104
|
+
meta[c.key] = metaAcc;
|
|
105
|
+
}
|
|
106
|
+
return { collections, meta };
|
|
107
|
+
}
|
|
86
108
|
export async function readAtMany(em, prefix, fields, parentIds) {
|
|
87
109
|
const out = new Map();
|
|
88
110
|
for (const id of parentIds)
|
|
@@ -123,18 +145,17 @@ const modPrefix = (m) => `${m.library}__${m.shelf}`;
|
|
|
123
145
|
export const splitCollections = (m, input) => splitAt(m.fields, input);
|
|
124
146
|
export const writeCollections = (tx, m, parentId, collections) => writeAt(tx, modPrefix(m), m.fields, parentId, collections);
|
|
125
147
|
export const readCollections = (em, m, parentId) => readAt(em, modPrefix(m), m.fields, parentId);
|
|
148
|
+
export const readCollectionsWithMeta = (em, m, parentId) => readAtWithMeta(em, modPrefix(m), m.fields, parentId);
|
|
126
149
|
export const deleteCollections = (tx, m, parentId) => deleteAt(tx, modPrefix(m), m.fields, parentId);
|
|
127
150
|
function columnsTargets(fields) {
|
|
128
|
-
return fieldEntries(fields)
|
|
129
|
-
.filter(([, fm]) => fm.columns)
|
|
130
|
-
.map(([k, fm]) => [k, Object.keys(fm.columns)]);
|
|
151
|
+
return fieldEntries(fields).filter(([, fm]) => fm.columns);
|
|
131
152
|
}
|
|
132
153
|
function embeddedGroups(fields) {
|
|
133
154
|
const out = [];
|
|
134
155
|
for (const it of fields) {
|
|
135
|
-
if (
|
|
156
|
+
if (hasChildren(it) && isEmbeddedGroup(it))
|
|
136
157
|
out.push([it.key, it]);
|
|
137
|
-
else if (
|
|
158
|
+
else if (hasChildren(it) && it.scope === 'hoist')
|
|
138
159
|
out.push(...embeddedGroups(it.fields));
|
|
139
160
|
}
|
|
140
161
|
return out;
|
|
@@ -150,32 +171,22 @@ export function flattenEmbeddedAt(fields, input) {
|
|
|
150
171
|
out[`${key}__${sub}`] = val;
|
|
151
172
|
}
|
|
152
173
|
}
|
|
153
|
-
for (const [key,
|
|
174
|
+
for (const [key, fm] of columnsTargets(fields)) {
|
|
154
175
|
if (!(key in out))
|
|
155
176
|
continue;
|
|
156
|
-
const
|
|
177
|
+
const value = out[key];
|
|
157
178
|
delete out[key];
|
|
158
|
-
|
|
159
|
-
out[`${key}__${s}`] = obj && typeof obj === 'object' ? obj[s] : null;
|
|
179
|
+
Object.assign(out, flattenComposite(fm, key, value));
|
|
160
180
|
}
|
|
161
181
|
return out;
|
|
162
182
|
}
|
|
163
183
|
export function nestEmbeddedAt(fields, row) {
|
|
164
184
|
const out = { ...row };
|
|
165
|
-
for (const
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
if (!(col in out))
|
|
171
|
-
continue;
|
|
172
|
-
const v = out[col];
|
|
173
|
-
nested[s] = v;
|
|
174
|
-
delete out[col];
|
|
175
|
-
if (v != null && v !== '')
|
|
176
|
-
filled = true;
|
|
177
|
-
}
|
|
178
|
-
if (filled)
|
|
185
|
+
for (const col of cacheClockColumns(fields))
|
|
186
|
+
delete out[col];
|
|
187
|
+
for (const [key, fm] of columnsTargets(fields)) {
|
|
188
|
+
const nested = nestComposite(fm, key, out, (col) => delete out[col]);
|
|
189
|
+
if (nested !== undefined)
|
|
179
190
|
out[key] = nested;
|
|
180
191
|
}
|
|
181
192
|
for (const [key, g] of embeddedGroups(fields)) {
|