@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.
@@ -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
- return { ...decoded, ...collections };
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
- out.set(id, { ...decoded, ...(collectionsById.get(id) ?? {}) });
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 { base, collections } = splitAt(e.fields, data);
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
- return { ...nested, ...collections };
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
- return { ...parsedData, id, created_at: ts, updated_at: ts };
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
- result = { ...nested, ...collections, ...(_extends ? { _extends } : {}) };
195
- writeEvent(tx, ctx.actor, 'restore', `${m.library}/${m.shelf}`, id, null, result);
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.2.0",
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.2.0",
28
- "@coffer-org/sdk": "^3.2.0",
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",