add updating and deleting of porfile data with terminus and mermurations, and indexing with Meilisearch

This commit is contained in:
Darren Zal 2023-03-20 23:44:42 -06:00
parent 16522fb46b
commit 56671bdae2
17 changed files with 793 additions and 231 deletions

View File

@ -1,40 +1,2 @@
import csv
from itertools import islice
from schema import ImpactArea, Blockchain, Topic, Web3, Organization
from terminusdb_client import WOQLClient
from datetime import datetime
import pytz
import re
import emoji
# we keep all the information in dictionaries with Employee id as keys
orgs = {}
client = WOQLClient("https://cloud.terminusdb.com/Myseelia/")
client.connect(db="playground3", team="Myseelia", use_token=True)
import re
orgs[0] = Organization(
# assignee = "",
# blockchainecosystem = set(),
# description = "",
# logo = "",
name = "darren"
# preJan20thUpvotes = 0,
# reviewed = "",
# submittedbyemail = "",
# submittedbyname = "",
# submittedbyowner = "",
# subscribed = "",
# topic = set(),
# upvotes = 0,
# web3 = set(),
# impactarea = set(),
# datecreated = datetime.min
)
client.insert_document(list(orgs.values()), commit_msg="Adding 4 orgs")
[{'@id': 'Organization/85405b13178f8e090cbcd58fd22111c0f72f77b59cbe00bb389ecd04c41d0e90', '@type': 'Organization', 'assignee': 'https://2local.io/', 'blockchainecosystem': ['BinanceSmartChain'], 'datecreated': '2022-05-07T11:03:00Z', 'description': '2local loyalty platform with the goal to achieve a sustainable world with prosperity for all. The cashback system supports sustainable and local-to-local working businesses. This cashback is generated from the profit from Yield Farms and Staking Pools. 2local doesnt profit from its users but creates value with its users.', 'impactarea': ['SocialJustice'], 'name': '2local.io',
'preJan20thUpvotes': '4', 'reviewed': 'checked', 'topic': ['Marketplace'], 'upvotes': '4'}, {'@id': 'Organization/f2dd1e11327e68d3ca3550cdec338186032d4fc4de4e9924ff97a8260a549be7', '@type': 'Organization', 'assignee': 'https://www.acredaos.com/', 'blockchainecosystem': ['Ethereum'], 'datecreated': '2022-05-07T11:03:00Z', 'description': 'ACRE DAOs is a decentralized impact investment club and Web3 access portal to the community of ACRE Invest token holders.', 'impactarea': ['SocialJustice'], 'name': 'ACRE DAOs\n', 'preJan20thUpvotes': '2', 'reviewed': 'checked', 'topic': ['Investing', 'Land'], 'upvotes': '2', 'web3': ['DAO']}]

View File

@ -0,0 +1,290 @@
<script lang="ts">
import cytoscape from 'cytoscape'
import { onMount } from 'svelte'
import { bubble } from 'svelte/internal'
import TerminusClient from '@terminusdb/terminusdb-client'
import { MeiliSearch } from 'meilisearch'
import { generateKnowledgeGraph } from './cytoscape.ts'
let cy
interface INodeData {
id: string
}
interface INode {
data: INodeData
}
interface IEdgeData {
id: string
source: string
target: string
label: string
}
interface IEdge {
data: IEdgeData
}
import json_graph from './knowledge_graph.json'
let knowledgeGraphJson: any = json_graph
// knowledgeGraphJson = await response.json()
// } else {
// alert(`HTTP-Error: ${response.status}`)
// }
// }
let nodes: INode[] = []
let edges: IEdge[] = []
onMount(async () => {
nodes = knowledgeGraphJson.entities.map((entity: any) => ({
data: { id: entity.id, label: entity.label }
}))
edges = knowledgeGraphJson.relations.map(
(relation: any, index: string) => ({
data: {
id: index,
source: relation.source,
target: relation.target,
label: relation.type
}
})
)
cy = cytoscape({
container: document.getElementById('cy'),
elements: {
nodes,
edges
},
style: [
{
selector: 'node',
style: {
'text-valign': 'center',
'text-halign': 'center',
'text-wrap': 'wrap',
'text-max-width': function (ele) {
return Math.max(1, Math.ceil(ele.degree() / 2)) * 30
},
'font-size': function (ele) {
return Math.max(1, Math.ceil(ele.degree() / 2)) * 6
},
'background-color': '#75f6df',
'border-color': '#223152',
'border-width': function (ele) {
return Math.max(1, Math.ceil(ele.degree() / 2))
},
label: 'data(label)',
width: function (ele) {
return Math.max(1, Math.ceil(ele.degree() / 2)) * 40
},
height: function (ele) {
return Math.max(1, Math.ceil(ele.degree() / 2)) * 40
}
}
},
{
selector: 'edge',
style: {
'font-size': 20,
width: 5,
'line-color': '#223152',
'target-arrow-color': '#223152',
'target-arrow-shape': 'triangle',
'curve-style': 'bezier',
'text-rotation': 'autorotate',
'text-offset': { x: 20, y: -20 },
'text-background-opacity': 1,
'text-background-color': '#fafafa',
'text-background-shape': 'roundrectangle',
label: 'data(label)'
}
}
],
layout: {
name: 'cose'
// infinite: true,
}
})
cy.nodes().forEach(function (node) {
node.data({
degree: node.connectedEdges().length
})
})
var nodes = cy.nodes()
nodes = nodes.sort(function (a, b) {
return b.data('degree') - a.data('degree')
})
var top100 = nodes.slice(0, 1000)
//console.log(top100)
cy.nodes().forEach(function (node) {
if (!top100.includes(node)) {
node.hide()
}
})
let toggle = true
// cy.off('tap', 'node', event => {
// const node = event.target;
// const nodeId = node.data('id');
// alert('unDisplay info for ' + nodeId);
// });
cy.on('tap', 'node', function (evt) {
var node = evt.target
var connectedEdges = node.connectedEdges()
var connectedNodes = node.neighborhood().nodes()
var allElements = cy.elements()
var allNodes = cy.nodes()
var allEdges = cy.edges()
if (node.style('display') == 'element') {
// hide all nodes and edges except the selected node and its neighbors
allNodes.style('display', 'none')
allEdges.style('display', 'none')
connectedNodes.style('display', 'element')
node.style('display', 'element')
connectedEdges.style('display', 'element')
} else {
// show all nodes and edges
allNodes.style('display', 'element')
allEdges.style('display', 'element')
}
})
// Reset the state when clicking away from the node
cy.on('tap', function (e) {
if (e.target === cy) {
cy.nodes().style('display', 'element')
cy.edges().style('display', 'element')
cy.nodes().data('highlighted', false)
}
})
cy.on('tap', 'edge', event => {
const edge = event.target
const edgeId = edge.data('id')
alert('Display info for ' + edgeId)
})
// cy.on('tap', 'node', function(){
// alert("put code here"));
// });
// cy.layout({
// name: 'cola'
// }).run();
})
var searchTerm = ''
function updateSearchTerm(e) {
searchTerm = e.target.value
// Perform search in real timebased on searchTerm here
}
async function entered(e) {
const searchclient = new MeiliSearch({
host: 'https://ms-9ea4a96f02a8-1969.sfo.meilisearch.io',
apiKey: '117c691a34b21a6651798479ebffd181eb276958'
})
const index = searchclient.index('people')
// this will search both keys and values
// const search = await index.search(e.target.value.toString(), { q: '*' });
// const searchResult = await index.search('orgs', {
// attributesToRetrieve: ['id']
// })
const searchResult = await index.search(e.target.value.toString())
// need to turn the search results into an array of ids which can be used to query the knowledge graph
const resultsgraph = await generateKnowledgeGraph(searchResult.hits).then(
resultsgraph => {
// console.log(resultsgraph)
const allNodes = resultsgraph.entities.map((entity: any) => ({
data: { id: entity.id, label: entity.label }
}))
const allEdges = resultsgraph.relations.map(
(relation: any, index: string) => ({
data: {
id: index,
source: relation.source,
target: relation.target,
label: relation.type
}
})
)
cy.remove(cy.elements())
cy.add(allNodes)
cy.add(allEdges)
cy.layout({
name: 'cose'
// other layout options here
}).run()
}
)
}
</script>
<div class="pt-8 p-6 md:p-8 mx-auto">
<input
id="search"
type="text"
placeholder="Search..."
on:input={updateSearchTerm}
on:keydown={event => {
if (event.keyCode === 13) {
entered(event)
}
}}
/>
</div>
<section class="overflow-hidden text-gray-700">
<div class="cyDiv" />
<div id="cy" />
</section>
<style>
#search {
position: absolute;
top: 10px;
z-index: 100;
background-color: white;
width: 50%;
height: 40px;
border-radius: 20px;
padding: 10px 20px 10px 40px;
}
#search input[type='text'] {
position: absolute;
top: 0px;
left: 50px;
width: 80%;
height: 100%;
border: none;
background-color: transparent;
font-size: 18px;
}
#cy-div {
z-index: 99;
}
#cy {
width: 100%;
height: 95%;
position: absolute;
top: 55px;
left: 0px;
}
</style>

View File

@ -41,20 +41,6 @@
let edges: IEdge[] = []
onMount(async () => {
nodes = knowledgeGraphJson.entities.map((entity: any) => ({
data: { id: entity.id, label: entity.label }
}))
edges = knowledgeGraphJson.relations.map(
(relation: any, index: string) => ({
data: {
id: index,
source: relation.source,
target: relation.target,
label: relation.type
}
})
)
cy = cytoscape({
container: document.getElementById('cy'),
@ -113,6 +99,51 @@
}
})
const searchclient = new MeiliSearch({
host: 'https://ms-9ea4a96f02a8-1969.sfo.meilisearch.io',
apiKey: '117c691a34b21a6651798479ebffd181eb276958'
})
const index = searchclient.index('people')
// this will search both keys and values
// const search = await index.search(e.target.value.toString(), { q: '*' });
// const searchResult = await index.search('orgs', {
// attributesToRetrieve: ['id']
// })
const searchResult = await index.getDocuments(
{
limit: 1000
}
)
console.log(searchResult)
// need to turn the search results into an array of ids which can be used to query the knowledge graph
const resultsgraph = await generateKnowledgeGraph(searchResult.results).then(
resultsgraph => {
console.log(resultsgraph)
const allNodes = resultsgraph.entities.map((entity: any) => ({
data: { id: entity.id, label: entity.label }
}))
const allEdges = resultsgraph.relations.map(
(relation: any, index: string) => ({
data: {
id: index,
source: relation.source,
target: relation.target,
label: relation.type
}
})
)
cy.remove(cy.elements())
cy.add(allNodes)
cy.add(allEdges)
cy.layout({
name: 'cose'
// other layout options here
}).run()
}
)
cy.nodes().forEach(function (node) {
node.data({
degree: node.connectedEdges().length

View File

@ -5,17 +5,24 @@ import * as fs from 'fs'
const WOQL = TerminusClient.WOQL
function findNameById(ids: object[], id: string): string {
console.log('finding id ' + id);
const document = ids.find(doc => 'person/' + doc['id'] === id);
console.log('found name ' + JSON.parse(document['name']));
return document ? JSON.parse(document['name']) : '';
}
export async function generateKnowledgeGraph(ids: object[]): Promise<object> {
console.log(ids)
const entities: { id: string; label: string; type: string }[] = []
const relations: { source: string; target: string; type: string }[] = []
for (const document of ids) {
const personid = 'Person/' + document['id']
const personid = 'person/' + document['id']
let personEntity = entities.find(entity => entity.id === personid)
if (!personEntity) {
entities.push({
id: personid,
label: document['name'],
label: typeof document['name'] === 'string' && document['name'].startsWith('"') && document['name'].endsWith('"') ? JSON.parse(document['name']) : document['name'],
type: 'person'
})
personEntity = entities[entities.length - 1]
@ -23,25 +30,27 @@ export async function generateKnowledgeGraph(ids: object[]): Promise<object> {
const linktypes = ['vouches_for', 'LI']
for (const link of linktypes) {
let linkValues = document[link]
try {
linkValues = JSON.parse(linkValues)
} catch (error) {
console.log(error)
let linkValues = document[link];
if (typeof linkValues === 'string') {
try {
linkValues = JSON.parse(linkValues);
} catch (error) {
console.error(`Error parsing JSON for link "${link}":`, error);
}
}
if (Array.isArray(linkValues)) {
for (const linkValue of linkValues) {
if (linkValue === undefined) continue
const linkId = linkValue.replace(/^"|"$/g, '')
if (linkId !== undefined && linkId !== '') {
if (linkId !== undefined && linkId !== '' && linkId.startsWith('person/')) {
let linkEntity = entities.find(
entity => entity.id === linkId
)
if (!linkEntity) {
entities.push({
id: linkId,
label: linkValue,
type: 'attribute'
label: findNameById(ids, linkId),
type: 'person'
})
linkEntity = entities[entities.length - 1]
}
@ -60,8 +69,7 @@ export async function generateKnowledgeGraph(ids: object[]): Promise<object> {
}
} else {
let linkId = linkValues
// console.log(linkId)
if (linkId !== undefined && linkId !== '') {
if (linkId !== undefined && linkId !== '' && linkId.startsWith('person/')) {
linkId = linkId.replace(/^"|"$/g, '')
let linkEntity = entities.find(
entity => entity.id === linkId
@ -70,7 +78,7 @@ export async function generateKnowledgeGraph(ids: object[]): Promise<object> {
entities.push({
id: linkId,
label: document[link],
type: 'attribute'
type: 'person'
})
linkEntity = entities[entities.length - 1]
}

View File

@ -256,6 +256,7 @@
top: 10px;
z-index: 100;
background-color: white;
color: black;
width: 50%;
height: 40px;
border-radius: 20px;

View File

@ -5,10 +5,11 @@
import TerminusClient from "@terminusdb/terminusdb-client"
import { filesystemStore, sessionStore } from '$src/stores'
import { AREAS, galleryStore } from '$routes/gallery/stores'
import { getImagesFromWNFS, type Image } from '$routes/gallery/lib/gallery'
import { getJSONFromWNFS, type Image } from '$routes/gallery/lib/gallery'
import FileUploadCard from '$routes/gallery/components/upload/FileUploadCard.svelte'
import ImageCard from '$routes/gallery/components/imageGallery/ImageCard.svelte'
import ImageModal from '$routes/gallery/components/imageGallery/ImageModal.svelte'
import { ipfsGatewayUrl } from '$lib/app-info';
const client = new TerminusClient.WOQLClient(
"https://cloud.terminusdb.com/Myseelia",{
@ -42,7 +43,7 @@
const clearSelectedImage = () => (selectedImage = null)
// If galleryStore.selectedArea changes from private to public, re-run getImagesFromWNFS
// If galleryStore.selectedArea changes from private to public, re-run getJSONFromWNFS
let selectedArea = null
const unsubscribeGalleryStore = galleryStore.subscribe(async updatedStore => {
// Get initial selectedArea
@ -52,7 +53,7 @@
if (selectedArea !== updatedStore.selectedArea) {
selectedArea = updatedStore.selectedArea
await getImagesFromWNFS()
await getJSONFromWNFS()
}
})
@ -62,7 +63,7 @@
if (newState.authed && $filesystemStore && !imagesFetched) {
imagesFetched = true
// Get images from the user's public WNFS
getImagesFromWNFS()
getJSONFromWNFS()
}
})
@ -162,6 +163,13 @@ button{
<br />
<button class="bg-blue-500 text-white dark:text-black" type="submit">Submit</button>
</form>
<!-- <a
href={`https://ipfs.${ipfsGatewayUrl}/ipfs/${image.cid}/userland`}
target="_blank"
class="underline mb-4 hover:text-slate-500"
>
View on IPFS
</a> -->
</div>
{#if selectedImage}

View File

@ -4,7 +4,7 @@
import { filesystemStore, sessionStore } from '$src/stores'
import { AREAS, galleryStore } from '$routes/gallery/stores'
import { getImagesFromWNFS, type Image } from '$routes/gallery/lib/gallery'
import { getJSONFromWNFS, type Image } from '$routes/gallery/lib/gallery'
import FileUploadCard from '$routes/gallery/components/upload/FileUploadCard.svelte'
import ImageCard from '$routes/gallery/components/imageGallery/ImageCard.svelte'
import ImageModal from '$routes/gallery/components/imageGallery/ImageModal.svelte'
@ -17,7 +17,7 @@
const setSelectedImage: (image: Image) => void = image =>
(selectedImage = image)
// If galleryStore.selectedArea changes from private to public, re-run getImagesFromWNFS
// If galleryStore.selectedArea changes from private to public, re-run getJSONFromWNFS
let selectedArea = null
const unsubscribeGalleryStore = galleryStore.subscribe(async updatedStore => {
// Get initial selectedArea
@ -36,7 +36,7 @@
if (newState.authed && $filesystemStore && !imagesFetched) {
imagesFetched = true
// Get images from the user's public WNFS
getImagesFromWNFS()
getJSONFromWNFS()
}
})

View File

@ -1,7 +1,7 @@
<script lang="ts">
import {
getImagesFromWNFS,
uploadImageToWNFS
getJSONFromWNFS,
uploadJSONToWNFS
} from '$routes/gallery/lib/gallery'
import { addNotification } from '$lib/notifications'
@ -33,14 +33,14 @@
addNotification('Please upload images only', 'error')
console.error('Please upload images only')
} else {
await uploadImageToWNFS(file)
await uploadJSONToWNFS(file)
}
}
})
)
// Refetch images and update galleryStore
await getImagesFromWNFS()
await getJSONFromWNFS()
// Disable isDragging state
isDragging = false

View File

@ -47,7 +47,7 @@ const FILE_SIZE_LIMIT = 5
/**
* Get images from the user's WNFS and construct the `src` value for the images
*/
export const getImagesFromWNFS: () => Promise<void> = async () => {
export const getJSONFromWNFS: () => Promise<void> = async () => {
try {
// Set loading: true on the galleryStore
galleryStore.update(store => ({ ...store, loading: true }))
@ -182,7 +182,7 @@ export const deleteImageFromWNFS: (
addNotification(`${name} image has been deleted`, 'success')
// Refetch images and update galleryStore
await getImagesFromWNFS()
await getJSONFromWNFS()
} else {
throw new Error(`${name} image has already been deleted`)
}
@ -205,5 +205,5 @@ export const handleFileInput: (
)
// Refetch images and update galleryStore
await getImagesFromWNFS()
await getJSONFromWNFS()
}

View File

@ -5,7 +5,7 @@
import TerminusClient from "@terminusdb/terminusdb-client"
import { filesystemStore, sessionStore } from '$src/stores'
import { AREAS, galleryStore } from '$routes/gallery/stores'
import { getImagesFromWNFS, type Image } from '$routes/gallery/lib/gallery'
import { getJSONFromWNFS, type Image } from '$routes/gallery/lib/gallery'
import FileUploadCard from '$routes/gallery/components/upload/FileUploadCard.svelte'
import ImageCard from '$routes/gallery/components/imageGallery/ImageCard.svelte'
import ImageModal from '$routes/gallery/components/imageGallery/ImageModal.svelte'
@ -42,7 +42,7 @@
const clearSelectedImage = () => (selectedImage = null)
// If galleryStore.selectedArea changes from private to public, re-run getImagesFromWNFS
// If galleryStore.selectedArea changes from private to public, re-run getJSONFromWNFS
let selectedArea = null
const unsubscribeGalleryStore = galleryStore.subscribe(async updatedStore => {
// Get initial selectedArea
@ -52,7 +52,7 @@
if (selectedArea !== updatedStore.selectedArea) {
selectedArea = updatedStore.selectedArea
await getImagesFromWNFS()
await getJSONFromWNFS()
}
})
@ -62,7 +62,7 @@
if (newState.authed && $filesystemStore && !imagesFetched) {
imagesFetched = true
// Get images from the user's public WNFS
getImagesFromWNFS()
getJSONFromWNFS()
}
})

View File

@ -4,7 +4,7 @@
import { filesystemStore, sessionStore } from '$src/stores'
import { AREAS, galleryStore } from '$routes/gallery/stores'
import { getImagesFromWNFS, type Image } from '$routes/gallery/lib/gallery'
import { getJSONFromWNFS, type Image } from '$routes/gallery/lib/gallery'
import FileUploadCard from '$routes/gallery/components/upload/FileUploadCard.svelte'
import ImageCard from '$routes/gallery/components/imageGallery/ImageCard.svelte'
import ImageModal from '$routes/gallery/components/imageGallery/ImageModal.svelte'
@ -17,7 +17,7 @@
const setSelectedImage: (image: Image) => void = image =>
(selectedImage = image)
// If galleryStore.selectedArea changes from private to public, re-run getImagesFromWNFS
// If galleryStore.selectedArea changes from private to public, re-run getJSONFromWNFS
let selectedArea = null
const unsubscribeGalleryStore = galleryStore.subscribe(async updatedStore => {
// Get initial selectedArea
@ -36,7 +36,7 @@
if (newState.authed && $filesystemStore && !imagesFetched) {
imagesFetched = true
// Get images from the user's public WNFS
getImagesFromWNFS()
getImagesFrgetJSONFromWNFSomWNFS()
}
})

View File

@ -1,7 +1,7 @@
<script lang="ts">
import {
getImagesFromWNFS,
uploadImageToWNFS
getJSONFromWNFS,
uploadJSONToWNFS
} from '$routes/gallery/lib/gallery'
import { addNotification } from '$lib/notifications'
@ -33,14 +33,14 @@
addNotification('Please upload images only', 'error')
console.error('Please upload images only')
} else {
await uploadImageToWNFS(file)
await uploadJSONToWNFS(file)
}
}
})
)
// Refetch images and update galleryStore
await getImagesFromWNFS()
await getJSONFromWNFS()
// Disable isDragging state
isDragging = false

View File

@ -47,7 +47,7 @@ const FILE_SIZE_LIMIT = 5
/**
* Get images from the user's WNFS and construct the `src` value for the images
*/
export const getImagesFromWNFS: () => Promise<void> = async () => {
export const getJSONFromWNFS: () => Promise<void> = async () => {
try {
// Set loading: true on the galleryStore
galleryStore.update(store => ({ ...store, loading: true }))
@ -182,7 +182,7 @@ export const deleteImageFromWNFS: (
addNotification(`${name} image has been deleted`, 'success')
// Refetch images and update galleryStore
await getImagesFromWNFS()
await getJSONFromWNFS()
} else {
throw new Error(`${name} image has already been deleted`)
}
@ -205,5 +205,5 @@ export const handleFileInput: (
)
// Refetch images and update galleryStore
await getImagesFromWNFS()
await getJSONFromWNFS()
}

View File

@ -5,31 +5,75 @@
import TerminusClient from "@terminusdb/terminusdb-client"
import { filesystemStore, sessionStore } from '$src/stores'
import { AREAS, galleryStore } from '$routes/gallery/stores'
import { getImagesFromWNFS, type Image } from '$routes/gallery/lib/gallery'
import { getJSONFromWNFS, uploadJSONToWNFS, deleteAllJSONFromWNFS, type Image } from '$routes/gallery/lib/gallery'
import FileUploadCard from '$routes/gallery/components/upload/FileUploadCard.svelte'
import ImageCard from '$routes/gallery/components/imageGallery/ImageCard.svelte'
import ImageModal from '$routes/gallery/components/imageGallery/ImageModal.svelte'
import { ipfsGatewayUrl } from '$lib/app-info';
import { addNotification } from '$lib/notifications'
import { MeiliSearch } from 'meilisearch'
function extractPersonId(url: string): string {
const personIdPattern = /person\/[a-zA-Z0-9]+/;
const match = url.match(personIdPattern);
return match ? match[0] : null;
}
interface Document {
[key: string]: any;
}
type KeyValuePair = [string, unknown];
const client = new TerminusClient.WOQLClient(
"https://cloud.terminusdb.com/Myseelia",{
user:"zaldarren@gmail.com",
organization:"Myseelia",
db: "Myseelia",
db: "murmurations",
token: "dGVybWludXNkYjovLy9kYXRhL2tleXNfYXBpLzg5OTY0ZGI5OWFlYjQ1Zjc5OGM5ZTRiZWI2MzExOGJhZjhiOWRiOWNlOTJiNmU2NGI0NDEzZjIzNDFmOGVkMjc=_869e9bd2465ad84126151962994fcfa22d4b7ec9375edf16b4182e7f36e4b2b820075ba22e78f629e0691eddbeae6998a6504d5ce287aa1df2602cb556b58e1730b0b93feb0e9304"
}
);
let username = $sessionStore.username;
let bioregion = '';
let ecozone = '';
let affiliatedOrganizations = "Organization/8c8368b55dc80f18ba254771701f6d1bc79a3a90f127c28b3145a2c2204e97ce";
let givenName = '';
let Name = '';
let hasCredential = {};
let cid
let primary_url, profileImage, Description
onMount(async () => {
await client.connect()
const schema = await client.getSchema("myseelia", "main")
// const schema = await client.getSchema("myseelia", "main")
const queryTemplate = { "name": username }
const tdbresult = await client.getDocument({"type":"person","as_list":true,"query":queryTemplate});
console.log("Query Documents",tdbresult)
if (tdbresult && tdbresult.length > 0){
console.log("found documents")
const doc = tdbresult[0]
bioregion = doc.locality
Name = doc.name
primary_url = doc.primary_url
profileImage = doc.image
Description = doc.description
} else {
console.log("no documents found")
}
try {
const jsonObjects = await getJSONFromWNFS()
console.log("jsonObjects", jsonObjects)
// Find the JSON object with the username "tester"
const usernameJSONObject = jsonObjects.find(obj => obj.name === 'tester')
// Log the CID of the "username.json" file to the console
cid = usernameJSONObject ? usernameJSONObject.cid : null
console.log("found it", cid)
} catch (error) {
console.error("no files fond on WNFS")
}
});
/**
@ -42,7 +86,7 @@
const clearSelectedImage = () => (selectedImage = null)
// If galleryStore.selectedArea changes from private to public, re-run getImagesFromWNFS
// If galleryStore.selectedArea changes from private to public, re-run getJSONFromWNFS
let selectedArea = null
const unsubscribeGalleryStore = galleryStore.subscribe(async updatedStore => {
// Get initial selectedArea
@ -52,7 +96,7 @@
if (selectedArea !== updatedStore.selectedArea) {
selectedArea = updatedStore.selectedArea
await getImagesFromWNFS()
await getJSONFromWNFS()
}
})
@ -62,7 +106,7 @@
if (newState.authed && $filesystemStore && !imagesFetched) {
imagesFetched = true
// Get images from the user's public WNFS
getImagesFromWNFS()
getJSONFromWNFS()
}
})
@ -79,54 +123,146 @@
export async function makeConnection(){
try{
const entryObj =
{
"@type" : "Person",
"userName" : username,
"givenName" : givenName,
"bioregion": bioregion,
"ecozone": ecozone,
"hasCredential": hasCredential,
"affiliation": affiliatedOrganizations
let entryObj: {
"@type": string;
name: string;
locality?: string;
image?: string;
primary_url?: string;
description?: string;
} = {
"@type": "person",
name: username,
...(bioregion && { locality: String(bioregion) }),
...(profileImage && { image: String(profileImage) }),
...(primary_url && { primary_url: String(primary_url) }),
...(Description && { description: String(Description) })
};
const murmurationSchema = {
linked_schemas: ['person_schema-v0.1.0'],
name: username,
primary_url: primary_url,
description: Description,
image: profileImage,
locality: bioregion,
};
if (username == entryObj.userName){
await client.updateDocument(entryObj)
console.log(JSON.stringify(murmurationSchema));
//upload to IPFS
await uploadJSONToWNFS(murmurationSchema)
try {
const jsonObjects = await getJSONFromWNFS()
console.log(jsonObjects)
// Find the JSON object with the username "tester"
const usernameJSONObject = jsonObjects.find(obj => obj.name === 'tester')
// Log the CID of the "username.json" file to the console
cid = usernameJSONObject ? usernameJSONObject.cid : null
console.log("found it", cid)
const url = "https://test-index.murmurations.network/v2/nodes";
const data = {
profile_url: `https://ipfs.${ipfsGatewayUrl}/ipfs/${cid}/userland`,
};
console.log(data.profile_url)
try {
const response = await fetch(url, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(data),
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
} else {
addNotification(`profile has been indexed on murmurations`, 'success')
}
const result = await response.json();
console.log("result from murmurations indexing: " + JSON.stringify(result));
} catch (error) {
console.error("Error posting data:", error);
}
} catch (error) {
console.error(error)
}
const queryTemplate = { "name": username }
const tdbresult = await client.getDocument({"type":"person","as_list":true,"query":queryTemplate});
console.log("Query Documents",tdbresult)
const searchclient = new MeiliSearch({
host: 'https://ms-9ea4a96f02a8-1969.sfo.meilisearch.io',
apiKey: '117c691a34b21a6651798479ebffd181eb276958'
})
const index = searchclient.index('people')
if (tdbresult && tdbresult.length > 0){
await client.deleteDocument({id:tdbresult[0]["@id"]});
// const searchResult = await index.search(`id="${tdbresult[0]["@id"]}"`);
// // Return the document if found, otherwise return null
// return searchResult.hits.length > 0 ? searchResult.hits[0] : null;
//update document
console.log("updating")
const result = await client.addDocument(entryObj);
addNotification(`data has been added to knowledge graph`, 'success')
console.log("added document", result)
const addedID = extractPersonId(String(result))
const queryTemplate = { "@id": addedID }
const terminusperson = await client.getDocument({ type: 'person', as_list: true, query: queryTemplate });
console.log("result ", terminusperson);
const real_id: string = terminusperson[0]['@id'];
const num_id: string = real_id.split('/').pop() || '';
// Create a new object without the '@id' key
const newDocument: Document = Object.entries(terminusperson[0])
.filter(([key]) => key !== '@id')
.reduce<Document>((acc, [key, value]: KeyValuePair) => {
acc[key] = value;
return acc;
}, {});
// Update the document with the new 'id'
newDocument['id'] = num_id;
const newDocumentsArray = [newDocument];
const searchResult = await index.addDocuments(newDocumentsArray);
console.log("searchResult", searchResult)
} else{
await client.addDocument(entryObj);
console.log('adding doc')
const addedPerson = await client.addDocument(entryObj);
console.log("added document", addedPerson)
addNotification(`profile has been added to knowledge graph`, 'success')
}
const entries2 = await client.getDocument({"graph_type":"instance","as_list":true,"type":"Person"})
const entries2 = await client.getDocument({"graph_type":"instance","as_list":true,"type":"person"})
console.log(entries2);
}catch(err){
console.error(err.message)
}
}
const onDelete = async (e) => {
e.preventDefault();
deleteAllJSONFromWNFS();
try {
const jsonObjects = await getJSONFromWNFS()
// Find the JSON object with the username "tester"
const usernameJSONObject = jsonObjects.find(obj => obj.userName === 'tester')
// Log the CID of the "username.json" file to the console
cid = usernameJSONObject ? usernameJSONObject.cid : null
} catch (error) {
console.error(error)
}
}
</script>
<style>
form {
display: grid;
grid-template-columns: 1fr 3fr;
}
label {
text-align: left;
}
input{
background-color: rgb(255, 255, 255);
border-radius: 4px;
}
button{
background-color: #4CAF50; /* Green */
padding: 12px 20px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 16px;
}
</style>
<section class="overflow-hidden text-gray-700">
<div class="pt-8 p-6 md:p-8 mx-auto">
<div
@ -136,31 +272,87 @@ button{
</div>
<form on:submit|preventDefault={handleSubmit}>
<label class="label dark:text-white">
Given Name:
<input class="input text-white dark:text-black" type="text" bind:value={givenName} />
Name:
<input
class="input text-white dark:text-black"
type="text"
bind:value={username}
readonly
/>
</label>
<br />
<label class="label dark:text-white">
Bioregion:
<input class="input text-white dark:text-black" type="text" bind:value={bioregion} />
Image:
<input
class="input text-white dark:text-black"
type="text"
bind:value={profileImage}
/>
</label>
<br />
<label class="label dark:text-white">
Bioregion / Locality:
<input
class="input text-white dark:text-black"
type="text"
bind:value={bioregion}
/>
</label>
<br />
<label class="label dark:text-white">
Ecozone:
<input class="input text-white dark:text-black" type="text" bind:value={ecozone} />
</label >
<input
class="input text-white dark:text-black"
type="text"
bind:value={ecozone}
/>
</label>
<!-- <br />
<label class="label dark:text-white">
Affiliated organizations:
<input
class="input text-white dark:text-black"
type="text"
bind:value={affiliatedOrganizations}
/>
</label> -->
<br />
<label class="label dark:text-white">
Has Credential:
<input class="input text-white dark:text-black" type="text" bind:value={hasCredential} />
</label >
<br />
<label class="label dark:text-white">
Affiliated organizations:
<input class="input text-white dark:text-black" type="text" bind:value={affiliatedOrganizations}/>
Primary URL:
<input
class="input text-white dark:text-black"
type="text"
bind:value={primary_url}
/>
</label>
<br />
<button class="bg-blue-500 text-white dark:text-black" type="submit">Submit</button>
<label class="label dark:text-white">
Description:
<input
class="input text-white dark:text-black"
type="text"
bind:value={Description}
/>
</label>
<br />
<button class="bg-blue-500 text-white dark:text-black" type="submit">
Submit
</button>
<br />
{#if cid !== null && cid !== undefined}
<div class="my-2">
<a
href={`https://ipfs.${ipfsGatewayUrl}/ipfs/${cid}/userland`}
target="_blank"
class="underline mb-4 hover:text-slate-500 dark:text-white"
title="Note, the data will be cached for a while on IPFS after deleting, Fission is not 'tombstoning' on their gateway and there is no 'hard delete' on IPFS."
>
View on IPFS
</a>
</div>
<br />
<button type="button" class="delete" on:click={onDelete}>Delete</button>
{/if}
</form>
</div>
@ -172,3 +364,37 @@ button{
/>
{/if}
</section>
<style>
form {
display: grid;
grid-template-columns: 1fr 3fr;
}
label {
text-align: left;
}
input {
background-color: rgb(255, 255, 255);
border-radius: 4px;
color: rgb(0, 0, 0);
}
button {
background-color: #4caf50; /* Green */
padding: 12px 20px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 16px;
}
.delete {
background-color: red;
padding: 12px 20px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 16px;
}
</style>

View File

@ -4,7 +4,7 @@
import { filesystemStore, sessionStore } from '$src/stores'
import { AREAS, galleryStore } from '$routes/gallery/stores'
import { getImagesFromWNFS, type Image } from '$routes/gallery/lib/gallery'
import { getJSONFromWNFS, type Image } from '$routes/gallery/lib/gallery'
import FileUploadCard from '$routes/gallery/components/upload/FileUploadCard.svelte'
import ImageCard from '$routes/gallery/components/imageGallery/ImageCard.svelte'
import ImageModal from '$routes/gallery/components/imageGallery/ImageModal.svelte'
@ -17,7 +17,7 @@
const setSelectedImage: (image: Image) => void = image =>
(selectedImage = image)
// If galleryStore.selectedArea changes from private to public, re-run getImagesFromWNFS
// If galleryStore.selectedArea changes from private to public, re-run getImaggetJSONFromWNFSesFromWNFS
let selectedArea = null
const unsubscribeGalleryStore = galleryStore.subscribe(async updatedStore => {
// Get initial selectedArea
@ -36,7 +36,7 @@
if (newState.authed && $filesystemStore && !imagesFetched) {
imagesFetched = true
// Get images from the user's public WNFS
getImagesFromWNFS()
getJSONFromWNFS()
}
})

View File

@ -1,7 +1,7 @@
<script lang="ts">
import {
getImagesFromWNFS,
uploadImageToWNFS
getJSONFromWNFS,
uploadJSONToWNFS
} from '$routes/gallery/lib/gallery'
import { addNotification } from '$lib/notifications'
@ -33,14 +33,14 @@
addNotification('Please upload images only', 'error')
console.error('Please upload images only')
} else {
await uploadImageToWNFS(file)
await uploadJSONToWNFS(file)
}
}
})
)
// Refetch images and update galleryStore
await getImagesFromWNFS()
await getJSONFromWNFS()
// Disable isDragging state
isDragging = false

View File

@ -1,14 +1,21 @@
import { get as getStore } from 'svelte/store'
import * as wn from 'webnative'
import * as uint8arrays from 'uint8arrays'
import type { CID } from 'multiformats/cid'
import { CID } from 'multiformats/cid'
import type { PuttableUnixTree, File as WNFile } from 'webnative/fs/types'
import type { Metadata } from 'webnative/fs/metadata'
import { filesystemStore } from '$src/stores'
import { filesystemStore, sessionStore } from '$src/stores'
import { AREAS, galleryStore } from '$routes/gallery/stores'
import { addNotification } from '$lib/notifications'
let username: string | null = null
// Subscribe to changes in the session store
sessionStore.subscribe(session => {
username = session.username
})
export type Image = {
cid: string
ctime: number
@ -45,112 +52,105 @@ export const GALLERY_DIRS = {
const FILE_SIZE_LIMIT = 5
/**
* Get images from the user's WNFS and construct the `src` value for the images
* Get JSON objects from the user's WNFS and return them as an array
*/
export const getImagesFromWNFS: () => Promise<void> = async () => {
export const getJSONFromWNFS: () => Promise<Array<Record<string, any>>> = async () => {
try {
// Set loading: true on the galleryStore
galleryStore.update(store => ({ ...store, loading: true }))
const { selectedArea } = getStore(galleryStore)
const isPrivate = selectedArea === AREAS.PRIVATE
const fs = getStore(filesystemStore)
// Set path to either private or public gallery dir
// Set path to gallery dir
const path = wn.path.directory(...GALLERY_DIRS[selectedArea])
// Get list of links for files in the gallery dir
const links = await fs.ls(path)
console.log('links', links)
let cid
Object.keys(links).forEach(name => {
const link = links[name]
console.log(link.cid.toString())
if (link.toString() == 'tester.json') {
cid = link.cid.toString()
}
})
const images = await Promise.all(
Object.entries(links).map(async ([name]) => {
const jsonObjects = await Promise.all(
Object.entries(links).map(async ([name, link]) => {
const file = await fs.get(
wn.path.file(...GALLERY_DIRS[selectedArea], `${name}`)
)
// The CID for private files is currently located in `file.header.content`,
// whereas the CID for public files is located in `file.cid`
const cid = isPrivate
? (file as GalleryFile).header.content.toString()
: (file as GalleryFile).cid.toString()
// The content of the file is a JSON-formatted string
const jsonString = uint8arrays.toString((file as GalleryFile).content, 'utf8')
// Create a base64 string to use as the image `src`
const src = `data:image/jpeg;base64, ${uint8arrays.toString(
(file as GalleryFile).content,
'base64'
)}`
// Parse the JSON-formatted string into a JavaScript object
const jsonObject = JSON.parse(jsonString)
return {
cid,
ctime: (file as GalleryFile).header.metadata.unixMeta.ctime,
name,
private: isPrivate,
size: (links[name] as Link).size,
src
}
// Get the CID for the current link and append it to the corresponding JSON object
const cid = link.cid.toString()
jsonObject.cid = cid
console.log(jsonObject)
return jsonObject
})
)
// Sort images by ctime(created at date)
// Sort JSON objects by ctime(created at date)
// NOTE: this will eventually be controlled via the UI
images.sort((a, b) => b.ctime - a.ctime)
jsonObjects.sort((a, b) => b.ctime - a.ctime)
// Push images to the galleryStore
galleryStore.update(store => ({
...store,
...(isPrivate
? {
privateImages: images
}
: {
publicImages: images
}),
loading: false
}))
// Set loading: false and return the JSON objects
galleryStore.update(store => ({ ...store, loading: false }))
return jsonObjects
} catch (error) {
console.error(error)
galleryStore.update(store => ({
...store,
loading: false
}))
galleryStore.update(store => ({ ...store, loading: false }))
return []
}
}
/**
* Upload an image to the user's private or public WNFS
* @param image
* Upload a JSON object to the user's private or public WNFS
* @param json
*/
export const uploadImageToWNFS: (
image: File
) => Promise<void> = async image => {
export const uploadJSONToWNFS: (
json: Record<string, any>
) => Promise<void> = async json => {
try {
const { selectedArea } = getStore(galleryStore)
const fs = getStore(filesystemStore)
// Reject files over 5MB
const imageSizeInMB = image.size / (1024 * 1024)
if (imageSizeInMB > FILE_SIZE_LIMIT) {
throw new Error('Image can be no larger than 5MB')
// Convert the JSON object to a string
const jsonString = JSON.stringify(json)
// Reject strings over 5MB
const stringSizeInMB = new globalThis.Blob([jsonString]).size / (1024 * 1024)
if (stringSizeInMB > FILE_SIZE_LIMIT) {
throw new Error('JSON object can be no larger than 5MB')
}
// Reject the upload if the image already exists in the directory
const imageExists = await fs.exists(
wn.path.file(...GALLERY_DIRS[selectedArea], image.name)
)
if (imageExists) {
throw new Error(`${image.name} image already exists`)
}
// Reject the upload if the file already exists in the directory
// const fileExists = await fs.exists(
// wn.path.file(...GALLERY_DIRS[selectedArea], `${username}.json`)
// )
// if (fileExists) {
// throw new Error(`${username}.json file already exists`)
// }
// Create a sub directory and add some content
// Create a sub directory and add the JSON string as a file
await fs.write(
wn.path.file(...GALLERY_DIRS[selectedArea], image.name),
image
wn.path.file(...GALLERY_DIRS[selectedArea], `${username}.json`),
new globalThis.Blob([jsonString], { type: 'application/json' })
)
// Announce the changes to the server
await fs.publish()
addNotification(`${image.name} image has been published`, 'success')
addNotification(`${username}.json file has been published`, 'success')
} catch (error) {
addNotification(error.message, 'error')
console.error(error)
@ -182,7 +182,7 @@ export const deleteImageFromWNFS: (
addNotification(`${name} image has been deleted`, 'success')
// Refetch images and update galleryStore
await getImagesFromWNFS()
await getJSONFromWNFS()
} else {
throw new Error(`${name} image has already been deleted`)
}
@ -192,6 +192,42 @@ export const deleteImageFromWNFS: (
}
}
/**
* Delete all JSON files from the user's private or public WNFS
*/
export const deleteAllJSONFromWNFS: () => Promise<void> = async () => {
try {
const { selectedArea } = getStore(galleryStore)
const fs = getStore(filesystemStore)
// Retrieve all JSON files in the selected directory
const fileList = await fs.ls(wn.path.directory(...GALLERY_DIRS[selectedArea]))
// Filter JSON files
const jsonFiles = Object.keys(fileList).filter(key => key.endsWith('.json'))
if (jsonFiles.length > 0) {
// Remove JSON files from the server
for (const fileName of jsonFiles) {
console.log(fileName)
await fs.rm(wn.path.file(...GALLERY_DIRS[selectedArea], fileName))
}
// Announce the changes to the server
await fs.publish()
addNotification(`Data has been deleted from IPFS`, 'success')
// Refetch JSON files and update galleryStore
await getJSONFromWNFS()
} else {
throw new Error(`No JSON files found to delete`)
}
} catch (error) {
addNotification(error.message, 'error')
console.error(error)
}
}
/**
* Handle uploads made by interacting with the file input directly
*/
@ -200,10 +236,10 @@ export const handleFileInput: (
) => Promise<void> = async files => {
await Promise.all(
Array.from(files).map(async file => {
await uploadImageToWNFS(file)
await uploadJSONToWNFS(file)
})
)
// Refetch images and update galleryStore
await getImagesFromWNFS()
await getJSONFromWNFS()
}