@rubytech/create-maxy 1.0.657 → 1.0.659
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/package.json +1 -1
- package/payload/platform/package-lock.json +49 -1837
- package/payload/platform/plugins/cloudflare/scripts/list-cf-domains.ts +115 -61
- package/payload/platform/plugins/docs/references/platform.md +1 -1
- package/payload/platform/plugins/docs/references/troubleshooting.md +3 -3
- package/payload/server/public/assets/{admin-C6FCOBJJ.js → admin-Czc-XCGo.js} +2 -2
- package/payload/server/public/assets/{data-OhPCCGxF.js → data-CrSbmVOi.js} +1 -1
- package/payload/server/public/assets/{file-vGZzzcEL.js → file-DJN8iDYd.js} +1 -1
- package/payload/server/public/assets/{graph-arM1qUve.js → graph-BtnUfXRD.js} +1 -1
- package/payload/server/public/assets/{house-CStAEh5N.js → house-0e8rAfIV.js} +1 -1
- package/payload/server/public/assets/jsx-runtime-c8K7DPME.css +1 -0
- package/payload/server/public/assets/{public-CA8hdxVS.js → public-CWJExYI2.js} +1 -1
- package/payload/server/public/assets/{share-2-BgiUCVf3.js → share-2-DZ7p8QqC.js} +1 -1
- package/payload/server/public/assets/{useVoiceRecorder-B343k-mr.js → useVoiceRecorder-BgiIIuSz.js} +1 -1
- package/payload/server/public/assets/{x-CNdlr5ao.js → x-CknuyFAk.js} +1 -1
- package/payload/server/public/data.html +6 -6
- package/payload/server/public/graph.html +6 -6
- package/payload/server/public/index.html +7 -7
- package/payload/server/public/public.html +4 -4
- package/payload/server/server.js +250 -156
- package/payload/server/public/assets/jsx-runtime-CE3bWIbP.css +0 -1
- /package/payload/server/public/assets/{jsx-runtime-ZaEwDls0.js → jsx-runtime-DHkQ2la3.js} +0 -0
package/payload/server/server.js
CHANGED
|
@@ -20254,8 +20254,236 @@ function resolveDataPath(raw2) {
|
|
|
20254
20254
|
return { ok: true, absolute: resolvedReal, dataRootReal, relative: relPath };
|
|
20255
20255
|
}
|
|
20256
20256
|
|
|
20257
|
-
//
|
|
20257
|
+
// ../lib/graph-trash/src/index.ts
|
|
20258
|
+
var UNIQUE_KEYS_BY_LABEL = {
|
|
20259
|
+
Person: ["email", "telephone"],
|
|
20260
|
+
Service: ["serviceId"],
|
|
20261
|
+
LocalBusiness: ["accountId"],
|
|
20262
|
+
Task: ["taskId"],
|
|
20263
|
+
Event: ["eventId"],
|
|
20264
|
+
KnowledgeDocument: ["attachmentId"],
|
|
20265
|
+
DigitalDocument: ["attachmentId"],
|
|
20266
|
+
Conversation: ["conversationId", "sessionKey"],
|
|
20267
|
+
Message: ["messageId"],
|
|
20268
|
+
OnboardingState: ["accountId"],
|
|
20269
|
+
Workflow: ["workflowId"],
|
|
20270
|
+
WorkflowStep: ["stepId"],
|
|
20271
|
+
WorkflowRun: ["runId"],
|
|
20272
|
+
Preference: ["preferenceId"],
|
|
20273
|
+
Email: ["emailId", "messageId"],
|
|
20274
|
+
AdminUser: ["userId"],
|
|
20275
|
+
ToolCall: ["callId"],
|
|
20276
|
+
// Composite component nulls — frees the composite constraint:
|
|
20277
|
+
AccessGrant: ["contactValue"],
|
|
20278
|
+
// composite (contactValue, agentSlug, accountId)
|
|
20279
|
+
UserProfile: ["userId"]
|
|
20280
|
+
// composite (accountId, userId)
|
|
20281
|
+
};
|
|
20282
|
+
async function trashNode(params) {
|
|
20283
|
+
const { session, accountId, elementId, by, reason } = params;
|
|
20284
|
+
const lookup = await session.run(
|
|
20285
|
+
`MATCH (n) WHERE elementId(n) = $eid AND n.accountId = $accountId
|
|
20286
|
+
RETURN labels(n) AS labels, properties(n) AS props`,
|
|
20287
|
+
{ eid: elementId, accountId }
|
|
20288
|
+
);
|
|
20289
|
+
if (lookup.records.length === 0) {
|
|
20290
|
+
throw new Error(
|
|
20291
|
+
`trashNode: node not found (elementId=${elementId} accountId=${accountId.slice(0, 8)}\u2026)`
|
|
20292
|
+
);
|
|
20293
|
+
}
|
|
20294
|
+
const allLabels = lookup.records[0].get("labels");
|
|
20295
|
+
const props = lookup.records[0].get("props");
|
|
20296
|
+
const baseLabels = allLabels.filter((l) => l !== "Trashed");
|
|
20297
|
+
if (allLabels.includes("Trashed")) {
|
|
20298
|
+
return {
|
|
20299
|
+
trashed: false,
|
|
20300
|
+
alreadyTrashed: true,
|
|
20301
|
+
nodeId: elementId,
|
|
20302
|
+
labels: baseLabels,
|
|
20303
|
+
trashedAt: String(props.trashedAt ?? ""),
|
|
20304
|
+
originalKeys: {}
|
|
20305
|
+
};
|
|
20306
|
+
}
|
|
20307
|
+
const uniqueKeys = /* @__PURE__ */ new Set();
|
|
20308
|
+
for (const label of baseLabels) {
|
|
20309
|
+
for (const key of UNIQUE_KEYS_BY_LABEL[label] ?? []) uniqueKeys.add(key);
|
|
20310
|
+
}
|
|
20311
|
+
const originalKeys = {};
|
|
20312
|
+
for (const k of uniqueKeys) {
|
|
20313
|
+
if (props[k] !== void 0 && props[k] !== null) originalKeys[k] = props[k];
|
|
20314
|
+
}
|
|
20315
|
+
const setNullClauses = Object.keys(originalKeys).map((k) => `n.\`${k}\` = null`).join(", ");
|
|
20316
|
+
const setNullSuffix = setNullClauses ? `, ${setNullClauses}` : "";
|
|
20317
|
+
const trashedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
20318
|
+
await session.run(
|
|
20319
|
+
`MATCH (n) WHERE elementId(n) = $eid
|
|
20320
|
+
SET n:Trashed,
|
|
20321
|
+
n.trashedAt = datetime($trashedAt),
|
|
20322
|
+
n.trashedBy = $by,
|
|
20323
|
+
n.trashReason = $reason,
|
|
20324
|
+
n._trashedKeys = $trashedKeysJson${setNullSuffix}`,
|
|
20325
|
+
{
|
|
20326
|
+
eid: elementId,
|
|
20327
|
+
trashedAt,
|
|
20328
|
+
by,
|
|
20329
|
+
reason: reason ?? null,
|
|
20330
|
+
trashedKeysJson: JSON.stringify(originalKeys)
|
|
20331
|
+
}
|
|
20332
|
+
);
|
|
20333
|
+
process.stderr.write(
|
|
20334
|
+
`[trash:marked] accountId=${accountId} elementId=${elementId} labels=${baseLabels.join(",")} by=${by} reason=${reason ?? "null"}
|
|
20335
|
+
`
|
|
20336
|
+
);
|
|
20337
|
+
return {
|
|
20338
|
+
trashed: true,
|
|
20339
|
+
alreadyTrashed: false,
|
|
20340
|
+
nodeId: elementId,
|
|
20341
|
+
labels: baseLabels,
|
|
20342
|
+
trashedAt,
|
|
20343
|
+
originalKeys
|
|
20344
|
+
};
|
|
20345
|
+
}
|
|
20346
|
+
async function restoreNode(params) {
|
|
20347
|
+
const { session, accountId, elementId } = params;
|
|
20348
|
+
const lookup = await session.run(
|
|
20349
|
+
`MATCH (n:Trashed) WHERE elementId(n) = $eid
|
|
20350
|
+
RETURN labels(n) AS labels, n._trashedKeys AS keysJson`,
|
|
20351
|
+
{ eid: elementId }
|
|
20352
|
+
);
|
|
20353
|
+
if (lookup.records.length === 0) {
|
|
20354
|
+
throw new Error(
|
|
20355
|
+
`restoreNode: trashed node not found (elementId=${elementId})`
|
|
20356
|
+
);
|
|
20357
|
+
}
|
|
20358
|
+
const allLabels = lookup.records[0].get("labels");
|
|
20359
|
+
const baseLabels = allLabels.filter((l) => l !== "Trashed");
|
|
20360
|
+
const keysJson = lookup.records[0].get("keysJson");
|
|
20361
|
+
const originalKeys = keysJson ? JSON.parse(keysJson) : {};
|
|
20362
|
+
for (const label of baseLabels) {
|
|
20363
|
+
const uniqueKeys = UNIQUE_KEYS_BY_LABEL[label] ?? [];
|
|
20364
|
+
for (const k of uniqueKeys) {
|
|
20365
|
+
const v = originalKeys[k];
|
|
20366
|
+
if (v === void 0 || v === null) continue;
|
|
20367
|
+
const conflict = await session.run(
|
|
20368
|
+
`MATCH (other:\`${label}\`)
|
|
20369
|
+
WHERE elementId(other) <> $eid
|
|
20370
|
+
AND NOT other:Trashed
|
|
20371
|
+
AND other.\`${k}\` = $val
|
|
20372
|
+
RETURN elementId(other) AS otherId LIMIT 1`,
|
|
20373
|
+
{ eid: elementId, val: v }
|
|
20374
|
+
);
|
|
20375
|
+
if (conflict.records.length > 0) {
|
|
20376
|
+
const otherId = conflict.records[0].get("otherId");
|
|
20377
|
+
throw new Error(
|
|
20378
|
+
`restoreNode: cannot restore ${label} elementId=${elementId} \u2014 active node elementId=${otherId} already holds ${k}=${JSON.stringify(v)}`
|
|
20379
|
+
);
|
|
20380
|
+
}
|
|
20381
|
+
}
|
|
20382
|
+
}
|
|
20383
|
+
const setClauses = Object.keys(originalKeys).map((k) => `n.\`${k}\` = $val_${k}`).join(", ");
|
|
20384
|
+
const setSuffix = setClauses ? `, ${setClauses}` : "";
|
|
20385
|
+
const setParams = { eid: elementId };
|
|
20386
|
+
for (const [k, v] of Object.entries(originalKeys)) setParams[`val_${k}`] = v;
|
|
20387
|
+
await session.run(
|
|
20388
|
+
`MATCH (n:Trashed) WHERE elementId(n) = $eid
|
|
20389
|
+
REMOVE n:Trashed, n.trashedAt, n.trashedBy, n.trashReason, n._trashedKeys
|
|
20390
|
+
SET n.restoredAt = datetime()${setSuffix}`,
|
|
20391
|
+
setParams
|
|
20392
|
+
);
|
|
20393
|
+
process.stderr.write(
|
|
20394
|
+
`[trash:restored] accountId=${accountId} elementId=${elementId} labels=${baseLabels.join(",")}
|
|
20395
|
+
`
|
|
20396
|
+
);
|
|
20397
|
+
return {
|
|
20398
|
+
restored: true,
|
|
20399
|
+
nodeId: elementId,
|
|
20400
|
+
labels: baseLabels,
|
|
20401
|
+
restoredKeys: originalKeys
|
|
20402
|
+
};
|
|
20403
|
+
}
|
|
20404
|
+
|
|
20405
|
+
// app/lib/file-delete-cascade.ts
|
|
20258
20406
|
var UUID_RE2 = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
20407
|
+
function parseAttachmentPath(relPath) {
|
|
20408
|
+
const segments = relPath.split("/").filter(Boolean);
|
|
20409
|
+
if (segments.length !== 4) return null;
|
|
20410
|
+
if (segments[0] !== "uploads") return null;
|
|
20411
|
+
const accountId = segments[1];
|
|
20412
|
+
const attachmentId = segments[2];
|
|
20413
|
+
const filename = segments[3];
|
|
20414
|
+
if (!UUID_RE2.test(accountId) || !UUID_RE2.test(attachmentId)) return null;
|
|
20415
|
+
const dot = filename.lastIndexOf(".");
|
|
20416
|
+
if (dot === -1) return null;
|
|
20417
|
+
const stem = filename.slice(0, dot);
|
|
20418
|
+
const ext = filename.slice(dot + 1);
|
|
20419
|
+
if (stem !== attachmentId) return null;
|
|
20420
|
+
if (ext === "meta" || filename.endsWith(".meta.json")) return null;
|
|
20421
|
+
return { accountId, attachmentId };
|
|
20422
|
+
}
|
|
20423
|
+
async function cascadeDeleteDocument(params) {
|
|
20424
|
+
const { accountId, attachmentId } = params;
|
|
20425
|
+
const session = getSession();
|
|
20426
|
+
try {
|
|
20427
|
+
const lookup = await session.run(
|
|
20428
|
+
`MATCH (d:KnowledgeDocument { accountId: $accountId, attachmentId: $attachmentId })
|
|
20429
|
+
WHERE NOT d:Trashed
|
|
20430
|
+
RETURN elementId(d) AS eid LIMIT 1`,
|
|
20431
|
+
{ accountId, attachmentId }
|
|
20432
|
+
);
|
|
20433
|
+
if (lookup.records.length === 0) return { nodes: 0 };
|
|
20434
|
+
const docElementId = lookup.records[0].get("eid");
|
|
20435
|
+
const childResult = await session.run(
|
|
20436
|
+
`MATCH (d) WHERE elementId(d) = $eid
|
|
20437
|
+
OPTIONAL MATCH (d)-[:HAS_SECTION]->(s:Section)
|
|
20438
|
+
OPTIONAL MATCH (s)-[:HAS_CHUNK]->(c:Chunk)
|
|
20439
|
+
WITH collect(DISTINCT s) AS sections, collect(DISTINCT c) AS chunks
|
|
20440
|
+
RETURN [s IN sections WHERE s IS NOT NULL | elementId(s)] AS sectionIds,
|
|
20441
|
+
[c IN chunks WHERE c IS NOT NULL | elementId(c)] AS chunkIds`,
|
|
20442
|
+
{ eid: docElementId }
|
|
20443
|
+
);
|
|
20444
|
+
if (childResult.records.length === 0) {
|
|
20445
|
+
throw new Error(
|
|
20446
|
+
`cascadeDeleteDocument: child lookup returned zero rows for elementId=${docElementId} \u2014 concurrent trash or connection drop`
|
|
20447
|
+
);
|
|
20448
|
+
}
|
|
20449
|
+
const sectionIds = childResult.records[0].get("sectionIds") ?? [];
|
|
20450
|
+
const chunkIds = childResult.records[0].get("chunkIds") ?? [];
|
|
20451
|
+
await trashNode({
|
|
20452
|
+
session,
|
|
20453
|
+
accountId,
|
|
20454
|
+
elementId: docElementId,
|
|
20455
|
+
by: "file-delete-cascade",
|
|
20456
|
+
reason: "file deleted via /data"
|
|
20457
|
+
});
|
|
20458
|
+
for (const sid of sectionIds) {
|
|
20459
|
+
await trashNode({
|
|
20460
|
+
session,
|
|
20461
|
+
accountId,
|
|
20462
|
+
elementId: sid,
|
|
20463
|
+
by: `file-delete-cascade:from-${docElementId}`,
|
|
20464
|
+
reason: `cascade from KnowledgeDocument ${docElementId}`
|
|
20465
|
+
});
|
|
20466
|
+
}
|
|
20467
|
+
for (const cid of chunkIds) {
|
|
20468
|
+
await trashNode({
|
|
20469
|
+
session,
|
|
20470
|
+
accountId,
|
|
20471
|
+
elementId: cid,
|
|
20472
|
+
by: `file-delete-cascade:from-${docElementId}`,
|
|
20473
|
+
reason: `cascade from KnowledgeDocument ${docElementId}`
|
|
20474
|
+
});
|
|
20475
|
+
}
|
|
20476
|
+
return { nodes: 1 };
|
|
20477
|
+
} finally {
|
|
20478
|
+
try {
|
|
20479
|
+
await session.close();
|
|
20480
|
+
} catch {
|
|
20481
|
+
}
|
|
20482
|
+
}
|
|
20483
|
+
}
|
|
20484
|
+
|
|
20485
|
+
// server/routes/admin/files.ts
|
|
20486
|
+
var UUID_RE3 = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
20259
20487
|
async function readMeta(absDir, baseName) {
|
|
20260
20488
|
try {
|
|
20261
20489
|
const raw2 = await readFile4(join12(absDir, `${baseName}.meta.json`), "utf8");
|
|
@@ -20277,7 +20505,7 @@ async function readAccountNames() {
|
|
|
20277
20505
|
return map;
|
|
20278
20506
|
}
|
|
20279
20507
|
for (const name of names) {
|
|
20280
|
-
if (!
|
|
20508
|
+
if (!UUID_RE3.test(name)) continue;
|
|
20281
20509
|
const configPath2 = resolve29(accountsDir, name, "account.json");
|
|
20282
20510
|
try {
|
|
20283
20511
|
const raw2 = await readFile4(configPath2, "utf8");
|
|
@@ -20295,7 +20523,7 @@ async function readAccountNames() {
|
|
|
20295
20523
|
return map;
|
|
20296
20524
|
}
|
|
20297
20525
|
async function enrich(absolute, entry, accountNames) {
|
|
20298
|
-
if (entry.kind === "directory" &&
|
|
20526
|
+
if (entry.kind === "directory" && UUID_RE3.test(entry.name)) {
|
|
20299
20527
|
const meta = await readMeta(join12(absolute, entry.name), entry.name);
|
|
20300
20528
|
if (meta?.filename) {
|
|
20301
20529
|
entry.displayName = meta.filename;
|
|
@@ -20311,7 +20539,7 @@ async function enrich(absolute, entry, accountNames) {
|
|
|
20311
20539
|
if (entry.kind === "file") {
|
|
20312
20540
|
const dot = entry.name.lastIndexOf(".");
|
|
20313
20541
|
const base = dot === -1 ? entry.name : entry.name.slice(0, dot);
|
|
20314
|
-
if (
|
|
20542
|
+
if (UUID_RE3.test(base)) {
|
|
20315
20543
|
const meta = await readMeta(absolute, base);
|
|
20316
20544
|
if (meta?.filename) {
|
|
20317
20545
|
entry.displayName = meta.filename;
|
|
@@ -20323,7 +20551,7 @@ async function enrich(absolute, entry, accountNames) {
|
|
|
20323
20551
|
function buildDisplayPath(relPath, accountNames) {
|
|
20324
20552
|
if (relPath === "." || relPath === "") return [];
|
|
20325
20553
|
return relPath.split("/").filter(Boolean).map((seg) => {
|
|
20326
|
-
const dn =
|
|
20554
|
+
const dn = UUID_RE3.test(seg) ? accountNames.get(seg) : void 0;
|
|
20327
20555
|
return dn ? { name: seg, displayName: dn } : { name: seg };
|
|
20328
20556
|
});
|
|
20329
20557
|
}
|
|
@@ -20351,7 +20579,7 @@ app24.get("/", requireAdminSession, async (c) => {
|
|
|
20351
20579
|
const names = await readdir2(absolute);
|
|
20352
20580
|
const entries = [];
|
|
20353
20581
|
for (const name of names) {
|
|
20354
|
-
if (
|
|
20582
|
+
if (UUID_RE3.test(name.replace(/\.meta\.json$/, "")) && name.endsWith(".meta.json")) {
|
|
20355
20583
|
continue;
|
|
20356
20584
|
}
|
|
20357
20585
|
try {
|
|
@@ -20497,7 +20725,8 @@ app24.post("/upload", requireAdminSession, async (c) => {
|
|
|
20497
20725
|
});
|
|
20498
20726
|
app24.delete("/", requireAdminSession, async (c) => {
|
|
20499
20727
|
const sessionKey = c.var.sessionKey;
|
|
20500
|
-
|
|
20728
|
+
const accountId = getAccountIdForSession(sessionKey);
|
|
20729
|
+
if (!accountId) {
|
|
20501
20730
|
console.error(`[data] auth-rejected endpoint="DELETE /api/admin/files" reason="no account for session"`);
|
|
20502
20731
|
return c.json({ error: "Account not found for session" }, 401);
|
|
20503
20732
|
}
|
|
@@ -20527,7 +20756,7 @@ app24.delete("/", requireAdminSession, async (c) => {
|
|
|
20527
20756
|
}
|
|
20528
20757
|
const dot = base.lastIndexOf(".");
|
|
20529
20758
|
const stem = dot === -1 ? base : base.slice(0, dot);
|
|
20530
|
-
const sidecarPath =
|
|
20759
|
+
const sidecarPath = UUID_RE3.test(stem) && base !== `${stem}.meta.json` ? join12(dirname10(absolute), `${stem}.meta.json`) : null;
|
|
20531
20760
|
await unlink2(absolute);
|
|
20532
20761
|
if (sidecarPath) {
|
|
20533
20762
|
try {
|
|
@@ -20536,6 +20765,19 @@ app24.delete("/", requireAdminSession, async (c) => {
|
|
|
20536
20765
|
}
|
|
20537
20766
|
}
|
|
20538
20767
|
console.error(`[data] file-delete path="${relPath}" bytes=${info.size}`);
|
|
20768
|
+
const parsed = parseAttachmentPath(relPath);
|
|
20769
|
+
if (parsed) {
|
|
20770
|
+
try {
|
|
20771
|
+
const { nodes } = await cascadeDeleteDocument({
|
|
20772
|
+
accountId,
|
|
20773
|
+
attachmentId: parsed.attachmentId
|
|
20774
|
+
});
|
|
20775
|
+
console.error(`[data] file-delete graph-cascade path="${relPath}" nodes=${nodes}`);
|
|
20776
|
+
} catch (err) {
|
|
20777
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
20778
|
+
console.error(`[data] file-delete graph-cascade-failed path="${relPath}" err="${message}"`);
|
|
20779
|
+
}
|
|
20780
|
+
}
|
|
20539
20781
|
return c.json({ ok: true });
|
|
20540
20782
|
} catch (err) {
|
|
20541
20783
|
const code = err.code;
|
|
@@ -20884,154 +21126,6 @@ function pruneNode(node) {
|
|
|
20884
21126
|
}
|
|
20885
21127
|
var graph_subgraph_default = app26;
|
|
20886
21128
|
|
|
20887
|
-
// ../lib/graph-trash/src/index.ts
|
|
20888
|
-
var UNIQUE_KEYS_BY_LABEL = {
|
|
20889
|
-
Person: ["email", "telephone"],
|
|
20890
|
-
Service: ["serviceId"],
|
|
20891
|
-
LocalBusiness: ["accountId"],
|
|
20892
|
-
Task: ["taskId"],
|
|
20893
|
-
Event: ["eventId"],
|
|
20894
|
-
KnowledgeDocument: ["attachmentId"],
|
|
20895
|
-
DigitalDocument: ["attachmentId"],
|
|
20896
|
-
Conversation: ["conversationId", "sessionKey"],
|
|
20897
|
-
Message: ["messageId"],
|
|
20898
|
-
OnboardingState: ["accountId"],
|
|
20899
|
-
Workflow: ["workflowId"],
|
|
20900
|
-
WorkflowStep: ["stepId"],
|
|
20901
|
-
WorkflowRun: ["runId"],
|
|
20902
|
-
Preference: ["preferenceId"],
|
|
20903
|
-
Email: ["emailId", "messageId"],
|
|
20904
|
-
AdminUser: ["userId"],
|
|
20905
|
-
ToolCall: ["callId"],
|
|
20906
|
-
// Composite component nulls — frees the composite constraint:
|
|
20907
|
-
AccessGrant: ["contactValue"],
|
|
20908
|
-
// composite (contactValue, agentSlug, accountId)
|
|
20909
|
-
UserProfile: ["userId"]
|
|
20910
|
-
// composite (accountId, userId)
|
|
20911
|
-
};
|
|
20912
|
-
async function trashNode(params) {
|
|
20913
|
-
const { session, accountId, elementId, by, reason } = params;
|
|
20914
|
-
const lookup = await session.run(
|
|
20915
|
-
`MATCH (n) WHERE elementId(n) = $eid AND n.accountId = $accountId
|
|
20916
|
-
RETURN labels(n) AS labels, properties(n) AS props`,
|
|
20917
|
-
{ eid: elementId, accountId }
|
|
20918
|
-
);
|
|
20919
|
-
if (lookup.records.length === 0) {
|
|
20920
|
-
throw new Error(
|
|
20921
|
-
`trashNode: node not found (elementId=${elementId} accountId=${accountId.slice(0, 8)}\u2026)`
|
|
20922
|
-
);
|
|
20923
|
-
}
|
|
20924
|
-
const allLabels = lookup.records[0].get("labels");
|
|
20925
|
-
const props = lookup.records[0].get("props");
|
|
20926
|
-
const baseLabels = allLabels.filter((l) => l !== "Trashed");
|
|
20927
|
-
if (allLabels.includes("Trashed")) {
|
|
20928
|
-
return {
|
|
20929
|
-
trashed: false,
|
|
20930
|
-
alreadyTrashed: true,
|
|
20931
|
-
nodeId: elementId,
|
|
20932
|
-
labels: baseLabels,
|
|
20933
|
-
trashedAt: String(props.trashedAt ?? ""),
|
|
20934
|
-
originalKeys: {}
|
|
20935
|
-
};
|
|
20936
|
-
}
|
|
20937
|
-
const uniqueKeys = /* @__PURE__ */ new Set();
|
|
20938
|
-
for (const label of baseLabels) {
|
|
20939
|
-
for (const key of UNIQUE_KEYS_BY_LABEL[label] ?? []) uniqueKeys.add(key);
|
|
20940
|
-
}
|
|
20941
|
-
const originalKeys = {};
|
|
20942
|
-
for (const k of uniqueKeys) {
|
|
20943
|
-
if (props[k] !== void 0 && props[k] !== null) originalKeys[k] = props[k];
|
|
20944
|
-
}
|
|
20945
|
-
const setNullClauses = Object.keys(originalKeys).map((k) => `n.\`${k}\` = null`).join(", ");
|
|
20946
|
-
const setNullSuffix = setNullClauses ? `, ${setNullClauses}` : "";
|
|
20947
|
-
const trashedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
20948
|
-
await session.run(
|
|
20949
|
-
`MATCH (n) WHERE elementId(n) = $eid
|
|
20950
|
-
SET n:Trashed,
|
|
20951
|
-
n.trashedAt = datetime($trashedAt),
|
|
20952
|
-
n.trashedBy = $by,
|
|
20953
|
-
n.trashReason = $reason,
|
|
20954
|
-
n._trashedKeys = $trashedKeysJson${setNullSuffix}`,
|
|
20955
|
-
{
|
|
20956
|
-
eid: elementId,
|
|
20957
|
-
trashedAt,
|
|
20958
|
-
by,
|
|
20959
|
-
reason: reason ?? null,
|
|
20960
|
-
trashedKeysJson: JSON.stringify(originalKeys)
|
|
20961
|
-
}
|
|
20962
|
-
);
|
|
20963
|
-
process.stderr.write(
|
|
20964
|
-
`[trash:marked] accountId=${accountId} elementId=${elementId} labels=${baseLabels.join(",")} by=${by} reason=${reason ?? "null"}
|
|
20965
|
-
`
|
|
20966
|
-
);
|
|
20967
|
-
return {
|
|
20968
|
-
trashed: true,
|
|
20969
|
-
alreadyTrashed: false,
|
|
20970
|
-
nodeId: elementId,
|
|
20971
|
-
labels: baseLabels,
|
|
20972
|
-
trashedAt,
|
|
20973
|
-
originalKeys
|
|
20974
|
-
};
|
|
20975
|
-
}
|
|
20976
|
-
async function restoreNode(params) {
|
|
20977
|
-
const { session, accountId, elementId } = params;
|
|
20978
|
-
const lookup = await session.run(
|
|
20979
|
-
`MATCH (n:Trashed) WHERE elementId(n) = $eid
|
|
20980
|
-
RETURN labels(n) AS labels, n._trashedKeys AS keysJson`,
|
|
20981
|
-
{ eid: elementId }
|
|
20982
|
-
);
|
|
20983
|
-
if (lookup.records.length === 0) {
|
|
20984
|
-
throw new Error(
|
|
20985
|
-
`restoreNode: trashed node not found (elementId=${elementId})`
|
|
20986
|
-
);
|
|
20987
|
-
}
|
|
20988
|
-
const allLabels = lookup.records[0].get("labels");
|
|
20989
|
-
const baseLabels = allLabels.filter((l) => l !== "Trashed");
|
|
20990
|
-
const keysJson = lookup.records[0].get("keysJson");
|
|
20991
|
-
const originalKeys = keysJson ? JSON.parse(keysJson) : {};
|
|
20992
|
-
for (const label of baseLabels) {
|
|
20993
|
-
const uniqueKeys = UNIQUE_KEYS_BY_LABEL[label] ?? [];
|
|
20994
|
-
for (const k of uniqueKeys) {
|
|
20995
|
-
const v = originalKeys[k];
|
|
20996
|
-
if (v === void 0 || v === null) continue;
|
|
20997
|
-
const conflict = await session.run(
|
|
20998
|
-
`MATCH (other:\`${label}\`)
|
|
20999
|
-
WHERE elementId(other) <> $eid
|
|
21000
|
-
AND NOT other:Trashed
|
|
21001
|
-
AND other.\`${k}\` = $val
|
|
21002
|
-
RETURN elementId(other) AS otherId LIMIT 1`,
|
|
21003
|
-
{ eid: elementId, val: v }
|
|
21004
|
-
);
|
|
21005
|
-
if (conflict.records.length > 0) {
|
|
21006
|
-
const otherId = conflict.records[0].get("otherId");
|
|
21007
|
-
throw new Error(
|
|
21008
|
-
`restoreNode: cannot restore ${label} elementId=${elementId} \u2014 active node elementId=${otherId} already holds ${k}=${JSON.stringify(v)}`
|
|
21009
|
-
);
|
|
21010
|
-
}
|
|
21011
|
-
}
|
|
21012
|
-
}
|
|
21013
|
-
const setClauses = Object.keys(originalKeys).map((k) => `n.\`${k}\` = $val_${k}`).join(", ");
|
|
21014
|
-
const setSuffix = setClauses ? `, ${setClauses}` : "";
|
|
21015
|
-
const setParams = { eid: elementId };
|
|
21016
|
-
for (const [k, v] of Object.entries(originalKeys)) setParams[`val_${k}`] = v;
|
|
21017
|
-
await session.run(
|
|
21018
|
-
`MATCH (n:Trashed) WHERE elementId(n) = $eid
|
|
21019
|
-
REMOVE n:Trashed, n.trashedAt, n.trashedBy, n.trashReason, n._trashedKeys
|
|
21020
|
-
SET n.restoredAt = datetime()${setSuffix}`,
|
|
21021
|
-
setParams
|
|
21022
|
-
);
|
|
21023
|
-
process.stderr.write(
|
|
21024
|
-
`[trash:restored] accountId=${accountId} elementId=${elementId} labels=${baseLabels.join(",")}
|
|
21025
|
-
`
|
|
21026
|
-
);
|
|
21027
|
-
return {
|
|
21028
|
-
restored: true,
|
|
21029
|
-
nodeId: elementId,
|
|
21030
|
-
labels: baseLabels,
|
|
21031
|
-
restoredKeys: originalKeys
|
|
21032
|
-
};
|
|
21033
|
-
}
|
|
21034
|
-
|
|
21035
21129
|
// server/routes/admin/graph-delete.ts
|
|
21036
21130
|
var app27 = new Hono2();
|
|
21037
21131
|
app27.post("/", requireAdminSession, async (c) => {
|