@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.
Files changed (101) hide show
  1. package/dist/auth-api.js +4 -5
  2. package/dist/cache-clock.d.ts +8 -0
  3. package/dist/cache-clock.js +103 -0
  4. package/dist/cache-refresh.d.ts +7 -0
  5. package/dist/cache-refresh.js +322 -0
  6. package/dist/collection-io.d.ts +13 -0
  7. package/dist/collection-io.js +40 -29
  8. package/dist/compute-unit.d.ts +66 -0
  9. package/dist/compute-unit.js +369 -0
  10. package/dist/condition-where.js +1 -3
  11. package/dist/data-dir.d.ts +3 -0
  12. package/dist/data-dir.js +39 -0
  13. package/dist/entity-schema.js +5 -2
  14. package/dist/extend-table.d.ts +1 -1
  15. package/dist/extend-table.js +22 -8
  16. package/dist/file-fields.js +50 -23
  17. package/dist/frontend-agent.d.ts +1 -1
  18. package/dist/frontend-agent.js +2 -3
  19. package/dist/global-search.js +1 -1
  20. package/dist/index.js +20 -26
  21. package/dist/mcp-contract/client.d.ts +24 -0
  22. package/dist/mcp-contract/client.js +12 -0
  23. package/dist/mcp-contract/schema.d.ts +46 -0
  24. package/dist/mcp-contract/schema.js +90 -0
  25. package/dist/mcp-contract/tools.d.ts +19 -0
  26. package/dist/mcp-contract/tools.js +160 -0
  27. package/dist/mcp-http.js +4 -2
  28. package/dist/mcp-local.d.ts +1 -1
  29. package/dist/mcp-local.js +1 -1
  30. package/dist/mcp-tools.d.ts +13 -1
  31. package/dist/mcp-tools.js +46 -16
  32. package/dist/migrations.d.ts +5 -0
  33. package/dist/migrations.js +122 -0
  34. package/dist/mutate.js +77 -36
  35. package/dist/orchestrator/agent-capabilities.d.ts +2 -0
  36. package/dist/orchestrator/agent-capabilities.js +201 -0
  37. package/dist/orchestrator/allow.d.ts +16 -0
  38. package/dist/orchestrator/allow.js +65 -0
  39. package/dist/orchestrator/attachments.d.ts +2 -0
  40. package/dist/orchestrator/attachments.js +11 -0
  41. package/dist/orchestrator/config.d.ts +4 -0
  42. package/dist/orchestrator/config.js +19 -0
  43. package/dist/orchestrator/db.d.ts +17 -0
  44. package/dist/orchestrator/db.js +12 -0
  45. package/dist/orchestrator/diagnostics.d.ts +9 -0
  46. package/dist/orchestrator/diagnostics.js +13 -0
  47. package/dist/orchestrator/environment.d.ts +2 -0
  48. package/dist/orchestrator/environment.js +14 -0
  49. package/dist/orchestrator/file-inspection.d.ts +3 -0
  50. package/dist/orchestrator/file-inspection.js +61 -0
  51. package/dist/orchestrator/format.d.ts +1 -0
  52. package/dist/orchestrator/format.js +6 -0
  53. package/dist/orchestrator/index.d.ts +30 -0
  54. package/dist/orchestrator/index.js +56 -0
  55. package/dist/orchestrator/live-message.d.ts +14 -0
  56. package/dist/orchestrator/live-message.js +100 -0
  57. package/dist/orchestrator/pipeline.d.ts +11 -0
  58. package/dist/orchestrator/pipeline.js +198 -0
  59. package/dist/orchestrator/registry.d.ts +13 -0
  60. package/dist/orchestrator/registry.js +74 -0
  61. package/dist/orchestrator/starters.d.ts +19 -0
  62. package/dist/orchestrator/starters.js +81 -0
  63. package/dist/orchestrator/suggestion-capabilities.d.ts +5 -0
  64. package/dist/orchestrator/suggestion-capabilities.js +33 -0
  65. package/dist/orchestrator/system-assembly.d.ts +4 -0
  66. package/dist/orchestrator/system-assembly.js +3 -0
  67. package/dist/orchestrator/system-capabilities.d.ts +2 -0
  68. package/dist/orchestrator/system-capabilities.js +63 -0
  69. package/dist/orchestrator/types.d.ts +141 -0
  70. package/dist/orchestrator/types.js +1 -0
  71. package/dist/part-injection.d.ts +3 -4
  72. package/dist/part-injection.js +25 -368
  73. package/dist/plugin-hooks.d.ts +1 -0
  74. package/dist/plugin-i18n.js +2 -1
  75. package/dist/plugin-runtime.d.ts +2 -2
  76. package/dist/plugin-runtime.js +20 -11
  77. package/dist/plugin-updates.d.ts +2 -2
  78. package/dist/plugin-updates.js +6 -12
  79. package/dist/public-url.js +2 -1
  80. package/dist/recompute-derived.d.ts +2 -1
  81. package/dist/recompute-derived.js +111 -34
  82. package/dist/records-api.js +14 -16
  83. package/dist/registry-context.d.ts +1 -1
  84. package/dist/registry-context.js +3 -0
  85. package/dist/search-index.js +1 -1
  86. package/dist/search-indexer.d.ts +1 -1
  87. package/dist/search-indexer.js +3 -3
  88. package/dist/settings-groups.d.ts +14 -0
  89. package/dist/settings-groups.js +25 -0
  90. package/dist/settings-write.d.ts +4 -4
  91. package/dist/settings-write.js +23 -26
  92. package/dist/system-settings.d.ts +3 -0
  93. package/dist/system-settings.js +27 -0
  94. package/dist/thread-store.d.ts +3 -2
  95. package/dist/thread-store.js +6 -5
  96. package/dist/trigger-queue.d.ts +33 -0
  97. package/dist/trigger-queue.js +432 -0
  98. package/dist/uploads.js +6 -3
  99. package/dist/value-compare.d.ts +1 -0
  100. package/dist/value-compare.js +7 -0
  101. 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/core/search';
9
+ import { tokenize } from '@coffer-org/sdk/search';
9
10
  import { USERS_SHELF } from '@coffer-org/sdk/users-shelf';
10
- const PASSWORD_FIELDS = [field.password({ key: 'password' })];
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
+ }
@@ -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;
@@ -1,12 +1,13 @@
1
1
  import { serialize } from '@mikro-orm/core';
2
- import { collectionGroups, fieldEntries, storageColumns } from '@coffer-org/sdk/shelf';
3
- import { isGroup, isEmbeddedGroup, isJsonStored } from '@coffer-org/sdk/fields';
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 (isGroup(f) && !f.key)
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 fieldEntries(fields)
21
- .filter(([, fm]) => !fm.virtual)
22
- .flatMap(([k, fm]) => storageColumns(k, fm).map(([c]) => c));
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 (isGroup(it) && isEmbeddedGroup(it))
156
+ if (hasChildren(it) && isEmbeddedGroup(it))
136
157
  out.push([it.key, it]);
137
- else if (isGroup(it) && !it.key)
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, subs] of columnsTargets(fields)) {
174
+ for (const [key, fm] of columnsTargets(fields)) {
154
175
  if (!(key in out))
155
176
  continue;
156
- const obj = out[key];
177
+ const value = out[key];
157
178
  delete out[key];
158
- for (const s of subs)
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 [key, subs] of columnsTargets(fields)) {
166
- const nested = {};
167
- let filled = false;
168
- for (const s of subs) {
169
- const col = `${key}__${s}`;
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)) {