Skip to content
Closed
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
270 changes: 270 additions & 0 deletions docs/schema-v2.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,270 @@
## Glossary

- **Space**: A place for grouping information.
- **Private Space**: Information of a space that is not publicly accessible, but only accessible to members of a space.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I additionally segregate between Private spaces which are just private to a single individual and Shared spaces which are private spaces that are shared and synchronized.

- **Public Space**: Information of a space that is publicly accessible.
- **Personal Space**: A space controlled by a single person or organization.

## Relationship between Schema and Code

The general idea is that the type definitions (schema) are also data.

When building an app though you need to define the types in code. Therefor these are requirements we need to define:

- Local schema as defined in the code allows the dev to full customize names & structure of the schema.
- Maximum interoperability with the Graph
- Every type and attribute has a corresponding entity that has been published to the public schema. When you define a schema in the code, it doesn't mean it exists in a public schema.

## Spaces and Ownership

While traditional apps are silos with the Graph, the users and organizations should be the owners of their data.

When using an app you still should publish to your personal or public spaces that you control.
There can be app specific spaces, but the expected use-case is that these are use to define types in the graph.

For the public schemas there is a global index and we need to specify to an indexer what spaces to index for a specific app. It's not yet clear or well defined how this works.

## Schema Design

## Schema Design

- Relations are defined directly on the entity type (see an alternative Design below)
- We want immediate feedback on invalid relations.
- Handling invalid Relations
- In case the from or to is missing we ignore the relation completely.
- In case the index is missing, we set an index at the end of the list. Later we can provide a callback to choose different behavior. Note: he data service should be validating this already, but can happen in case of end-to-end encrypted sync.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would remove the part from this and the next item that changes the state of the data.. I would rather have bad behavior than rewriting of state.

Copy link
Collaborator Author

@nikgraf nikgraf Dec 18, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How should the bad behaviour behave in this case? e.g. for React it would make sense to have at least predictable bad behaviour. Thinking about this could otherwise lead to endless loops and suddenly one piece of bad data can crash other peoples apps.

- In case the index is not unique we pick one of them and move it between this and the next item.

### Design A

An object based schema definition where relations must be defined on the entity type. This design assumes that the local schema doesn't deal with entities having multiple types. This fact is only dealt with in the mappings file and therefor the local schema stays a lot simpler.

```ts
export const schema = {
User: {
name: type.Text,
email: type.Text,
ownedEvents: type.Relation({
types: 'Event',
}),
},
Event: {
name: type.Text,
owners: type.Relation({
types: 'User',
}),
},
};
```

Relations can be named to allow for multiple relations to the same type.

```ts
export const schema = {
User: {
name: type.Text,
email: type.Text,
ownedEvents: type.Relation({
map: 'user_owned_events',
types: 'Event',
}),
participatingEvents: type.Relation({
map: 'event_participants',
types: 'Event',
}),
},
Event: {
name: type.Text,
owners: type.Relation({
map: 'user_owned_events',
types: 'User',
}),
participants: type.Relation({
map: 'event_participants',
types: 'User',
}),
},
};
```

## Mapping

The mapping should be simple and without unnecessary nesting.

```ts
type AttributeId = string;

type TypeMapping = {
id: string; // Public type ID
spaceId: string; // Public space ID (optional)
attributes: {
[localAttributeName: string]: AttributeId;
}
}

type Mappings = [localTypeName: string]: TypeMapping
```

Example:
```ts
const mappings = {
User: { // matches the local type name
id: 'xyz', // matches the public type ID
spaceId: 'abc', // matches the public space ID
attributes: {
name: 'gfd',
email: 'asd',
}
},
Event: {
typeID: 'wzx',
attributes: {
name: 'asd',
},
},
};
```

This allows for simpler reasoning, but leaves room for edge-cases that lead to impossible types e.g. when creating an entity with attributes that don't match. Such edge-cases should by not allowing them and they should result in an Error.

## Syncing Types (Local <-> Public)

Use cases:

1. Use an existing schema (use types from any different spaces)
2. Use an existing schema (use types from any different spaces) and extend it with new attributes
3. Use an existing schema (use types from any different spaces), but change types of attributes
4. Don't use any existing schema

Publishing also must consider if the proposal for a schema change is rejected.

-> To be defined

## API

We expect that in an app you mostly going to interact with the entities from one space. There we want to provide an API to set a default space ID for querying and creating entities.

```ts
import { setDefaultSpaceId, subscribeToSpace } from '@graph-protocol/graph-framework';

setDefaultSpaceId('abc'); // this will automatically subscribe to the space

// in order to manually subscribe to other spaces as well we can use the subscribeToSpace function
// this is important to get updates for the entities
subscribeToSpace('cde');
```

When using React we can leverage a provider to provide the necessary information to the app.

```tsx
import { GraphProvider } from '@graph-protocol/graph-framework';

<GraphProvider defaultSpaceId={'abc'} spaces={['cde', 'fgh']}>
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@baiirun @yanivtal This would be one way of defining which spaces should be synced in order to query all the necessary data to be shown in an applications. That said I wonder where this data might come from in an application. It could be stored locally, but if I login to another device this must be synced. Naive approach would be to sync all, but a user could have hundreds of private spaces across hundreds of apps and we probably only want the relevant ones.

I was thinking about if apps can decide where to read/write data to then we maybe could create information in the personal space (private so it's not public info) which private spaces this specific app used. So after I login as an app I can read which spaces are relevant and should be synced.

Then the next question is if every app can access all my information or if we want some kind of mechanism to provide permissions.

This is bit of missing puzzle piece to me. Have you had more ideas here?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah this is super interesting and part of what we need to think about. I do think it could make sense for a user to have to grant permissions to an app to access a space. I'm not sure exactly how we should enforce that, using something that looks like a wallet UI (can be done later).

I'm up for the idea of having apps write to the user's personal space with configuration data that it needs to do things like sync.

<App />
</GraphProvider>
```

Note: if a function uses an spaceId that is not set as the default space ID or one of the spaces then it will throw an error.

### Create a new entity

Version 1:

```ts
const item = createEntity(
['User', 'Event'],
{
name: "John Doe",
},
'abc' // optional space ID where the entity should be published in
)
```

Version 2:
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@baiirun @yanivtal In the call we talked about an API like Version 1. Personally I like Version 2. Which one should we go for?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I personally like one better. I don't mind having the types key but the attributes key feels verbose to me.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we did for some reason want to do 2, I think "data" could be better than "attributes"

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I prefer the second one because it's more explicit, even if it's more verbose.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Synced w/ Yaniv and we are going with the second one and use data instead of attributes.


```ts
const entity = createEntity({
types: ['User', 'Event'],
data: {
name: "John Doe",
},
spaceId: 'abc', // optional space ID where the entity should be published in
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

change to space instead of spaceId

})
```

#### Error cases

In case you have two types with the same attribute names, but different types TypeScript should throw an error.

e.g.

```ts
export const schema = {
Sensor: {
temperature: type.Number,
},
Location: {
temperature: type.Text,
},
};

const entity = createEntity({
types: ['Sensor', 'Location'], // throws an error since the attribute temperature is of different types
attributes: {
temperature: "John Doe",
},
})
```

### Publishing an entity
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rename: Publishing edits


This will automatically retrieve the latest public version of the entity, create a diff and based on that create an OPS to publish the difference.

```ts
publish(entity.id);
Copy link
Contributor

@baiirun baiirun Dec 17, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm assuming this is publishing to the public graph?

There's other fields in an EDIT that a user might want to specify, like the name, additional authors that contributed to the EDIT, etc. GRC-20 outlines which fields are allowed on an EDIT so we probably want to expose all of those to the publish API.

Copy link
Collaborator Author

@nikgraf nikgraf Dec 17, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

spot on! also synced with Yaniv and it makes more sense to create a multi-step process

Step 1: createEdit calculate the ops based on the diff, allow to add meta data e.g. name, additional authors
Step 2: user can review the ops and remove parts that should not be published
Step 3: publish submit the edit

In theory if the app knows you exact intentions manual review could be skipped, but this might not be always the case.

```

### Update an entity

Update the attributes of an entity. TODO unclear if we can provide type-safety in this case.

```ts
updateEntity({
id: entity.id,
attributes: {
name: "Jane Doe",
},
});
```

Update the types and attributes of an entity.

```ts
updateEntity({
id: entity.id,
types: ['User', 'Sensor'], // optional to overwrite the types. This overwrites the entire list of types
attributes: {
name: "Jane Doe",
temperature: 123,
},
});
```

### Setting an attribute on an entity

It's possible to set an attribute on an entity and publish it directly. Since there is no entry in the schema or mappings file we need to set and publish the attribute directly. If we only store the attribute locally on the next publish the attribute would not be taken into account since we don't know what's the public attribute ID.

```ts
setAndPublishEntityAttribute({
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wouldn't call this an "entity attribute" maybe we use "triple"? And we can just call them "entity", "attribute" and "value". Do we need the key?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We also probably want to support publishing relations in the same way and not just triples. So it would be a setAndPublishProperty if we're calling data that is either a triple or a relation a "Property."

id: entity.id,
key: 'name',
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove key and use the attributeId directly. This way we also don't need to publish it right away and we can include it in the createEdit step.

Think of a query API style to also retrieve all the properties for an entity.

value: 'John Doe',
attributeId: 'abc',
});
```

### Querying entities

Here we want to match the SDK for the public GraphQL. Still in progress here: https://www.notion.so/Data-block-query-strings-152273e214eb808898dac2d6b1b3820c
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is up-for-debate IMO. My perspective is that the graph-framework should have the same constraints for which operators are allowed (e.g., is, isNot >=, etc.) as the filter string spec. But the query format can be optimized/idiomatic for JS and React developers. I posted in Slack a bit more on this as well.


TODO how do we distinguish between local and public queries?
Loading
Loading