Skip to content

Commit

Permalink
feat: write only latest entity change, misc (#734)
Browse files Browse the repository at this point in the history
* feat: write only latest entity change, misc

* feat: fetch entity details from QueryApi

* feat: handle delete in indexer

* chore: indexer formatting

* feat: entity deletion
  • Loading branch information
gabehamilton authored Apr 3, 2024
1 parent a6f51dd commit 521b0d2
Show file tree
Hide file tree
Showing 9 changed files with 212 additions and 75 deletions.
2 changes: 1 addition & 1 deletion indexers/components/components.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { Block } from "@near-lake/primitives";
* @param {block} Block - A Near Protocol Block
*/
async function getBlock(block: Block) {
const ACCOUNT_NAME = "dataplaform_near";
const ACCOUNT_NAME = "dataplatform_near";
const INDEXER_NAME = `${ACCOUNT_NAME}_components`;
const METADATA_TABLE = `${INDEXER_NAME}_metadata`;
const VERSION_TABLE = `${INDEXER_NAME}_versions`;
Expand Down
125 changes: 78 additions & 47 deletions indexers/entities/entities.js
Original file line number Diff line number Diff line change
Expand Up @@ -85,34 +85,60 @@ async function getBlock(block: Block) {
const entityWrites = getSocialOperations(block, "entities");
await entityWrites.map(async ({ accountId, data }) => {
try {
const namespaces = Object.keys(data);
namespaces.map(async (namespace) => {
const namespaceData = data[namespace];
const entityTypes = Object.keys(namespaceData);
entityTypes.map(async (entityType) => {
const entityTypeData = namespaceData[entityType];
const entities = Object.keys(entityTypeData);
entities.map(async (name) => {
const entityProps = entityTypeData[name];
const { displayName, logoUrl, ...entityAttributes } = entityProps;
const entity = {
namespace: namespace,
entity_type: entityType,
account_id: accountId,
name: name,
display_name: displayName,
logo_url: logoUrl,
attributes: entityAttributes,
};
await context.db.Entities.upsert(
entity,
["entity_type", "account_id", "name"],
["display_name", "logo_url", "attributes"]
);
if (typeof data === "string") {
data = JSON.parse(data);
}
const dataArray = Array.isArray(data) ? data : [data];
dataArray.map(async (data) => {
const namespaces = Object.keys(data);
namespaces.map(async (namespace) => {
const namespaceData = data[namespace];
const entityTypes = Object.keys(namespaceData);
entityTypes.map(async (entityType) => {
const entityTypeData = namespaceData[entityType];
const entities = Object.keys(entityTypeData);
entities.map(async (name) => {
const entityProps = entityTypeData[name];
if (entityProps["operation"]) {
switch (entityProps["operation"]) {
case "delete":
await context.db.Entities.delete({
namespace: namespace,
entity_type: entityType,
account_id: accountId,
name: name,
});
console.log(
`${entityType} ${namespace}/${name} from ${accountId} has been deleted from the database`
);
return;
default:
console.error(
`Operation ${entityProps["operation"]} not supported`
);
return;
}
}
const { displayName, logoUrl, ...entityAttributes } = entityProps;
const entity = {
namespace: namespace,
entity_type: entityType,
account_id: accountId,
name: name,
display_name: displayName,
logo_url: logoUrl,
attributes: entityAttributes,
};
await context.db.Entities.upsert(
entity,
["entity_type", "account_id", "name"],
["display_name", "logo_url", "attributes"]
);

console.log(
`${entityType} from ${accountId} has been added to the database`
);
console.log(
`${entityType} ${namespace}/${name} from ${accountId} has been added to the database`
);
});
});
});
});
Expand All @@ -132,24 +158,30 @@ async function getBlock(block: Block) {
})
.map(async ({ accountId, data }) => {
try {
const type = "star";
const starData = data[type];
if (!starData) {
console.log("No star data found");
return;
}
const star = JSON.parse(starData);
// "{\"key\":{\"type\":\"social\",\"path\":\"flatirons.near/examples/favoriteCar/delorean\"},\"value\":{\"type\":\"star\"}}"
const starArray = typeof star === "object" ? [star] : star;
starArray.map(async ({ key, value }) => {
if (!key || !value || !key.path || !value.type) {
console.log("Required fields not found for star", key, value);
const type = "star";
const starData = data[type];
if (!starData) {
console.log("No star data found");
return;
}
const { path } = key;
const { type } = value;
const [targetAccountId, entitiesConstant, namespace, entityType, name] = path.split("/");
const incDecQuery = `
const star = JSON.parse(starData);
// "{\"key\":{\"type\":\"social\",\"path\":\"flatirons.near/examples/favoriteCar/delorean\"},\"value\":{\"type\":\"star\"}}"
const starArray = typeof star === "object" ? [star] : star;
starArray.map(async ({ key, value }) => {
if (!key || !value || !key.path || !value.type) {
console.log("Required fields not found for star", key, value);
return;
}
const { path } = key;
const { type } = value;
const [
targetAccountId,
entitiesConstant,
namespace,
entityType,
name,
] = path.split("/");
const incDecQuery = `
mutation ChangeStars {
update_dataplatform_near_entities_entities(
where: {namespace: {_eq: "${namespace}"}, entity_type: {_eq: "${entityType}"},
Expand All @@ -161,15 +193,14 @@ async function getBlock(block: Block) {
}
}
`;
console.log("mutation is", incDecQuery);
await context.graphql(incDecQuery, {});
});
console.log("mutation is", incDecQuery);
await context.graphql(incDecQuery, {});
});
} catch (e) {
console.log(
`Failed to process star of entity from ${accountId} to the database`,
e
);
}

});
}
3 changes: 2 additions & 1 deletion src/Entities/Examples/FavoriteCars.jsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
// This is an example of how to use the Entity components
// It uses a custom schema to specify additional fields
return (
<div className="gateway-page-container">
<p>A custom message</p>
Expand All @@ -6,7 +8,6 @@ return (
props={{
namespace: "examples",
entityType: "favoriteCar",
title: "Favorite Car",
schemaFile: "${REPL_ACCOUNT}/widget/Entities.Examples.FavoriteCarSchema",
//todo defaultImage: "some car photo",
}}
Expand Down
4 changes: 3 additions & 1 deletion src/Entities/Examples/FavoritePlaces.jsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
// This is an example of how to use the Entity components
// It is the simplest example, using the GenericEntityConfig widget and no additional fields
return (
<div className="gateway-page-container">
<Widget
src="${REPL_ACCOUNT}/widget/Entities.Template.GenericEntityConfig"
props={{ namespace: "examples", entityType: "favoritePlace", title: "Favorite Place" }}
props={{ namespace: "examples", entityType: "favoritePlace" }}
/>
</div>
);
24 changes: 24 additions & 0 deletions src/Entities/QueryApi/Client.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,24 @@ function loadItems(queries, queryName, offset, collection, onLoad) {
});
}

function loadItem(queries, queryName, collection, onLoad) {
return fetchGraphQL(queries, queryName, {}).then((result) => {
if (result.status === 200 && result.body) {
if (result.body.errors) {
console.log("error:", result.body.errors);
return;
}
let data = result.body.data;
if (data) {
const newItem = data[collection];
if (newItem) {
onLoad(newItem);
}
}
}
});
}

function convertObjectKeysSnakeToPascal(item) {
const newItem = {};
Object.keys(item).forEach((key) => {
Expand All @@ -52,12 +70,18 @@ function convertObjectKeysPascalToSnake(item) {
});
return newItem;
}
const capitalize = (s) => {
if (typeof s !== "string") return "";
return s.charAt(0).toUpperCase() + s.slice(1);
};

return {
fetchGraphQL,
loadItems,
loadItem,
convertObjectKeysSnakeToPascal,
convertObjectKeysPascalToSnake,
capitalize,
LIMIT,
GRAPHQL_ENDPOINT,
HASURA_ROLE,
Expand Down
2 changes: 1 addition & 1 deletion src/Entities/Template/EntityCreate.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ const onSubmitDefault = (formValues) => {
const entity = { [name]: rest };
const ns = namespace ? namespace : "default";
const data = { [ns]: { [entityType]: entity } };
Social.set({ entities: data }, { force: true });
Social.set({ entities: JSON.stringify(data) }, { force: true });
};
const onSubmitFunction = onSubmit ?? onSubmitDefault;

Expand Down
92 changes: 71 additions & 21 deletions src/Entities/Template/EntityDetails.jsx
Original file line number Diff line number Diff line change
@@ -1,31 +1,75 @@
const { href } = VM.require("${REPL_DEVHUB}/widget/core.lib.url");
if (!href) {
return <></>;
return <p>Loading modules...</p>;
}
let { src, tab, highlightComment, schemaFile, namespace } = props;
const { loadItem, convertObjectKeysSnakeToPascal, capitalize } = VM.require(
"${REPL_ACCOUNT}/widget/Entities.QueryApi.Client",
);
if (!loadItem) {
return <p>Loading modules...</p>;
}
let { src, tab, schemaFile, namespace } = props;
const [accountId, entityType, entityName] = src.split("/") ?? [null, null, null];

let entity = Social.get(`${accountId}/entities/${namespace}/${entityType}/${entityName}/**`);
const exists = !existsData || Object.keys(existsData).length > 0;
if (!exists) {
const schemaLocation = schemaFile ? schemaFile : `${REPL_ACCOUNT}/widget/Entities.Template.GenericSchema`;
const { genSchema } = VM.require(schemaLocation, { namespace, entityType });
if (!genSchema) {
return <>Loading schema...</>;
}
const schema = genSchema(namespace, entityType);

const entityIndexer = "entities";
const entityTable = "entities";
const user = "dataplatform_near";
const collection = `${user}_${entityIndexer}_${entityTable}`;

const query = `
query SingleEntity {
${collection}(
where: { account_id: {_eq: "${accountId}"}, name: {_eq: "${entityName}"},
entity_type: {_eq: "${entityType}"}, namespace: {_eq: "${namespace}"}}
) {
entity_type
namespace
id
account_id
name
display_name
logo_url
attributes
stars
created_at
updated_at
}
}
`;

const [entity, setEntity] = useState(null);
const [error, setError] = useState(null);
const onLoad = (itemInArray) => {
if (itemInArray.length === 0 || !itemInArray[0]) {
setError(`${entityType} with name ${name} not found`);
return;
}
const fetchedItem = itemInArray[0];
const fullEntity = convertObjectKeysSnakeToPascal(fetchedItem);
setEntity(fullEntity);
};
loadItem(query, "SingleEntity", collection, onLoad);

if (error) {
return (
<div className="alert alert-danger mx-3" role="alert">
<div>Error</div>
<div>Could not find: {src}</div>
<Link to={href({ widgetSrc: `${REPL_ACCOUNT}/widget/Nexus` })}>Back to Nexus</Link>
</div>
);
}

const schemaLocation = schemaFile ? schemaFile : `${REPL_ACCOUNT}/widget/Entities.Template.GenericSchema`;
const { genSchema } = VM.require(schemaLocation, { namespace, entityType });
if (!genSchema) {
return <></>;
if (!entity) {
return <p>Loading...</p>;
}
const schema = genSchema(namespace, entityType);
const { title } = schema;

entity = { accountId, namespace: namespace, entityType: entityType, name: entityName, ...entity };
const { prompt } = entity;
const editType = accountId === context.accountId ? "edit" : "fork";
const editLabel = editType === "edit" ? "Edit" : "Fork";
const editIcon = editType === "edit" ? "ph-bold ph-pencil-simple" : "ph-bold ph-git-fork";
Expand Down Expand Up @@ -56,6 +100,12 @@ const Header = styled.h1`
font-weight: 600;
`;

const Properties = styled.div`
margin: 2em;
display: grid;
grid-template-columns: 5em 1fr;
gap: 16px;
`;
const Text = styled.p`
margin: 0;
font-size: 14px;
Expand All @@ -75,18 +125,18 @@ const PropValue = styled.p`
padding-bottom: 10px;
`;
const entityProperties = (obj) => {
const { accountId, name, displayName, logoUrl, namespace, entityType, ...attributes } = obj;
const attributes = obj.attributes;
return (
<>
<Properties>
{Object.keys(attributes).map((k) => (
<>
<Text bold key={k}>
{k}:
{capitalize(k)}:
</Text>
<PropValue>{obj[k]}</PropValue>
<PropValue>{attributes[k]}</PropValue>
</>
))}
</>
</Properties>
);
};

Expand All @@ -107,11 +157,11 @@ return (
props={{
variant: "line",
size: "large",
defaultValue: "prompt",
defaultValue: "properties",
items: [
{
name: "Properties",
value: "prompt",
value: "properties",
content: entityProperties(entity),
icon: "ph ph-code",
},
Expand Down
Loading

0 comments on commit 521b0d2

Please sign in to comment.