@coffer-org/server 3.2.0 → 3.3.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/extend-table.js +10 -3
- package/dist/mutate.js +55 -7
- package/dist/part-injection.d.ts +12 -0
- package/dist/part-injection.js +125 -0
- package/dist/recompute-derived.js +1 -1
- package/package.json +3 -3
package/dist/extend-table.js
CHANGED
|
@@ -4,9 +4,13 @@ import { encodeJsonAt } from "./mutate.js";
|
|
|
4
4
|
import { encodeTemporalAt, decodeTemporalAt } from "./temporal.js";
|
|
5
5
|
import { chunk } from "./batch.js";
|
|
6
6
|
import { selectRows } from "./read-rows.js";
|
|
7
|
+
import { injectParts, applyStoredParts } from "./part-injection.js";
|
|
7
8
|
export function extendEntityName(e) {
|
|
8
9
|
return `extend__${e.id}`;
|
|
9
10
|
}
|
|
11
|
+
function extendPartCtx(e, record) {
|
|
12
|
+
return { record, settings: {}, global: {}, meta: { library: 'extend', shelf: e.id } };
|
|
13
|
+
}
|
|
10
14
|
export async function getExtendRecord(e, baseId, em) {
|
|
11
15
|
const fork = em ?? getEm().fork();
|
|
12
16
|
const name = extendEntityName(e);
|
|
@@ -15,7 +19,8 @@ export async function getExtendRecord(e, baseId, em) {
|
|
|
15
19
|
return undefined;
|
|
16
20
|
const decoded = nestEmbeddedAt(e.fields, decodeTemporalAt(e.fields, flat));
|
|
17
21
|
const collections = await readAt(fork, name, e.fields, baseId);
|
|
18
|
-
|
|
22
|
+
const record = { ...decoded, ...collections };
|
|
23
|
+
return injectParts(e.fields, record, extendPartCtx(e, record));
|
|
19
24
|
}
|
|
20
25
|
export async function getExtendRecords(e, baseIds, em) {
|
|
21
26
|
const out = new Map();
|
|
@@ -34,13 +39,15 @@ export async function getExtendRecords(e, baseIds, em) {
|
|
|
34
39
|
for (const flat of flats) {
|
|
35
40
|
const id = Number(flat.base_id);
|
|
36
41
|
const decoded = nestEmbeddedAt(e.fields, decodeTemporalAt(e.fields, flat));
|
|
37
|
-
|
|
42
|
+
const record = { ...decoded, ...(collectionsById.get(id) ?? {}) };
|
|
43
|
+
out.set(id, await injectParts(e.fields, record, extendPartCtx(e, record)));
|
|
38
44
|
}
|
|
39
45
|
return out;
|
|
40
46
|
}
|
|
41
47
|
export async function upsertExtendRecord(em, e, baseId, data) {
|
|
42
48
|
const name = extendEntityName(e);
|
|
43
|
-
const
|
|
49
|
+
const withParts = await applyStoredParts(e.fields, data, extendPartCtx(e, data));
|
|
50
|
+
const { base, collections } = splitAt(e.fields, withParts);
|
|
44
51
|
const flat = encodeJsonAt(e.fields, encodeTemporalAt(e.fields, flattenEmbeddedAt(e.fields, base)));
|
|
45
52
|
await em.upsert(name, { base_id: baseId, ...flat });
|
|
46
53
|
await writeAt(em, name, e.fields, baseId, collections);
|
package/dist/mutate.js
CHANGED
|
@@ -9,9 +9,27 @@ import { normalizeFileFields, dropUnchangedFileFields, touchesFileFields } from
|
|
|
9
9
|
import { notifyRecordsChanged } from "./index-signal.js";
|
|
10
10
|
import { encodeTemporal, decodeTemporal } from "./temporal.js";
|
|
11
11
|
import { splitCollections, writeCollections, deleteCollections, flattenEmbedded, nestEmbedded, readCollections, } from "./collection-io.js";
|
|
12
|
+
import { injectParts, applyStoredParts } from "./part-injection.js";
|
|
13
|
+
const EMPTY_SETTINGS = {};
|
|
14
|
+
const EMPTY_GLOBAL = {};
|
|
12
15
|
function nowIso() {
|
|
13
16
|
return new Date().toISOString();
|
|
14
17
|
}
|
|
18
|
+
function partCtx(m, record) {
|
|
19
|
+
return {
|
|
20
|
+
record,
|
|
21
|
+
settings: EMPTY_SETTINGS,
|
|
22
|
+
global: EMPTY_GLOBAL,
|
|
23
|
+
meta: { library: m.library, shelf: m.shelf },
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
async function mergeStoredParts(m, record) {
|
|
27
|
+
const computed = await applyStoredParts(m.fields, record, partCtx(m, record));
|
|
28
|
+
if (computed === record)
|
|
29
|
+
return false;
|
|
30
|
+
Object.assign(record, computed);
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
15
33
|
export function encodeJsonAt(fields, data) {
|
|
16
34
|
const out = { ...data };
|
|
17
35
|
for (const [k, f] of fieldEntries(fields)) {
|
|
@@ -67,7 +85,13 @@ export async function getRecord(m, entityName, id, opts = {}) {
|
|
|
67
85
|
const flat = decodeTemporal(m, row);
|
|
68
86
|
const nested = nestEmbedded(m, flat);
|
|
69
87
|
const collections = await readCollections(fork, m, id);
|
|
70
|
-
|
|
88
|
+
const record = { ...nested, ...collections };
|
|
89
|
+
return injectParts(m.fields, record, {
|
|
90
|
+
record,
|
|
91
|
+
settings: EMPTY_SETTINGS,
|
|
92
|
+
global: EMPTY_GLOBAL,
|
|
93
|
+
meta: { library: m.library, shelf: m.shelf },
|
|
94
|
+
});
|
|
71
95
|
}
|
|
72
96
|
export async function createRecord(m, entityName, input, ctx, afterBase) {
|
|
73
97
|
const parsed = buildZodObject(m).safeParse(input);
|
|
@@ -78,6 +102,7 @@ export async function createRecord(m, entityName, input, ctx, afterBase) {
|
|
|
78
102
|
const fileIssues = normalizeFileFields(m, parsedData);
|
|
79
103
|
if (fileIssues.length)
|
|
80
104
|
throw new ValidationError(fileIssues);
|
|
105
|
+
await mergeStoredParts(m, parsedData);
|
|
81
106
|
const { base, collections } = splitCollections(m, { ...parsedData });
|
|
82
107
|
const data = encodeJson(m, encodeTemporal(m, flattenEmbedded(m, { ...base, created_at: ts, updated_at: ts })));
|
|
83
108
|
let id;
|
|
@@ -88,7 +113,7 @@ export async function createRecord(m, entityName, input, ctx, afterBase) {
|
|
|
88
113
|
await tx.flush();
|
|
89
114
|
id = entity.id;
|
|
90
115
|
await writeCollections(tx, m, id, collections);
|
|
91
|
-
const derived = applyDerived(m.fields, { ...parsedData, id });
|
|
116
|
+
const derived = await applyDerived(m.fields, { ...parsedData, id });
|
|
92
117
|
if (Object.keys(derived).length) {
|
|
93
118
|
await tx.nativeUpdate(entityName, { id }, encodeJson(m, encodeTemporal(m, flattenEmbedded(m, derived))));
|
|
94
119
|
Object.assign(parsedData, derived);
|
|
@@ -101,7 +126,13 @@ export async function createRecord(m, entityName, input, ctx, afterBase) {
|
|
|
101
126
|
});
|
|
102
127
|
});
|
|
103
128
|
notifyRecordsChanged();
|
|
104
|
-
|
|
129
|
+
const record = { ...parsedData, id, created_at: ts, updated_at: ts };
|
|
130
|
+
return injectParts(m.fields, record, {
|
|
131
|
+
record,
|
|
132
|
+
settings: EMPTY_SETTINGS,
|
|
133
|
+
global: EMPTY_GLOBAL,
|
|
134
|
+
meta: { library: m.library, shelf: m.shelf },
|
|
135
|
+
});
|
|
105
136
|
}
|
|
106
137
|
export async function updateRecord(m, entityName, id, input, ctx, afterBase) {
|
|
107
138
|
let patch = input;
|
|
@@ -146,11 +177,17 @@ export async function updateRecord(m, entityName, id, input, ctx, afterBase) {
|
|
|
146
177
|
}
|
|
147
178
|
if (reqIssues.length)
|
|
148
179
|
throw new ValidationError(reqIssues);
|
|
180
|
+
const target = { ...merged, ...collections };
|
|
181
|
+
if (await mergeStoredParts(m, target)) {
|
|
182
|
+
const resplit = splitCollections(m, target);
|
|
183
|
+
Object.assign(merged, resplit.base);
|
|
184
|
+
Object.assign(collections, resplit.collections);
|
|
185
|
+
}
|
|
149
186
|
const dbRow = encodeJson(m, encodeTemporal(m, flattenEmbedded(m, { ...merged, updated_at: ts })));
|
|
150
187
|
await tx.upsert(entityName, dbRow);
|
|
151
188
|
await writeCollections(tx, m, id, collections);
|
|
152
189
|
const allCollections = await readCollections(tx, m, id);
|
|
153
|
-
const derived = applyDerived(m.fields, { ...merged, ...allCollections });
|
|
190
|
+
const derived = await applyDerived(m.fields, { ...merged, ...allCollections });
|
|
154
191
|
if (Object.keys(derived).length) {
|
|
155
192
|
await tx.nativeUpdate(entityName, { id }, encodeJson(m, encodeTemporal(m, flattenEmbedded(m, derived))));
|
|
156
193
|
Object.assign(merged, derived);
|
|
@@ -158,7 +195,12 @@ export async function updateRecord(m, entityName, id, input, ctx, afterBase) {
|
|
|
158
195
|
const _extends = afterBase ? await afterBase(tx, id) : undefined;
|
|
159
196
|
const after = { ...merged, ...allCollections, updated_at: ts, ...(_extends ? { _extends } : {}) };
|
|
160
197
|
writeEvent(tx, ctx.actor, 'update', `${m.library}/${m.shelf}`, id, existing, after);
|
|
161
|
-
result = after
|
|
198
|
+
result = await injectParts(m.fields, after, {
|
|
199
|
+
record: after,
|
|
200
|
+
settings: EMPTY_SETTINGS,
|
|
201
|
+
global: EMPTY_GLOBAL,
|
|
202
|
+
meta: { library: m.library, shelf: m.shelf },
|
|
203
|
+
});
|
|
162
204
|
});
|
|
163
205
|
notifyRecordsChanged();
|
|
164
206
|
return result;
|
|
@@ -191,8 +233,14 @@ export async function restoreRecord(m, entityName, id, ctx, afterBase) {
|
|
|
191
233
|
const nested = nestEmbedded(m, flat);
|
|
192
234
|
const collections = await readCollections(tx, m, id);
|
|
193
235
|
const _extends = afterBase ? await afterBase(tx, id) : undefined;
|
|
194
|
-
|
|
195
|
-
writeEvent(tx, ctx.actor, 'restore', `${m.library}/${m.shelf}`, id, null,
|
|
236
|
+
const restored = { ...nested, ...collections, ...(_extends ? { _extends } : {}) };
|
|
237
|
+
writeEvent(tx, ctx.actor, 'restore', `${m.library}/${m.shelf}`, id, null, restored);
|
|
238
|
+
result = await injectParts(m.fields, restored, {
|
|
239
|
+
record: restored,
|
|
240
|
+
settings: EMPTY_SETTINGS,
|
|
241
|
+
global: EMPTY_GLOBAL,
|
|
242
|
+
meta: { library: m.library, shelf: m.shelf },
|
|
243
|
+
});
|
|
196
244
|
});
|
|
197
245
|
notifyRecordsChanged();
|
|
198
246
|
return result;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { type LayoutEl } from '@coffer-org/sdk/fields';
|
|
2
|
+
export interface PartCtx {
|
|
3
|
+
record: Record<string, unknown>;
|
|
4
|
+
settings: Record<string, unknown>;
|
|
5
|
+
global: Record<string, unknown>;
|
|
6
|
+
meta: {
|
|
7
|
+
library: string;
|
|
8
|
+
shelf: string;
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
export declare function injectParts(fields: LayoutEl[], row: Record<string, unknown>, ctx: PartCtx): Promise<Record<string, unknown>>;
|
|
12
|
+
export declare function applyStoredParts(fields: LayoutEl[], row: Record<string, unknown>, ctx: PartCtx): Promise<Record<string, unknown>>;
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { isField, isGroup, isCollectionGroup } from '@coffer-org/sdk/fields';
|
|
2
|
+
async function partValue(p, ctx, self) {
|
|
3
|
+
return typeof p.value === 'function' ? await p.value({ ...ctx, self }) : p.value;
|
|
4
|
+
}
|
|
5
|
+
async function completeOne(key, fm, value, ctx) {
|
|
6
|
+
if (value == null || typeof value !== 'object' || Array.isArray(value))
|
|
7
|
+
return value;
|
|
8
|
+
const self = { ...value };
|
|
9
|
+
for (const p of fm.parts) {
|
|
10
|
+
if (p.mode !== 'pinned' && p.mode !== 'computed')
|
|
11
|
+
continue;
|
|
12
|
+
try {
|
|
13
|
+
self[p.role] = await partValue(p, ctx, self);
|
|
14
|
+
}
|
|
15
|
+
catch (e) {
|
|
16
|
+
console.warn(`[part-injection] '${key}.${p.role}' threw`, e);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return self;
|
|
20
|
+
}
|
|
21
|
+
async function computeOne(key, fm, value, ctx) {
|
|
22
|
+
if (value == null || typeof value !== 'object' || Array.isArray(value))
|
|
23
|
+
return value;
|
|
24
|
+
const self = { ...value };
|
|
25
|
+
const contextOnly = [];
|
|
26
|
+
for (const p of fm.parts) {
|
|
27
|
+
if (p.mode !== 'pinned' && p.mode !== 'computed')
|
|
28
|
+
continue;
|
|
29
|
+
contextOnly.push(p.role);
|
|
30
|
+
try {
|
|
31
|
+
self[p.role] = await partValue(p, ctx, self);
|
|
32
|
+
}
|
|
33
|
+
catch (e) {
|
|
34
|
+
console.warn(`[part-compute] '${key}.${p.role}' threw`, e);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
for (const p of fm.parts) {
|
|
38
|
+
if (p.mode !== 'computedStored')
|
|
39
|
+
continue;
|
|
40
|
+
try {
|
|
41
|
+
self[p.key] = await partValue(p, ctx, self);
|
|
42
|
+
}
|
|
43
|
+
catch (e) {
|
|
44
|
+
console.warn(`[part-compute] '${key}.${p.role}' threw`, e);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
for (const role of contextOnly)
|
|
48
|
+
delete self[role];
|
|
49
|
+
return self;
|
|
50
|
+
}
|
|
51
|
+
async function walkFields(fields, obj, ctx, complete) {
|
|
52
|
+
const out = { ...obj };
|
|
53
|
+
for (const it of fields) {
|
|
54
|
+
if (isField(it)) {
|
|
55
|
+
const fm = it.type;
|
|
56
|
+
if (!fm.parts?.length)
|
|
57
|
+
continue;
|
|
58
|
+
const value = out[it.key];
|
|
59
|
+
if (value == null)
|
|
60
|
+
continue;
|
|
61
|
+
if (fm.hints?.['multiple'] === true) {
|
|
62
|
+
if (!Array.isArray(value))
|
|
63
|
+
continue;
|
|
64
|
+
out[it.key] = await Promise.all(value.map((v) => complete(it.key, fm, v, ctx)));
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
out[it.key] = await complete(it.key, fm, value, ctx);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
else if (isGroup(it)) {
|
|
71
|
+
if (!it.key) {
|
|
72
|
+
Object.assign(out, await walkFields(it.fields, out, ctx, complete));
|
|
73
|
+
}
|
|
74
|
+
else if (isCollectionGroup(it)) {
|
|
75
|
+
const rows = out[it.key];
|
|
76
|
+
if (Array.isArray(rows)) {
|
|
77
|
+
out[it.key] = await Promise.all(rows.map((row) => row != null && typeof row === 'object'
|
|
78
|
+
? walkFields(it.fields, row, ctx, complete)
|
|
79
|
+
: row));
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
else {
|
|
83
|
+
const sub = out[it.key];
|
|
84
|
+
if (sub != null && typeof sub === 'object' && !Array.isArray(sub)) {
|
|
85
|
+
out[it.key] = await walkFields(it.fields, sub, ctx, complete);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return out;
|
|
91
|
+
}
|
|
92
|
+
const partWorkCache = new WeakMap();
|
|
93
|
+
function partWork(fields) {
|
|
94
|
+
const cached = partWorkCache.get(fields);
|
|
95
|
+
if (cached !== undefined)
|
|
96
|
+
return cached;
|
|
97
|
+
const found = { unstored: false, computedStored: false };
|
|
98
|
+
for (const it of fields) {
|
|
99
|
+
if (isField(it)) {
|
|
100
|
+
for (const p of it.type.parts ?? []) {
|
|
101
|
+
if (p.mode === 'pinned' || p.mode === 'computed')
|
|
102
|
+
found.unstored = true;
|
|
103
|
+
else if (p.mode === 'computedStored')
|
|
104
|
+
found.computedStored = true;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
else if (isGroup(it)) {
|
|
108
|
+
const sub = partWork(it.fields);
|
|
109
|
+
found.unstored ||= sub.unstored;
|
|
110
|
+
found.computedStored ||= sub.computedStored;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
partWorkCache.set(fields, found);
|
|
114
|
+
return found;
|
|
115
|
+
}
|
|
116
|
+
export async function injectParts(fields, row, ctx) {
|
|
117
|
+
if (!partWork(fields).unstored)
|
|
118
|
+
return row;
|
|
119
|
+
return walkFields(fields, row, ctx, completeOne);
|
|
120
|
+
}
|
|
121
|
+
export async function applyStoredParts(fields, row, ctx) {
|
|
122
|
+
if (!partWork(fields).computedStored)
|
|
123
|
+
return row;
|
|
124
|
+
return walkFields(fields, row, ctx, computeOne);
|
|
125
|
+
}
|
|
@@ -22,7 +22,7 @@ export async function recomputeDerivedFields(m, entityName) {
|
|
|
22
22
|
const flat = decodeTemporal(m, row);
|
|
23
23
|
const nested = nestEmbedded(m, flat);
|
|
24
24
|
const collections = await readCollections(fork, m, id);
|
|
25
|
-
const derived = applyDerived(m.fields, { ...nested, ...collections });
|
|
25
|
+
const derived = await applyDerived(m.fields, { ...nested, ...collections });
|
|
26
26
|
if (Object.keys(derived).length === 0)
|
|
27
27
|
continue;
|
|
28
28
|
const encoded = encodeJson(m, encodeTemporal(m, flattenEmbedded(m, derived)));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@coffer-org/server",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.3.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"engines": {
|
|
6
6
|
"node": ">=24"
|
|
@@ -24,8 +24,8 @@
|
|
|
24
24
|
"postpack": "node ../../scripts/swap-exports.mjs src"
|
|
25
25
|
},
|
|
26
26
|
"dependencies": {
|
|
27
|
-
"@coffer-org/core": "^3.
|
|
28
|
-
"@coffer-org/sdk": "^3.
|
|
27
|
+
"@coffer-org/core": "^3.3.0",
|
|
28
|
+
"@coffer-org/sdk": "^3.3.0",
|
|
29
29
|
"@extractus/oembed-extractor": "^4.1.0",
|
|
30
30
|
"@fastify/cors": "^11.2.0",
|
|
31
31
|
"@fastify/multipart": "^10.0.0",
|