-
Notifications
You must be signed in to change notification settings - Fork 3
ENG-332 content to concept conversion functions #220
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
8 commits
Select commit
Hold shift + click to select a range
3763245
ENG-332 content to concept conversion
maparent d3a8d39
error checking (coderabbit)
maparent 3d03ecf
comply to parameter count limit
maparent 92b1032
style and add orderConceptUpsertsByDependency
maparent a35ac16
return unknows
maparent b7ee7ec
repair serious mistakes. More comments
maparent 81faf60
last mistake
maparent 663cf67
last adjustments
maparent File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,240 @@ | ||
| import { DiscourseNode } from "./getDiscourseNodes"; | ||
| import getDiscourseRelations from "./getDiscourseRelations"; | ||
| import type { DiscourseRelation } from "./getDiscourseRelations"; | ||
| import type { SupabaseContext } from "~/utils/supabaseContext"; | ||
|
|
||
| import type { LocalConceptDataInput } from "@repo/database/inputTypes"; | ||
|
|
||
| const getNodeExtraData = ( | ||
| node_uid: string, | ||
| ): { | ||
| author_uid: string; | ||
| created: string; | ||
| last_modified: string; | ||
| page_uid: string; | ||
| } => { | ||
| const result = window.roamAlphaAPI.q( | ||
| `[ | ||
| :find | ||
| ?author_uid | ||
| ?page_uid | ||
| ?created | ||
| ?last_modified | ||
| :in $ ?block_uid | ||
| :where | ||
| [?block :block/uid ?block_uid] | ||
| [?block :create/user ?author_id] | ||
| [?author_id :user/uid ?author_uid] | ||
| [?block :create/time ?created] | ||
| [?block :edit/time ?last_modified] | ||
| [(get-else $ ?block :block/page ?block) ?page_id] | ||
| [?page_id :block/uid ?page_uid] | ||
| ]`, | ||
| node_uid, | ||
| ); | ||
| if (result.length !== 1 || result[0].length !== 4) | ||
| throw new Error("Invalid result from Roam query"); | ||
|
|
||
| const [author_uid, page_uid, created_t, last_modified_t] = result[0] as [ | ||
| string, | ||
| string, | ||
| number, | ||
| number, | ||
| ]; | ||
| const created = new Date(created_t).toISOString(); | ||
| const last_modified = new Date(last_modified_t).toISOString(); | ||
| return { | ||
| author_uid, | ||
| created, | ||
| last_modified, | ||
| page_uid, | ||
| }; | ||
| }; | ||
|
|
||
| export const discourseNodeSchemaToLocalConcept = ( | ||
| context: SupabaseContext, | ||
| node: DiscourseNode, | ||
| ): LocalConceptDataInput => { | ||
| const titleParts = node.text.split("/"); | ||
| return { | ||
| space_id: context.spaceId, | ||
| name: titleParts[titleParts.length - 1], | ||
| represented_by_local_id: node.type, | ||
| is_schema: true, | ||
| ...getNodeExtraData(node.type), | ||
| }; | ||
| }; | ||
|
|
||
| export const discourseNodeBlockToLocalConcept = ( | ||
| context: SupabaseContext, | ||
| { | ||
| nodeUid, | ||
| schemaUid, | ||
| text, | ||
| }: { | ||
| nodeUid: string; | ||
| schemaUid: string; | ||
| text: string; | ||
| }, | ||
| ): LocalConceptDataInput => { | ||
| return { | ||
| space_id: context.spaceId, | ||
| name: text, | ||
| represented_by_local_id: nodeUid, | ||
| schema_represented_by_local_id: schemaUid, | ||
| is_schema: false, | ||
| ...getNodeExtraData(nodeUid), | ||
| }; | ||
| }; | ||
|
|
||
| const STANDARD_ROLES = ["source", "target"]; | ||
|
|
||
| export const discourseRelationSchemaToLocalConcept = ( | ||
| context: SupabaseContext, | ||
| relation: DiscourseRelation, | ||
| ): LocalConceptDataInput => { | ||
| return { | ||
| space_id: context.spaceId, | ||
| represented_by_local_id: relation.id, | ||
| // Not using the label directly, because it is not unique and name should be unique | ||
| name: `${relation.id}-${relation.label}`, | ||
| is_schema: true, | ||
| local_reference_content: Object.fromEntries( | ||
| Object.entries(relation).filter(([key, v]) => | ||
| STANDARD_ROLES.includes(key), | ||
| ), | ||
| ) as { [key: string]: string }, | ||
| literal_content: { | ||
| roles: STANDARD_ROLES, | ||
| label: relation.label, | ||
| complement: relation.complement, | ||
| representation: relation.triples.map((t) => t[0]), | ||
| }, | ||
| ...getNodeExtraData(relation.id), | ||
| }; | ||
| }; | ||
|
|
||
| export const discourseRelationDataToLocalConcept = ( | ||
| context: SupabaseContext, | ||
maparent marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| relationSchemaUid: string, | ||
| relationNodes: { [role: string]: string }, | ||
| ): LocalConceptDataInput => { | ||
| const roamRelation = getDiscourseRelations().find( | ||
| (r) => r.id === relationSchemaUid, | ||
| ); | ||
| if (roamRelation === undefined) { | ||
| throw new Error(`Invalid roam relation id ${relationSchemaUid}`); | ||
| } | ||
| const relation = discourseRelationSchemaToLocalConcept(context, roamRelation); | ||
| const litContent = (relation.literal_content | ||
| ? relation.literal_content | ||
| : {}) as unknown as { [key: string]: any }; | ||
| const roles = (litContent["roles"] as string[] | undefined) || STANDARD_ROLES; | ||
| const casting: { [role: string]: string } = Object.fromEntries( | ||
| roles | ||
| .map((role) => [role, relationNodes[role]]) | ||
| .filter(([, uid]) => uid !== undefined), | ||
| ); | ||
| if (Object.keys(casting).length === 0) { | ||
| throw new Error( | ||
| `No valid node UIDs supplied for roles ${roles.join(", ")}`, | ||
| ); | ||
| } | ||
| // TODO: Also get the nodes from the representation, using QueryBuilder. That will likely give me the relation object | ||
| const nodeData = Object.values(casting).map((v) => getNodeExtraData(v)); | ||
| // roundabout way to do a max from stringified dates | ||
| const last_modified = new Date( | ||
| Math.max(...nodeData.map((nd) => new Date(nd.last_modified).getTime())), | ||
| ).toISOString(); | ||
| // creation is actually creation of the relation node, not the rest of the cast, but this will do as a first approximation. | ||
| // Still using max, since the relation cannot be created before its cast | ||
| const created = new Date( | ||
| Math.max(...nodeData.map((nd) => new Date(nd.created).getTime())), | ||
| ).toISOString(); | ||
| const author_local_id: string = nodeData[0].author_uid; // take any one; again until I get the relation object | ||
| const represented_by_local_id = | ||
| casting["target"] || Object.values(casting)[0]; // This one is tricky. Prefer the target for now. | ||
| return { | ||
| space_id: context.spaceId, | ||
| represented_by_local_id, | ||
| author_local_id, | ||
| created, | ||
| last_modified, | ||
| name: `${relationSchemaUid}-${Object.values(casting).join("-")}`, | ||
| is_schema: false, | ||
| schema_represented_by_local_id: relationSchemaUid, | ||
| local_reference_content: casting, | ||
| }; | ||
| }; | ||
|
|
||
| export const relatedConcepts = (concept: LocalConceptDataInput): string[] => { | ||
| const relations = Object.values( | ||
| concept.local_reference_content || {}, | ||
| ).flat() as string[]; | ||
| if (concept.schema_represented_by_local_id) { | ||
| relations.push(concept.schema_represented_by_local_id); | ||
| } | ||
| // remove duplicates | ||
maparent marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| return [...new Set(relations)]; | ||
| }; | ||
|
|
||
| const orderConceptsRec = ( | ||
| ordered: LocalConceptDataInput[], | ||
| concept: LocalConceptDataInput, | ||
| remainder: { [key: string]: LocalConceptDataInput }, | ||
| ): Set<string> => { | ||
| const relatedConceptIds = relatedConcepts(concept); | ||
| let missing: Set<string> = new Set(); | ||
| while (relatedConceptIds.length > 0) { | ||
| const relatedConceptId = relatedConceptIds.shift()!; | ||
| const relatedConcept = remainder[relatedConceptId]; | ||
| if (relatedConcept === undefined) { | ||
| missing.add(relatedConceptId); | ||
| } else { | ||
| missing = missing.union( | ||
| orderConceptsRec(ordered, relatedConcept, remainder), | ||
| ); | ||
| delete remainder[relatedConceptId]; | ||
| } | ||
| } | ||
| ordered.push(concept); | ||
| delete remainder[concept.represented_by_local_id!]; | ||
| return missing; | ||
| }; | ||
|
|
||
| /* | ||
| If writing a concept upsert method, you want to insure that | ||
| a node's dependencies are defined before the node itself is upserted. | ||
| The dependencies are as defined in relatedConcepts. | ||
| If you upsert in the following order: [node schemas, relation schemas, nodes, relations] | ||
| then the depencies will be implicitly respected. | ||
| (It will be tricker when we have recursive relations.) | ||
| If you are starting from a random stream of nodes, you would want to order them with this function. | ||
| It assumes all input has defined represented_by_local_id, | ||
| and that nodes that are not in the upsert set are already in the database. | ||
| the Id of those nodes is returned and can be used to check that assumption. | ||
| We also assume that there are no dependency cycles. | ||
| */ | ||
| export const orderConceptsByDependency = ( | ||
| concepts: LocalConceptDataInput[], | ||
| ): { ordered: LocalConceptDataInput[]; missing: string[] } => { | ||
| if (concepts.length === 0) return { ordered: concepts, missing: [] }; | ||
| const conceptById: { [key: string]: LocalConceptDataInput } = | ||
| Object.fromEntries(concepts.map((c) => [c.represented_by_local_id, c])); | ||
| const ordered: LocalConceptDataInput[] = []; | ||
| let missing: Set<string> = new Set(); | ||
| while (Object.keys(conceptById).length > 0) { | ||
| const first = Object.values(conceptById)[0]; | ||
| missing = missing.union(orderConceptsRec(ordered, first, conceptById)); | ||
| } | ||
| return { ordered, missing: [...missing] }; | ||
| }; | ||
|
|
||
| // the input to the upsert method would look like this: | ||
|
|
||
| // const idata: LocalConceptDataInput[] = [ | ||
| // { "name": "Claim", "author_local_id": "sR22zZ470dNPkIf9PpjQXXdTBjG2", "represented_by_local_id": "a_roam_uid", "created": "2000/01/01", "last_modified": "2001/01/02", "is_schema": true }, | ||
| // { "name": "A Claim", "author_local_id": "sR22zZ470dNPkIf9PpjQXXdTBjG2", "represented_by_local_id": "a_roam_uid2", "created": "2000/01/03", "last_modified": "2001/01/04", "is_schema": false, "schema_represented_by_local_id": "a_roam_uid" }, | ||
| // { "name": "test2", "author_local_id": "sR22zZ470dNPkIf9PpjQXXdTBjG2", "created": "2000/01/04", "last_modified": "2001/01/05", "is_schema": false, "literal_content": { "source": "a_roam_uid", "target": ["a_roam_uid", "a_roam_uid2"] }, "local_reference_content": { "source": "a_roam_uid", "target": ["a_roam_uid", "a_roam_uid2"] } }] | ||
|
|
||
| // const { data, error } = await supabase_client.rpc("upsert_concepts", { v_space_id: 12, data: idata }); | ||
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.