@x12i/memorix-service 3.12.4 → 3.14.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/README.md +1 -1
- package/dist/analytics/host.d.ts +2 -2
- package/dist/analytics/host.d.ts.map +1 -1
- package/dist/analytics/host.js.map +1 -1
- package/dist/analytics/read-row.d.ts +33 -0
- package/dist/analytics/read-row.d.ts.map +1 -0
- package/dist/analytics/read-row.js +76 -0
- package/dist/analytics/read-row.js.map +1 -0
- package/dist/analytics/tracker.d.ts +14 -0
- package/dist/analytics/tracker.d.ts.map +1 -1
- package/dist/analytics/tracker.js +55 -17
- package/dist/analytics/tracker.js.map +1 -1
- package/dist/anomaly/catalog.d.ts +4 -0
- package/dist/anomaly/catalog.d.ts.map +1 -0
- package/dist/anomaly/catalog.js +315 -0
- package/dist/anomaly/catalog.js.map +1 -0
- package/dist/anomaly/descriptors.d.ts +11 -0
- package/dist/anomaly/descriptors.d.ts.map +1 -0
- package/dist/anomaly/descriptors.js +113 -0
- package/dist/anomaly/descriptors.js.map +1 -0
- package/dist/anomaly/index.d.ts +6 -0
- package/dist/anomaly/index.d.ts.map +1 -0
- package/dist/anomaly/index.js +5 -0
- package/dist/anomaly/index.js.map +1 -0
- package/dist/anomaly/records.d.ts +28 -0
- package/dist/anomaly/records.d.ts.map +1 -0
- package/dist/anomaly/records.js +384 -0
- package/dist/anomaly/records.js.map +1 -0
- package/dist/anomaly/types.d.ts +116 -0
- package/dist/anomaly/types.d.ts.map +1 -0
- package/dist/anomaly/types.js +24 -0
- package/dist/anomaly/types.js.map +1 -0
- package/dist/anomaly/values.d.ts +31 -0
- package/dist/anomaly/values.d.ts.map +1 -0
- package/dist/anomaly/values.js +171 -0
- package/dist/anomaly/values.js.map +1 -0
- package/dist/app.d.ts.map +1 -1
- package/dist/app.js +12 -0
- package/dist/app.js.map +1 -1
- package/dist/catalog-bridge.d.ts +21 -3
- package/dist/catalog-bridge.d.ts.map +1 -1
- package/dist/catalog-bridge.js +148 -34
- package/dist/catalog-bridge.js.map +1 -1
- package/dist/errors.d.ts +5 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +18 -0
- package/dist/errors.js.map +1 -1
- package/dist/knowledge-upstream.d.ts +32 -0
- package/dist/knowledge-upstream.d.ts.map +1 -1
- package/dist/knowledge-upstream.js +86 -2
- package/dist/knowledge-upstream.js.map +1 -1
- package/dist/mcp/nexum-tools.d.ts +1 -1
- package/dist/mcp/nexum-tools.d.ts.map +1 -1
- package/dist/mcp/nexum-tools.js +39 -22
- package/dist/mcp/nexum-tools.js.map +1 -1
- package/dist/metadata-view.d.ts +30 -0
- package/dist/metadata-view.d.ts.map +1 -0
- package/dist/metadata-view.js +67 -0
- package/dist/metadata-view.js.map +1 -0
- package/dist/nexum-analysis/enqueue.d.ts +1 -1
- package/dist/nexum-analysis/enqueue.d.ts.map +1 -1
- package/dist/nexum-analysis/enqueue.js +4 -2
- package/dist/nexum-analysis/enqueue.js.map +1 -1
- package/dist/nexum-analysis/knowledge-read.d.ts +45 -0
- package/dist/nexum-analysis/knowledge-read.d.ts.map +1 -0
- package/dist/nexum-analysis/knowledge-read.js +26 -0
- package/dist/nexum-analysis/knowledge-read.js.map +1 -0
- package/dist/openapi/openapi.json +18458 -17718
- package/dist/patches/access.d.ts +15 -0
- package/dist/patches/access.d.ts.map +1 -0
- package/dist/patches/access.js +27 -0
- package/dist/patches/access.js.map +1 -0
- package/dist/patches/engine.d.ts +67 -0
- package/dist/patches/engine.d.ts.map +1 -0
- package/dist/patches/engine.js +966 -0
- package/dist/patches/engine.js.map +1 -0
- package/dist/patches/guards.d.ts +23 -0
- package/dist/patches/guards.d.ts.map +1 -0
- package/dist/patches/guards.js +77 -0
- package/dist/patches/guards.js.map +1 -0
- package/dist/patches/legacy-rows.d.ts +56 -0
- package/dist/patches/legacy-rows.d.ts.map +1 -0
- package/dist/patches/legacy-rows.js +162 -0
- package/dist/patches/legacy-rows.js.map +1 -0
- package/dist/patches/ops.d.ts +24 -0
- package/dist/patches/ops.d.ts.map +1 -0
- package/dist/patches/ops.js +204 -0
- package/dist/patches/ops.js.map +1 -0
- package/dist/patches/parse.d.ts +5 -0
- package/dist/patches/parse.d.ts.map +1 -0
- package/dist/patches/parse.js +323 -0
- package/dist/patches/parse.js.map +1 -0
- package/dist/patches/paths.d.ts +38 -0
- package/dist/patches/paths.d.ts.map +1 -0
- package/dist/patches/paths.js +204 -0
- package/dist/patches/paths.js.map +1 -0
- package/dist/patches/service.d.ts +16 -0
- package/dist/patches/service.d.ts.map +1 -0
- package/dist/patches/service.js +128 -0
- package/dist/patches/service.js.map +1 -0
- package/dist/patches/store.d.ts +87 -0
- package/dist/patches/store.d.ts.map +1 -0
- package/dist/patches/store.js +27 -0
- package/dist/patches/store.js.map +1 -0
- package/dist/patches/types.d.ts +112 -0
- package/dist/patches/types.d.ts.map +1 -0
- package/dist/patches/types.js +5 -0
- package/dist/patches/types.js.map +1 -0
- package/dist/platform.d.ts +15 -0
- package/dist/platform.d.ts.map +1 -1
- package/dist/platform.js +141 -8
- package/dist/platform.js.map +1 -1
- package/dist/routes/anomaly.d.ts +5 -0
- package/dist/routes/anomaly.d.ts.map +1 -0
- package/dist/routes/anomaly.js +308 -0
- package/dist/routes/anomaly.js.map +1 -0
- package/dist/routes/data.d.ts.map +1 -1
- package/dist/routes/data.js +297 -198
- package/dist/routes/data.js.map +1 -1
- package/dist/routes/knowledge.d.ts.map +1 -1
- package/dist/routes/knowledge.js +6 -22
- package/dist/routes/knowledge.js.map +1 -1
- package/dist/routes/metadata.d.ts.map +1 -1
- package/dist/routes/metadata.js +28 -1
- package/dist/routes/metadata.js.map +1 -1
- package/dist/routes/patches.d.ts +7 -0
- package/dist/routes/patches.d.ts.map +1 -0
- package/dist/routes/patches.js +111 -0
- package/dist/routes/patches.js.map +1 -0
- package/dist/routes/subject-roots-backfill.d.ts +4 -0
- package/dist/routes/subject-roots-backfill.d.ts.map +1 -0
- package/dist/routes/subject-roots-backfill.js +15 -0
- package/dist/routes/subject-roots-backfill.js.map +1 -0
- package/dist/single-read-discriminator.d.ts +17 -0
- package/dist/single-read-discriminator.d.ts.map +1 -0
- package/dist/single-read-discriminator.js +67 -0
- package/dist/single-read-discriminator.js.map +1 -0
- package/dist/subject-roots-backfill.d.ts +24 -0
- package/dist/subject-roots-backfill.d.ts.map +1 -0
- package/dist/subject-roots-backfill.js +133 -0
- package/dist/subject-roots-backfill.js.map +1 -0
- package/dist/write-precondition.d.ts +5 -1
- package/dist/write-precondition.d.ts.map +1 -1
- package/dist/write-precondition.js +11 -3
- package/dist/write-precondition.js.map +1 -1
- package/dist/writes/content-write.d.ts +207 -0
- package/dist/writes/content-write.d.ts.map +1 -0
- package/dist/writes/content-write.js +629 -0
- package/dist/writes/content-write.js.map +1 -0
- package/dist/writes/effects.d.ts +13 -0
- package/dist/writes/effects.d.ts.map +1 -0
- package/dist/writes/effects.js +28 -0
- package/dist/writes/effects.js.map +1 -0
- package/dist/writes/error-shape.d.ts +11 -0
- package/dist/writes/error-shape.d.ts.map +1 -0
- package/dist/writes/error-shape.js +107 -0
- package/dist/writes/error-shape.js.map +1 -0
- package/dist/writes/queued-write.d.ts +38 -0
- package/dist/writes/queued-write.d.ts.map +1 -0
- package/dist/writes/queued-write.js +288 -0
- package/dist/writes/queued-write.js.map +1 -0
- package/dist/writes/receipt-store.d.ts +131 -0
- package/dist/writes/receipt-store.d.ts.map +1 -0
- package/dist/writes/receipt-store.js +360 -0
- package/dist/writes/receipt-store.js.map +1 -0
- package/dist/writes/worker.d.ts +30 -0
- package/dist/writes/worker.d.ts.map +1 -0
- package/dist/writes/worker.js +100 -0
- package/dist/writes/worker.js.map +1 -0
- package/openapi/openapi.json +18458 -17718
- package/package.json +4 -4
package/dist/routes/data.js
CHANGED
|
@@ -2,13 +2,18 @@ import { parseLiveDeclaration } from "@x12i/memorix-data";
|
|
|
2
2
|
import { resolveScope } from "../request-scope.js";
|
|
3
3
|
import { ServiceError } from "../errors.js";
|
|
4
4
|
import { requireCallbackAccess, requireDeploymentAccess } from "../access-gate.js";
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
5
|
+
import { runContentBatch, runContentWrite } from "../writes/content-write.js";
|
|
6
|
+
import { pendingRecord, publicQueuedReceipt, queueKeyIdFor, readQueueFlag, runQueuedBatch, runQueuedWrite, } from "../writes/queued-write.js";
|
|
7
|
+
import { commitWriteEffects } from "../writes/effects.js";
|
|
8
|
+
import { patchGuardCheck } from "../patches/service.js";
|
|
9
|
+
import { resolveRecordIdAlias } from "../identity-alias.js";
|
|
7
10
|
import { maybeEnqueueNexumAnalysis } from "../nexum-analysis/enqueue.js";
|
|
11
|
+
import { listKnowledgeGraphsForSource } from "../nexum-analysis/knowledge-read.js";
|
|
8
12
|
import { memorixActivityRing, recordLooksLikeKnowledgeGraph, } from "../activity-ring.js";
|
|
9
13
|
import { getRequestCaller, requireCaller } from "../caller-identity.js";
|
|
10
|
-
import { runHookTrigger } from "../hooks/request-trigger.js";
|
|
11
14
|
import { parseSafeFilters, parseSafeFiltersValue, parseSafeSort, parseSafeSortValue } from "../safe-query.js";
|
|
15
|
+
import { declaredDiscriminatorValues } from "../catalog-bridge.js";
|
|
16
|
+
import { singleReadDiscriminatorValue, withDiscriminatorHelp } from "../single-read-discriminator.js";
|
|
12
17
|
function parseIncludeObject(raw) {
|
|
13
18
|
if (raw === undefined)
|
|
14
19
|
return undefined;
|
|
@@ -45,13 +50,27 @@ function subjectRecordIdFromRequest(req) {
|
|
|
45
50
|
}
|
|
46
51
|
return recordId;
|
|
47
52
|
}
|
|
48
|
-
function
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
53
|
+
function publicWriteReceipt(row) {
|
|
54
|
+
return {
|
|
55
|
+
receiptId: row.receiptId,
|
|
56
|
+
status: row.status,
|
|
57
|
+
...(row.writeId ? { writeId: row.writeId } : {}),
|
|
58
|
+
createdAt: row.createdAt,
|
|
59
|
+
updatedAt: row.updatedAt,
|
|
60
|
+
...(row.receipt ? { receipt: row.receipt } : {}),
|
|
61
|
+
...(row.error
|
|
62
|
+
? {
|
|
63
|
+
error: {
|
|
64
|
+
code: row.error.code,
|
|
65
|
+
field: row.error.field,
|
|
66
|
+
message: row.error.message,
|
|
67
|
+
retryable: row.error.retryable,
|
|
68
|
+
...(row.error.retryAfterMs !== undefined ? { retryAfterMs: row.error.retryAfterMs } : {}),
|
|
69
|
+
},
|
|
70
|
+
}
|
|
71
|
+
: {}),
|
|
72
|
+
...(row.status === "pending" ? { retryAfterMs: row.error?.retryAfterMs ?? 1_000 } : {}),
|
|
73
|
+
};
|
|
55
74
|
}
|
|
56
75
|
function contentDatasource(req, body) {
|
|
57
76
|
const service = requireCaller(req);
|
|
@@ -75,7 +94,134 @@ function draftForDatasourceStamp(record) {
|
|
|
75
94
|
...(record.insights ? { insights: record.insights } : {}),
|
|
76
95
|
};
|
|
77
96
|
}
|
|
97
|
+
function readRunStatus(raw) {
|
|
98
|
+
if (raw === undefined || raw === "" || raw === "all")
|
|
99
|
+
return undefined;
|
|
100
|
+
if (raw === "queued" || raw === "stored" || raw === "rejected")
|
|
101
|
+
return raw;
|
|
102
|
+
throw new ServiceError("VALIDATION", "status must be queued, stored or rejected", 400, {
|
|
103
|
+
field: "status",
|
|
104
|
+
retryable: false,
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
/** The cursor is the accept number of the last row on the page. Callers treat it as opaque. */
|
|
108
|
+
function writeReceiptCursor(seq) {
|
|
109
|
+
return Buffer.from(String(seq), "utf8").toString("base64url");
|
|
110
|
+
}
|
|
111
|
+
function readReceiptCursor(raw) {
|
|
112
|
+
if (raw === undefined || raw === "")
|
|
113
|
+
return undefined;
|
|
114
|
+
const seq = Number(Buffer.from(raw, "base64url").toString("utf8"));
|
|
115
|
+
// Only a cursor this service wrote decodes and encodes back to itself. Anything else would
|
|
116
|
+
// silently decode to a number and restart the list from the top.
|
|
117
|
+
if (!Number.isInteger(seq) || seq < 1 || writeReceiptCursor(seq) !== raw) {
|
|
118
|
+
throw new ServiceError("VALIDATION", "cursor is not valid", 400, { field: "cursor", retryable: false });
|
|
119
|
+
}
|
|
120
|
+
return seq;
|
|
121
|
+
}
|
|
122
|
+
function isReadNotFound(err) {
|
|
123
|
+
return Boolean(err && typeof err === "object" && err.code === "READ_NOT_FOUND");
|
|
124
|
+
}
|
|
125
|
+
/** One content instance by recordId; `filter` may only select the discriminator. */
|
|
126
|
+
async function getSingleContent(platform, scope, q, recordId, include) {
|
|
127
|
+
const ports = platform.portsFor(scope);
|
|
128
|
+
const discriminatorPath = ports.catalog.getContentProfile?.(q.contentType)?.discriminator?.path;
|
|
129
|
+
const discriminatorValue = singleReadDiscriminatorValue({
|
|
130
|
+
discriminatorValue: q.discriminatorValue ?? q.discriminator,
|
|
131
|
+
filters: parseSafeFilters(q.filter),
|
|
132
|
+
discriminatorPath,
|
|
133
|
+
});
|
|
134
|
+
const queuedFor = async () => {
|
|
135
|
+
const keyId = queueKeyIdFor(ports.catalog, {
|
|
136
|
+
objectType: q.objectType,
|
|
137
|
+
contentType: q.contentType,
|
|
138
|
+
recordId,
|
|
139
|
+
discriminatorValue,
|
|
140
|
+
});
|
|
141
|
+
return keyId ? platform.writeReceiptsFor(scope.orgId).latestQueuedForKey(scope.orgId, keyId) : null;
|
|
142
|
+
};
|
|
143
|
+
let record;
|
|
144
|
+
try {
|
|
145
|
+
record = (await ports.reader.getContent(scope, {
|
|
146
|
+
objectType: q.objectType,
|
|
147
|
+
contentType: q.contentType,
|
|
148
|
+
recordId,
|
|
149
|
+
...(discriminatorValue ? { discriminatorValue } : {}),
|
|
150
|
+
}, include ? { include } : undefined));
|
|
151
|
+
}
|
|
152
|
+
catch (err) {
|
|
153
|
+
if (isReadNotFound(err)) {
|
|
154
|
+
const pending = await queuedFor();
|
|
155
|
+
if (pending) {
|
|
156
|
+
return {
|
|
157
|
+
record: pendingRecord(pending),
|
|
158
|
+
stored: false,
|
|
159
|
+
pending,
|
|
160
|
+
...(discriminatorValue ? { discriminatorValue } : {}),
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
throw withDiscriminatorHelp(err, () => discriminatorPath
|
|
165
|
+
? declaredDiscriminatorValues(platform.metadata, scope.agentIds, q.objectType, q.contentType, discriminatorPath)
|
|
166
|
+
: undefined);
|
|
167
|
+
}
|
|
168
|
+
const pending = await queuedFor();
|
|
169
|
+
return {
|
|
170
|
+
record,
|
|
171
|
+
stored: true,
|
|
172
|
+
...(discriminatorValue ? { discriminatorValue } : {}),
|
|
173
|
+
...(pending ? { pending } : {}),
|
|
174
|
+
};
|
|
175
|
+
}
|
|
78
176
|
export async function registerDataRoutes(app, platform) {
|
|
177
|
+
app.get("/api/data/knowledge-graphs", async (req) => {
|
|
178
|
+
requireCallbackAccess(req, "explore");
|
|
179
|
+
const scope = resolveScope(req);
|
|
180
|
+
const q = req.query;
|
|
181
|
+
const sourceRecordId = q.sourceRecordId?.trim() ?? "";
|
|
182
|
+
if (!sourceRecordId) {
|
|
183
|
+
throw new ServiceError("VALIDATION", "sourceRecordId required", 400);
|
|
184
|
+
}
|
|
185
|
+
const caller = getRequestCaller(req);
|
|
186
|
+
const ports = platform.portsFor(scope);
|
|
187
|
+
const listed = await listKnowledgeGraphsForSource({
|
|
188
|
+
reader: ports.reader,
|
|
189
|
+
scope,
|
|
190
|
+
sourceRecordId,
|
|
191
|
+
limit: Number(q.limit ?? 50) || 50,
|
|
192
|
+
cursor: q.cursor,
|
|
193
|
+
});
|
|
194
|
+
memorixActivityRing.recordFetch({
|
|
195
|
+
orgId: scope.orgId,
|
|
196
|
+
caller,
|
|
197
|
+
mode: "list",
|
|
198
|
+
objectType: listed.addressing.objectType,
|
|
199
|
+
contentType: listed.addressing.contentType,
|
|
200
|
+
recordId: sourceRecordId,
|
|
201
|
+
hit: listed.records.length > 0,
|
|
202
|
+
count: listed.records.length,
|
|
203
|
+
knowledgeGraph: listed.records.length > 0,
|
|
204
|
+
});
|
|
205
|
+
return { scope, ...listed };
|
|
206
|
+
});
|
|
207
|
+
app.get("/api/data/content/context", async (req) => {
|
|
208
|
+
requireCallbackAccess(req, "explore");
|
|
209
|
+
const scope = resolveScope(req);
|
|
210
|
+
const q = req.query;
|
|
211
|
+
if (!q.objectType || !q.contentType) {
|
|
212
|
+
throw new ServiceError("VALIDATION", "objectType and contentType required", 400);
|
|
213
|
+
}
|
|
214
|
+
const recordId = resolveRecordIdAlias(q.id, q.recordId);
|
|
215
|
+
if (!recordId) {
|
|
216
|
+
throw new ServiceError("VALIDATION", "recordId required", 400);
|
|
217
|
+
}
|
|
218
|
+
const { record } = await getSingleContent(platform, scope, { ...q, objectType: q.objectType, contentType: q.contentType }, recordId);
|
|
219
|
+
const context = record.context;
|
|
220
|
+
if (!context || typeof context !== "object" || Array.isArray(context) || Object.keys(context).length === 0) {
|
|
221
|
+
throw new ServiceError("NOT_FOUND", "context is not stored on this record", 404);
|
|
222
|
+
}
|
|
223
|
+
return context;
|
|
224
|
+
});
|
|
79
225
|
app.get("/api/data/content", async (req) => {
|
|
80
226
|
requireCallbackAccess(req, "explore");
|
|
81
227
|
const scope = resolveScope(req);
|
|
@@ -90,12 +236,11 @@ export async function registerDataRoutes(app, platform) {
|
|
|
90
236
|
const recordId = resolveRecordIdAlias(q.id, q.recordId);
|
|
91
237
|
if (recordId) {
|
|
92
238
|
let record;
|
|
239
|
+
let discriminatorValue;
|
|
240
|
+
let pending;
|
|
241
|
+
let stored = true;
|
|
93
242
|
try {
|
|
94
|
-
record =
|
|
95
|
-
objectType: q.objectType,
|
|
96
|
-
contentType: q.contentType,
|
|
97
|
-
recordId,
|
|
98
|
-
}, { include }));
|
|
243
|
+
({ record, discriminatorValue, pending, stored } = await getSingleContent(platform, scope, { ...q, objectType: q.objectType, contentType: q.contentType }, recordId, include));
|
|
99
244
|
}
|
|
100
245
|
catch (err) {
|
|
101
246
|
const msg = err instanceof Error ? err.message : String(err);
|
|
@@ -141,34 +286,44 @@ export async function registerDataRoutes(app, platform) {
|
|
|
141
286
|
contentType: q.contentType,
|
|
142
287
|
id: recordId,
|
|
143
288
|
recordId,
|
|
289
|
+
...(discriminatorValue ? { discriminatorValue } : {}),
|
|
144
290
|
},
|
|
145
291
|
record,
|
|
146
|
-
|
|
292
|
+
// A key with only a queued submission has no revision yet. null cannot be sent as exact-revision.
|
|
293
|
+
currentRevision: stored ? record.revision : null,
|
|
294
|
+
...(pending ? { pending: true, receiptId: pending.receiptId } : {}),
|
|
147
295
|
unresolvedRelationships,
|
|
148
296
|
};
|
|
149
297
|
}
|
|
150
298
|
const limit = Math.min(Math.max(Number(q.limit ?? 50) || 50, 1), 200);
|
|
151
299
|
const filters = parseSafeFilters(q.filter);
|
|
152
300
|
const sort = parseSafeSort(q.sort);
|
|
153
|
-
if (q.count === "true"
|
|
154
|
-
const
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
301
|
+
if (q.count === "true") {
|
|
302
|
+
const indexed = platform.analytics
|
|
303
|
+
? await platform.analytics.countIndexed(scope, {
|
|
304
|
+
objectType: q.objectType,
|
|
305
|
+
contentType: q.contentType,
|
|
306
|
+
filters,
|
|
307
|
+
})
|
|
308
|
+
: null;
|
|
309
|
+
const count = indexed !== null
|
|
310
|
+
? indexed
|
|
311
|
+
: await ports.reader.countContent(scope, {
|
|
312
|
+
objectType: q.objectType,
|
|
313
|
+
contentType: q.contentType,
|
|
314
|
+
filters,
|
|
315
|
+
});
|
|
316
|
+
return {
|
|
317
|
+
scope,
|
|
318
|
+
count,
|
|
319
|
+
addressing: {
|
|
320
|
+
orgId: scope.orgId,
|
|
321
|
+
objectType: q.objectType,
|
|
322
|
+
contentType: q.contentType,
|
|
323
|
+
id: null,
|
|
324
|
+
recordId: null,
|
|
325
|
+
},
|
|
326
|
+
};
|
|
172
327
|
}
|
|
173
328
|
const listed = await ports.reader.listContent(scope, {
|
|
174
329
|
objectType: q.objectType,
|
|
@@ -237,178 +392,122 @@ export async function registerDataRoutes(app, platform) {
|
|
|
237
392
|
pageInfo: { nextCursor: listed.nextCursor ?? null, limit },
|
|
238
393
|
};
|
|
239
394
|
});
|
|
240
|
-
app.post("/api/data/content", async (req) => {
|
|
395
|
+
app.post("/api/data/content/batch", async (req) => {
|
|
241
396
|
requireDeploymentAccess(req, "write");
|
|
242
397
|
const scope = resolveScope(req);
|
|
243
|
-
const body = req.body;
|
|
244
|
-
if (!body.objectType || !body.contentType) {
|
|
245
|
-
throw new ServiceError("VALIDATION", "objectType, contentType required", 400);
|
|
246
|
-
}
|
|
247
398
|
await platform.analytics?.prepare(scope.orgId);
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
const recordId = body.record.recordId;
|
|
275
|
-
const discriminatorValue = typeof body.discriminatorValue === "string" && body.discriminatorValue.trim()
|
|
276
|
-
? body.discriminatorValue.trim()
|
|
277
|
-
: undefined;
|
|
278
|
-
const noted = await runHookTrigger(() => ports.writer.remove({
|
|
279
|
-
scope,
|
|
280
|
-
objectType,
|
|
281
|
-
contentType,
|
|
282
|
-
recordId,
|
|
283
|
-
...(discriminatorValue ? { discriminatorValue } : {}),
|
|
284
|
-
precondition: { kind: "revision", revision: expectedRevision },
|
|
285
|
-
datasource,
|
|
286
|
-
suppressHooks,
|
|
287
|
-
}));
|
|
288
|
-
const result = noted.value;
|
|
289
|
-
memorixActivityRing.recordLand({
|
|
290
|
-
orgId: scope.orgId,
|
|
291
|
-
caller: getRequestCaller(req),
|
|
292
|
-
objectType: body.objectType,
|
|
293
|
-
contentType: body.contentType,
|
|
294
|
-
recordId: result.recordId,
|
|
295
|
-
revision: result.previousRevision,
|
|
296
|
-
outcome: "removed",
|
|
297
|
-
hooks: noted.hooks,
|
|
298
|
-
});
|
|
299
|
-
return { scope, written: true, outcome: "updated", result, precondition: body.precondition };
|
|
300
|
-
}
|
|
301
|
-
if (!body.record) {
|
|
302
|
-
throw new ServiceError("VALIDATION", "record required", 400);
|
|
303
|
-
}
|
|
304
|
-
const record = {
|
|
305
|
-
...body.record,
|
|
306
|
-
objectType: body.objectType,
|
|
307
|
-
contentType: body.contentType,
|
|
308
|
-
};
|
|
309
|
-
if (record._system && typeof record._system === "object") {
|
|
310
|
-
const system = { ...record._system };
|
|
311
|
-
delete system._datasource;
|
|
312
|
-
record._system = system;
|
|
313
|
-
}
|
|
314
|
-
let result;
|
|
315
|
-
let hooks;
|
|
316
|
-
const objectType = body.objectType;
|
|
317
|
-
const contentType = body.contentType;
|
|
318
|
-
const recordId = body.record.recordId;
|
|
319
|
-
if (op === "add" || precondition.form === "absent") {
|
|
320
|
-
const noted = await runHookTrigger(() => ports.writer.add({
|
|
321
|
-
scope,
|
|
322
|
-
precondition: { kind: "absent" },
|
|
323
|
-
record,
|
|
324
|
-
datasource,
|
|
325
|
-
suppressHooks,
|
|
326
|
-
}));
|
|
327
|
-
result = noted.value;
|
|
328
|
-
hooks = noted.hooks;
|
|
329
|
-
}
|
|
330
|
-
else if (op === "patch") {
|
|
331
|
-
const noted = await runHookTrigger(() => ports.writer.patch({
|
|
332
|
-
scope,
|
|
333
|
-
precondition: { kind: "revision", revision: Number(expectedRevision) },
|
|
334
|
-
objectType,
|
|
335
|
-
contentType,
|
|
336
|
-
recordId,
|
|
337
|
-
ops: body.patch ?? [],
|
|
338
|
-
datasource,
|
|
339
|
-
suppressHooks,
|
|
340
|
-
}));
|
|
341
|
-
result = noted.value;
|
|
342
|
-
hooks = noted.hooks;
|
|
343
|
-
}
|
|
344
|
-
else if (op === "replace") {
|
|
345
|
-
const noted = await runHookTrigger(() => ports.writer.replace({
|
|
346
|
-
scope,
|
|
347
|
-
precondition: { kind: "revision", revision: Number(expectedRevision) },
|
|
348
|
-
record,
|
|
349
|
-
datasource,
|
|
350
|
-
suppressHooks,
|
|
351
|
-
}));
|
|
352
|
-
result = noted.value;
|
|
353
|
-
hooks = noted.hooks;
|
|
354
|
-
}
|
|
355
|
-
else {
|
|
356
|
-
// exact-revision upsert — never silently treat as create
|
|
357
|
-
const noted = await runHookTrigger(() => ports.writer.upsert({
|
|
358
|
-
scope,
|
|
359
|
-
precondition: { kind: "revision", revision: Number(expectedRevision) },
|
|
360
|
-
record,
|
|
361
|
-
datasource,
|
|
362
|
-
suppressHooks,
|
|
363
|
-
}));
|
|
364
|
-
result = noted.value;
|
|
365
|
-
hooks = noted.hooks;
|
|
366
|
-
}
|
|
367
|
-
const indexing = await maybeEnqueueNexumAnalysis({
|
|
368
|
-
platform,
|
|
369
|
-
scope,
|
|
370
|
-
objectType: body.objectType,
|
|
371
|
-
contentType: body.contentType,
|
|
372
|
-
recordId: result.record.recordId,
|
|
373
|
-
revision: result.record.revision,
|
|
374
|
-
data: result.record.data &&
|
|
375
|
-
typeof result.record.data === "object" &&
|
|
376
|
-
!Array.isArray(result.record.data)
|
|
377
|
-
? result.record.data
|
|
378
|
-
: body.record?.data &&
|
|
379
|
-
typeof body.record.data === "object" &&
|
|
380
|
-
!Array.isArray(body.record.data)
|
|
381
|
-
? body.record.data
|
|
382
|
-
: undefined,
|
|
383
|
-
});
|
|
384
|
-
memorixActivityRing.recordLand({
|
|
399
|
+
const caller = requireCaller(req);
|
|
400
|
+
const queue = readQueueFlag(req.body?.queue);
|
|
401
|
+
return (queue ? runQueuedBatch : runContentBatch)({
|
|
402
|
+
scope,
|
|
403
|
+
caller,
|
|
404
|
+
datasource: contentDatasource(req, {}),
|
|
405
|
+
ports: platform.portsFor(scope),
|
|
406
|
+
receipts: platform.writeReceiptsFor(scope.orgId),
|
|
407
|
+
onCommitted: (event) => commitWriteEffects({ platform, scope, caller, event }),
|
|
408
|
+
checkWrite: patchGuardCheck(platform, scope),
|
|
409
|
+
}, req.body, (item) => contentDatasource(req, item));
|
|
410
|
+
});
|
|
411
|
+
app.get("/api/data/receipts", async (req) => {
|
|
412
|
+
requireCallbackAccess(req, "explore");
|
|
413
|
+
const scope = resolveScope(req);
|
|
414
|
+
const caller = requireCaller(req);
|
|
415
|
+
const q = req.query;
|
|
416
|
+
const limit = Math.min(Math.max(Number(q.limit ?? 50) || 50, 1), 200);
|
|
417
|
+
const runId = q.runId?.trim();
|
|
418
|
+
const receipts = platform.writeReceiptsFor(scope.orgId);
|
|
419
|
+
if (runId) {
|
|
420
|
+
// Queued writes of one run, in accept order. Omitting status returns every status.
|
|
421
|
+
const status = readRunStatus(q.status);
|
|
422
|
+
const stepId = q.stepId?.trim() || undefined;
|
|
423
|
+
const after = readReceiptCursor(q.cursor);
|
|
424
|
+
const rows = await receipts.listByRun({
|
|
385
425
|
orgId: scope.orgId,
|
|
386
|
-
caller
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
indexing,
|
|
393
|
-
hooks,
|
|
426
|
+
caller,
|
|
427
|
+
runId,
|
|
428
|
+
...(stepId ? { stepId } : {}),
|
|
429
|
+
...(status ? { status } : {}),
|
|
430
|
+
...(after !== undefined ? { after } : {}),
|
|
431
|
+
limit: limit + 1,
|
|
394
432
|
});
|
|
433
|
+
const page = rows.slice(0, limit);
|
|
434
|
+
const last = page[page.length - 1];
|
|
395
435
|
return {
|
|
396
436
|
scope,
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
indexing,
|
|
437
|
+
receipts: page.map(publicQueuedReceipt),
|
|
438
|
+
pageInfo: {
|
|
439
|
+
nextCursor: rows.length > limit && last?.queue ? writeReceiptCursor(last.queue.seq) : null,
|
|
440
|
+
limit,
|
|
441
|
+
},
|
|
403
442
|
};
|
|
404
443
|
}
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
444
|
+
if (q.stepId?.trim()) {
|
|
445
|
+
throw new ServiceError("VALIDATION", "stepId needs runId", 400, { field: "runId", retryable: false });
|
|
446
|
+
}
|
|
447
|
+
const status = q.status === "pending" ||
|
|
448
|
+
q.status === "queued" ||
|
|
449
|
+
q.status === "stored" ||
|
|
450
|
+
q.status === "rejected" ||
|
|
451
|
+
q.status === "all"
|
|
452
|
+
? q.status
|
|
453
|
+
: "rejected";
|
|
454
|
+
const rows = await receipts.list({
|
|
455
|
+
orgId: scope.orgId,
|
|
456
|
+
caller,
|
|
457
|
+
...(status === "all" ? {} : { status }),
|
|
458
|
+
limit,
|
|
459
|
+
});
|
|
460
|
+
return { scope, receipts: rows.map((row) => (row.queue ? publicQueuedReceipt(row) : publicWriteReceipt(row))) };
|
|
461
|
+
});
|
|
462
|
+
// A static segment wins over `:receiptId`, so `summary` is never read as a receipt id.
|
|
463
|
+
app.get("/api/data/receipts/summary", async (req) => {
|
|
464
|
+
requireCallbackAccess(req, "explore");
|
|
465
|
+
const scope = resolveScope(req);
|
|
466
|
+
const caller = requireCaller(req);
|
|
467
|
+
const q = req.query;
|
|
468
|
+
const runId = q.runId?.trim();
|
|
469
|
+
if (!runId)
|
|
470
|
+
throw new ServiceError("VALIDATION", "runId required", 400, { field: "runId", retryable: false });
|
|
471
|
+
const stepId = q.stepId?.trim() || undefined;
|
|
472
|
+
const counts = await platform
|
|
473
|
+
.writeReceiptsFor(scope.orgId)
|
|
474
|
+
.summarizeRun({ orgId: scope.orgId, caller, runId, ...(stepId ? { stepId } : {}) });
|
|
475
|
+
return { scope, runId, ...(stepId ? { stepId } : {}), ...counts };
|
|
476
|
+
});
|
|
477
|
+
app.get("/api/data/receipts/:receiptId", async (req) => {
|
|
478
|
+
requireCallbackAccess(req, "explore");
|
|
479
|
+
const scope = resolveScope(req);
|
|
480
|
+
const caller = requireCaller(req);
|
|
481
|
+
const receiptId = req.params.receiptId?.trim() ?? "";
|
|
482
|
+
if (!receiptId)
|
|
483
|
+
throw new ServiceError("VALIDATION", "receiptId required", 400, { field: "receiptId" });
|
|
484
|
+
const row = await platform.writeReceiptsFor(scope.orgId).get(scope.orgId, receiptId);
|
|
485
|
+
if (!row || row.caller !== caller) {
|
|
486
|
+
throw new ServiceError("NOT_FOUND", "receipt not found", 404, { field: "receiptId", retryable: false });
|
|
487
|
+
}
|
|
488
|
+
return row.queue ? publicQueuedReceipt(row) : publicWriteReceipt(row);
|
|
489
|
+
});
|
|
490
|
+
app.post("/api/data/content", async (req, reply) => {
|
|
491
|
+
requireDeploymentAccess(req, "write");
|
|
492
|
+
const scope = resolveScope(req);
|
|
493
|
+
const body = (req.body ?? {});
|
|
494
|
+
await platform.analytics?.prepare(scope.orgId);
|
|
495
|
+
const caller = requireCaller(req);
|
|
496
|
+
const queue = readQueueFlag(body.queue);
|
|
497
|
+
const outcome = await (queue ? runQueuedWrite : runContentWrite)({
|
|
498
|
+
scope,
|
|
499
|
+
caller,
|
|
500
|
+
datasource: contentDatasource(req, body),
|
|
501
|
+
ports: platform.portsFor(scope),
|
|
502
|
+
receipts: platform.writeReceiptsFor(scope.orgId),
|
|
503
|
+
onCommitted: (event) => commitWriteEffects({ platform, scope, caller, event }),
|
|
504
|
+
checkWrite: patchGuardCheck(platform, scope),
|
|
505
|
+
}, body);
|
|
506
|
+
if (outcome.kind === "rejected")
|
|
507
|
+
throw outcome.error;
|
|
508
|
+
if (outcome.kind === "pending" || outcome.kind === "queued")
|
|
509
|
+
return reply.code(202).send(outcome.body);
|
|
510
|
+
return outcome.body;
|
|
412
511
|
});
|
|
413
512
|
app.post("/api/data/content/move-fields", async (req) => {
|
|
414
513
|
requireDeploymentAccess(req, "write");
|