Skip to content

Commit ed5cc28

Browse files
committed
* MS new claim-gen/junto importer works without error, on the four contemporary json-export examples.
* MS if the "addNodeLink" gql command fails, it returns the text of the parent and child nodes, to help the caller know where the problem was. (shouldn't be a security concern, since the database transaction has row-level-security enabled) * MS if an error occurs during the parsing/interpretation of a JSON blob put into the subtree-importer dialog, the error is caught and displayed in the UI. (rather than causing a wider UI error, that eg. can break the nav-bar)
1 parent a511b8f commit ed5cc28

7 files changed

Lines changed: 104 additions & 49 deletions

File tree

.vscode/settings.json

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,31 @@
2020

2121
"[rust]": {
2222
"editor.defaultFormatter": "rust-lang.rust-analyzer",
23-
"editor.formatOnSave": true
23+
24+
// 2025-06-06: this adds too much (often >1s) delay before save completes
25+
//"editor.formatOnSave": true,
26+
27+
// 2025-06-06: tried this, but seems to just never do formatting
28+
/*"editor.formatOnSave": true,
29+
"editor.formatOnSaveMode": "modificationsIfAvailable",*/
30+
31+
// 2025-06-06: decided to add a custom hotkey instead (user-level keybindings.json), which explicitly does a save-all operation THEN a format operation, IF the file is a Rust file
32+
/*{
33+
"key": "ctrl+s",
34+
"command": "runCommands",
35+
"when": "editorLangId == rust",
36+
"args": {
37+
"commands": [
38+
"workbench.action.files.saveAll",
39+
"editor.action.formatDocument",
40+
]
41+
}
42+
},
43+
{
44+
"key": "ctrl+s",
45+
"command": "workbench.action.files.saveAll",
46+
"when": "editorLangId != rust"
47+
},*/
2448
},
2549
"rust-analyzer.server.extraEnv": {
2650
// this allows us to use unstable features (as is true for the Docker builds), without rust-analyzer showing an error on the "#![feature(XXX)]" line
@@ -38,7 +62,7 @@
3862
},
3963
//"rust-analyzer.checkOnSave.enable": false,
4064
// have RA's cargo-check use a separate cache-directory (otherwise, eg. running cargo-check in terminal with different flags will mess up RA's cargo-check cache, requiring a full re-check for RA)
41-
"rust-analyzer.checkOnSave.extraArgs": [
65+
"rust-analyzer.check.extraArgs": [
4266
"--target-dir", "./Temp/rust-analyzer-check"
4367
],
4468
// maybe use these in the future, in place of the "-Awarnings" flag in config.toml and Dockerfile

Packages/app-server/src/db/commands/_temp/clone_map_special.rs

Lines changed: 2 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ use crate::db::commands::add_map::{add_map, AddMapInput};
2121
use crate::db::general::sign_in_::jwt_utils::{get_user_info_from_gql_ctx, resolve_jwt_to_user_info};
2222
use crate::db::maps::{get_map, MapInput};
2323
use crate::db::node_links::{get_node_links, ChildGroup, NodeLinkInput};
24-
use crate::db::node_phrasings::NodePhrasing_Embedded;
24+
use crate::db::node_phrasings::{get_first_non_empty_text_in_phrasing, get_first_non_empty_text_in_phrasing_embedded, NodePhrasing_Embedded};
2525
use crate::db::node_revisions::{get_node_revision, NodeRevision, NodeRevisionInput};
2626
use crate::db::nodes::{get_node, get_node_children};
2727
use crate::db::nodes_::_node::{Node, NodeInput};
@@ -180,20 +180,7 @@ pub fn clone_node_tree_special<'a>(ctx: &'a AccessorContext<'_>, actor: &'a User
180180
let grandchild_links = get_node_links(ctx, Some(child.id.as_str()), None).await?;
181181
let has_attachments = child_rev.attachments.len() > 0;
182182
// ensure that argument has no title, in any of the text_XXX fields
183-
let get_if_non_empty = |s: &Option<String>| {
184-
match s {
185-
Some(s) => match s {
186-
s if s.is_empty() => None,
187-
_ => Some(s.to_owned()),
188-
},
189-
None => None,
190-
}
191-
};
192-
let first_non_empty_title =
193-
get_if_non_empty(&Some(child_rev.phrasing.text_base.clone()))
194-
.or(get_if_non_empty(&child_rev.phrasing.text_negation.clone()))
195-
.or(get_if_non_empty(&child_rev.phrasing.text_question.clone()))
196-
.or(get_if_non_empty(&child_rev.phrasing.text_narrative.clone()));
183+
let first_non_empty_title = get_first_non_empty_text_in_phrasing_embedded(&child_rev.phrasing);
197184
/*if let Some(title) = first_non_empty_title {
198185
warn!("Argument node #{} has a non-empty title. If this is a dry-run, it's recommended to investigate these entries before proceeding. @title:\n\t{}", child.id.as_str(), title);
199186
}*/

Packages/app-server/src/db/commands/add_node_link.rs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ use rust_shared::db_constants::{GLOBAL_ROOT_NODE_ID, SYSTEM_USER_ID};
55
use rust_shared::rust_macros::wrap_slow_macros;
66
use rust_shared::serde::{Deserialize, Serialize};
77
use rust_shared::serde_json::{json, Value};
8+
use rust_shared::utils::general_::extensions::ToOwnedV;
89
use rust_shared::utils::time::time_since_epoch_ms_i64;
910
use rust_shared::utils::type_aliases::JSONValue;
1011
use rust_shared::{anyhow, async_graphql, serde_json, GQLError};
@@ -20,6 +21,8 @@ use crate::db::general::permission_helpers::assert_user_can_add_child;
2021
use crate::db::general::sign_in_::jwt_utils::{get_user_info_from_gql_ctx, resolve_jwt_to_user_info};
2122
use crate::db::node_links::{get_node_links, ChildGroup, NodeLink, NodeLinkInput, Polarity};
2223
use crate::db::node_links_::node_link_validity::assert_new_link_is_valid;
24+
use crate::db::node_phrasings::get_first_non_empty_text_in_phrasing_embedded;
25+
use crate::db::node_revisions::get_node_revision;
2326
use crate::db::nodes::get_node;
2427
use crate::db::nodes_::_node::Node;
2528
use crate::db::nodes_::_node_type::{get_node_type_info, NodeType};
@@ -86,7 +89,12 @@ pub async fn add_node_link(ctx: &AccessorContext<'_>, actor: &User, _is_root: bo
8689
/*let parent_to_child_links = get_node_links(ctx, Some(&parent_id), Some(&child_id)).await?;
8790
ensure!(parent_to_child_links.len() == 0, "Node #{child_id} is already a child of node #{parent_id}.");*/
8891

89-
assert_new_link_is_valid(ctx, &parent_id, &link.child, link.c_childType, link.group, link.polarity, Some(actor)).await?;
92+
if let Err(err) = assert_new_link_is_valid(ctx, &parent_id, &link.child, link.c_childType, link.group, link.polarity, Some(actor)).await {
93+
let parent_text = get_first_non_empty_text_in_phrasing_embedded(&get_node_revision(ctx, &parent.c_currentRevision).await?.phrasing);
94+
let child_text = get_first_non_empty_text_in_phrasing_embedded(&get_node_revision(ctx, &child.c_currentRevision).await?.phrasing);
95+
let link_json = serde_json::to_string(&link).unwrap_or_else(|_| "<failed to serialize link>".o());
96+
return Err(err.context(format!("New link is invalid. @parent_text({parent_text:?}) @childText({child_text:?}) @link({link_json})")).into());
97+
}
9098
}
9199

92100
upsert_db_entry_by_id_for_struct(&ctx, "nodeLinks".to_owned(), link.id.to_string(), link.clone()).await?;

Packages/app-server/src/db/node_phrasings.rs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,32 @@ pub async fn get_node_phrasings(ctx: &AccessorContext<'_>, node_id: &str) -> Res
3737
}))).await
3838
}
3939

40+
fn get_if_non_empty(s: Option<&str>) -> Option<String> {
41+
match s {
42+
Some(s) => match s {
43+
s if s.is_empty() => None,
44+
_ => Some(s.to_owned()),
45+
},
46+
None => None, //hi23233223232234234234234234234234234
47+
}
48+
}
49+
#[rustfmt::skip]
50+
pub fn get_first_non_empty_text_in_phrasing(phrasing: &NodePhrasing) -> Option<String> {
51+
let first_non_empty_title = get_if_non_empty(Some(&phrasing.text_base))
52+
.or(get_if_non_empty(phrasing.text_negation.as_deref()))
53+
.or(get_if_non_empty(phrasing.text_question.as_deref()))
54+
.or(get_if_non_empty(phrasing.text_narrative.as_deref()));
55+
first_non_empty_title
56+
}
57+
#[rustfmt::skip]
58+
pub fn get_first_non_empty_text_in_phrasing_embedded(phrasing: &NodePhrasing_Embedded) -> Option<String> {
59+
let first_non_empty_title = get_if_non_empty(Some(&phrasing.text_base))
60+
.or(get_if_non_empty(phrasing.text_negation.as_deref()))
61+
.or(get_if_non_empty(phrasing.text_question.as_deref()))
62+
.or(get_if_non_empty(phrasing.text_narrative.as_deref()));
63+
first_non_empty_title
64+
}
65+
4066
wrap_slow_macros! {
4167

4268
#[derive(Enum, Copy, Clone, Eq, PartialEq, Serialize, Deserialize)]

Packages/client/Source/UI/@Shared/Maps/Node/NodeUI_Menu/MI_ImportSubtree.tsx

Lines changed: 25 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import {apolloClient} from "Utils/LibIntegrations/Apollo.js";
1010
import {liveSkin} from "Utils/Styles/SkinManager.js";
1111
import {AddNotificationMessage, ES, InfoButton, O, Observer, P, RunInAction_Set, UseWindowEventListener} from "web-vcore";
1212
import {gql} from "@apollo/client";
13-
import {E, FromJSON, GetEntries, ModifyString, SleepAsync, Timer} from "js-vextensions";
13+
import {E, FromJSON, GetEntries, GetStackTraceStr, ModifyString, SleepAsync, Timer} from "js-vextensions";
1414
import {makeObservable} from "mobx";
1515
import {ignore} from "mobx-sync";
1616
import {Button, CheckBox, Column, Row, Select, Spinner, Text, TextArea} from "react-vcomponents";
@@ -128,7 +128,7 @@ class ImportSubtreeUI extends BaseComponent<
128128
selectFromIndex: number,
129129
searchQueryGen: number,
130130
},
131-
{resources: ImportResource[]}
131+
{resources: ImportResource[], resources_parseError: Error|n}
132132
> {
133133
static initialState: Partial<ExtractState<ImportSubtreeUI>> = {
134134
leftTab: ImportSubtreeUI_LeftTab.source,
@@ -156,25 +156,31 @@ class ImportSubtreeUI extends BaseComponent<
156156
const importContext = useMemo(()=>({mapID: map.id, nodeAccessPolicyID}), [map.id, nodeAccessPolicyID]);
157157

158158
let resources: ImportResource[] = [];
159+
let resources_parseError: Error|n;
159160
if (process) {
160-
if (uiState.sourceType == DataExchangeFormat.json_dm && forJSONDM_subtreeData != null) {
161-
resources = GetResourcesInImportSubtree_JsonDm(forJSONDM_subtreeData, node);
162-
} else if (uiState.sourceType == DataExchangeFormat.json_dm_fs && forJSONDMFS_subtreeData != null) {
163-
resources = GetResourcesInImportSubtree_JsonDmFs(forJSONDMFS_subtreeData);
164-
} else if (uiState.sourceType == DataExchangeFormat.json_cg && forJSONCG_subtreeData != null) {
165-
resources = GetResourcesInImportSubtree_CG(importContext, forJSONCG_subtreeData);
166-
} else if (uiState.sourceType == DataExchangeFormat.csv_sl && forCSVSL_subtreeData != null) {
167-
resources = GetResourcesInImportSubtree_CSV_SL(forCSVSL_subtreeData);
168-
}
169-
if (uiState.accessPolicyOverride != null) {
170-
for (const resource of resources) {
171-
if (resource instanceof IR_NodeAndRevision) {
172-
resource.node.accessPolicy = uiState.accessPolicyOverride;
161+
try {
162+
if (uiState.sourceType == DataExchangeFormat.json_dm && forJSONDM_subtreeData != null) {
163+
resources = GetResourcesInImportSubtree_JsonDm(forJSONDM_subtreeData, node);
164+
} else if (uiState.sourceType == DataExchangeFormat.json_dm_fs && forJSONDMFS_subtreeData != null) {
165+
resources = GetResourcesInImportSubtree_JsonDmFs(forJSONDMFS_subtreeData);
166+
} else if (uiState.sourceType == DataExchangeFormat.json_cg && forJSONCG_subtreeData != null) {
167+
resources = GetResourcesInImportSubtree_CG(importContext, forJSONCG_subtreeData);
168+
} else if (uiState.sourceType == DataExchangeFormat.csv_sl && forCSVSL_subtreeData != null) {
169+
resources = GetResourcesInImportSubtree_CSV_SL(forCSVSL_subtreeData);
170+
}
171+
if (uiState.accessPolicyOverride != null) {
172+
for (const resource of resources) {
173+
if (resource instanceof IR_NodeAndRevision) {
174+
resource.node.accessPolicy = uiState.accessPolicyOverride;
175+
}
173176
}
174177
}
178+
} catch (ex) {
179+
resources_parseError = ex as Error;
180+
console.error("Error while processing import-subtree data:", ex);
175181
}
176182
}
177-
this.Stash({resources});
183+
this.Stash({resources, resources_parseError});
178184

179185
const selectedIRs_nodeAndRev = [...uiState.selectedImportResources].filter(a=>a instanceof IR_NodeAndRevision) as IR_NodeAndRevision[];
180186
const selectedIRs_nodeAndRev_importedSoFar = selectedIRs_nodeAndRev_atImportStart - selectedIRs_nodeAndRev.length;
@@ -283,14 +289,7 @@ class ImportSubtreeUI extends BaseComponent<
283289
} else if (uiState.sourceType == DataExchangeFormat.json_cg) {
284290
let subtreeData_new: CG_Node|n = null;
285291
try {
286-
const rawData = FromJSON(newSourceText);
287-
if ("questions" in rawData) {
288-
subtreeData_new = rawData as CG_Node;
289-
} else if ("positions" in rawData) {
290-
subtreeData_new = {
291-
questions: [rawData],
292-
} as CG_Node;
293-
}
292+
subtreeData_new = FromJSON(newSourceText);
294293
newState.forJSONCG_subtreeData = subtreeData_new;
295294
newState.sourceText_parseError = null;
296295
} catch (err) {
@@ -403,6 +402,8 @@ class ImportSubtreeUI extends BaseComponent<
403402
</PolicyPicker>
404403
</Row>
405404
<ScrollView>
405+
{resources_parseError != null &&
406+
<Text mt={5} sel style={{color: "red", whiteSpace: "pre-wrap"}}>Error while processing import-subtree data: {resources_parseError.stack ?? resources_parseError.toString()}</Text>}
406407
<ReactList type="variable" length={resources.length}
407408
itemRenderer={this.RenderResource}
408409
itemSizeEstimator={this.EstimateResourceUIHeight}/>

Packages/client/Source/Utils/DataFormats/JSON/ClaimGen/DataModel.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import {Attachment, AttachmentType, DescriptionAttachment, NodeType, QuoteAttachment, ReferencesAttachment, Source, SourceChain, SourceType} from "dm_common";
22
import {Assert, IsString} from "js-vextensions";
33

4+
export const CG_Node_keysForChildrenObjects: Array<keyof CG_Node> = ["positions", "categories", "claims", "arguments"] as const;
45
export class CG_Node {
56
constructor(data: Partial<CG_Node>, _isSyntheticNodeObj_fromStringCollection: "atomic_claims" | "counter_claims" | "examples") {
67
Object.assign(this, data, {_isSyntheticNodeObj_fromStringCollection});
@@ -21,7 +22,15 @@ export class CG_Node {
2122
if (node._isSyntheticNodeObj_fromStringCollection == "counter_claims") return NodeType.claim;
2223
if (node._isSyntheticNodeObj_fromStringCollection == "examples") return NodeType.claim;
2324
}
24-
throw new Error("Cannot discern node-type for CG_Node: " + JSON.stringify(node));
25+
26+
console.warn("Cannot discern node-type for CG_Node, so using fallback node-type of category. @data: " + JSON.stringify(node, function(key, value) {
27+
// for children-object collections, just show the number of children, to avoid cluttering the error message
28+
if (CG_Node_keysForChildrenObjects.includes(key as any) && Array.isArray(value)) {
29+
return `<children count: ${value.length}; actual json omitted for brevity>`;
30+
}
31+
return value;
32+
}, "\t"));
33+
return NodeType.category; // fallback node-type (can happen for claim-gen exports that are just a "root node" with an "arguments" children-collection, but no text or anything for that root container-node)
2534
}
2635

2736
// special fields (added by importer itself, during interpretation process)

Packages/client/Source/Utils/DataFormats/JSON/ClaimGen/ImportHelpers.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,24 +9,24 @@ export class ImportContext {
99
}
1010

1111
export const GetResourcesInImportSubtree_CG = CreateAccessor((context: ImportContext, rootNode: CG_Node)=>{
12-
return GetResourcesInImportNode_CG(context, rootNode, [], [], null);
12+
return GetResourcesInImportNode_CG(context, rootNode, [0], [CG_Node.GetText(rootNode)], null, null);
1313
});
14-
export const GetResourcesInImportNode_CG = CreateAccessor((context: ImportContext, node: CG_Node, path_indexes: number[], path_titles: string[], parentResource: ImportResource|n)=>{
14+
export const GetResourcesInImportNode_CG = CreateAccessor((context: ImportContext, node: CG_Node, path_indexes: number[], path_titles: string[], parentResource: ImportResource|n, parentNode: CG_Node|n)=>{
1515
const result = [] as ImportResource[];
1616

1717
// own node
18-
const ownResource = NewNodeResource(context, node, path_indexes, path_titles, parentResource);
18+
const ownResource = NewNodeResource(context, node, path_indexes, path_titles, parentResource, parentNode);
1919
result.push(ownResource);
2020

2121
// children nodes
2222
for (const [i, child] of CG_Node.GetChildren(node).entries()) {
23-
result.push(...GetResourcesInImportNode_CG(context, child, path_indexes.concat(i), path_titles.concat(CG_Node.GetText(child)), ownResource));
23+
result.push(...GetResourcesInImportNode_CG(context, child, path_indexes.concat(i), path_titles.concat(CG_Node.GetText(child)), ownResource, node));
2424
}
2525

2626
return result;
2727
});
2828

29-
export const NewNodeResource = CreateAccessor((context: ImportContext, data: CG_Node, path_indexes: number[], path_titles: string[], parentResource: ImportResource|n)=>{
29+
export const NewNodeResource = CreateAccessor((context: ImportContext, data: CG_Node, path_indexes: number[], path_titles: string[], parentResource: ImportResource|n, parentData: CG_Node|n)=>{
3030
const nodeType = CG_Node.GetNodeType(data);
3131
const node = new NodeL1({
3232
type: nodeType,
@@ -39,7 +39,7 @@ export const NewNodeResource = CreateAccessor((context: ImportContext, data: CG_
3939
orderKey = orderKey.next();
4040
}
4141
const link = new NodeLink({
42-
group: ChildGroup.generic,
42+
group: parentData && CG_Node.GetNodeType(parentData) == NodeType.claim ? ChildGroup.freeform : ChildGroup.generic,
4343
orderKey: orderKey.toString(),
4444
//form: claimForm,
4545
});

0 commit comments

Comments
 (0)