diff --git a/composites/00-profile.graphql b/composites/00-profile.graphql index e7388f1..3e29242 100644 --- a/composites/00-profile.graphql +++ b/composites/00-profile.graphql @@ -1,5 +1,5 @@ type Profile - @createModel(accountRelation: SINGLE, description: "An author profile") + @createModel(accountRelation: SINGLE, description: "An author profile") @createIndex(fields: [{ path: "orcid" }]) @createIndex(fields: [{ path: "googleScholar" }]) @createIndex(fields: [{ path: "publicKey" }]) diff --git a/composites/001-researchField.graphql b/composites/001-researchField.graphql index 8bf5551..fcab995 100644 --- a/composites/001-researchField.graphql +++ b/composites/001-researchField.graphql @@ -2,5 +2,6 @@ type ResearchField @createModel(accountRelation: LIST, description: "A particular field of research") { owner: DID! @documentAccount + version: CommitID! @documentVersion title: String! @string(maxLength: 256) } diff --git a/composites/01-researchObject.graphql b/composites/01-researchObject.graphql index fdff4ad..b88ae6c 100644 --- a/composites/01-researchObject.graphql +++ b/composites/01-researchObject.graphql @@ -6,4 +6,5 @@ type ResearchObject version: CommitID! @documentVersion title: String! @string(maxLength: 250) manifest: CID! + metadata: String @string(maxLength: 1024) } diff --git a/composites/02-organization.graphql b/composites/02-organization.graphql index 7076ba3..3d76b81 100644 --- a/composites/02-organization.graphql +++ b/composites/02-organization.graphql @@ -3,6 +3,7 @@ type Organization @createIndex(fields: [{ path: "name" }]) { owner: DID! @documentAccount + version: CommitID! @documentVersion name: String! @string(maxLength: 250) members: [DID] @list(maxLength: 100000) } diff --git a/composites/06-attestation.graphql b/composites/06-attestation.graphql index aac5a34..d193f8f 100644 --- a/composites/06-attestation.graphql +++ b/composites/06-attestation.graphql @@ -4,19 +4,23 @@ type Claim @loadModel(id: "$CLAIM_ID") { type Attestation @createModel(accountRelation: LIST, description: "General attestation") - @createIndex(fields: [{ path: "targetID" }]) - @createIndex(fields: [{ path: "claimID" }]) @createIndex(fields: [{ path: "revoked" }]) + @createIndex(fields: [{ path: "targetID" }]) + @createIndex(fields: [{ path: "targetVersion" }]) + @createIndex(fields: [{ path: "claimVersion" }]) { source: DID! @documentAccount version: CommitID! @documentVersion # 1. Any type of document, shown with relation on reciever end but cannot - # use @documentRelation without specifying single model - # 2. This can be CommitID if we need to lock it down, but - # we'll need to query ceramic directly for the payload - # 3. This cannot be a DID, to attestations to people target the Profile + # use @documentRelation without specifying the target type + # 2. This cannot be a DID, so attestations to people target the Profile targetID: StreamID! + targetVersion: CommitID! + claimID: StreamID! @documentReference(model: "Claim") claim: Claim! @relationDocument(property: "claimID") - revoked: Boolean! + claimVersion: CommitID! + + # Revoke this attestation + revoked: Boolean } diff --git a/composites/07-researchComponent.graphql b/composites/07-researchComponent.graphql index 456b29f..f23fc54 100644 --- a/composites/07-researchComponent.graphql +++ b/composites/07-researchComponent.graphql @@ -5,12 +5,14 @@ type ResearchObject @loadModel(id: "$RESEARCH_OBJECT_ID") { type ResearchComponent @createModel( accountRelation: LIST, - description: "A contextualized DAG pointer for a research object, adding metadata to the contents" + description: "A contextualized DAG pointer for a research object" ) @createIndex(fields: [{ path: "dagNode" }]) - @createIndex(fields: [{ path: "researchObjectID" }]) + @createIndex(fields: [{ path: "mimeType" }]) + @createIndex(fields: [{ path: "researchObjectVersion" }]) { owner: DID! @documentAccount + version: CommitID! @documentVersion name: String! @string(maxLength: 512) mimeType: String! @string(maxLength: 128) dagNode: CID! @@ -18,4 +20,5 @@ type ResearchComponent # The associated research object in which this component lives researchObjectID: StreamID! @documentReference(model: "ResearchObject") researchObject: ResearchObject! @relationDocument(property: "researchObjectID") + researchObjectVersion: CommitID! } diff --git a/composites/08-referenceRelation.graphql b/composites/08-referenceRelation.graphql index c9e3bc3..792bba5 100644 --- a/composites/08-referenceRelation.graphql +++ b/composites/08-referenceRelation.graphql @@ -4,12 +4,21 @@ type ResearchObject @loadModel(id: "$RESEARCH_OBJECT_ID") { type ReferenceRelation @createModel(accountRelation: LIST, description: "Indicate a reference between research objects") + @createIndex(fields: [{ path: "fromVersion" }]) + @createIndex(fields: [{ path: "toVersion" }]) + @createIndex(fields: [{ path: "revoked" }]) { owner: DID! @documentAccount + version: CommitID! @documentVersion fromID: StreamID! @documentReference(model: "ResearchObject") from: ResearchObject! @relationDocument(property: "fromID") + fromVersion: CommitID! toID: StreamID! @documentReference(model: "ResearchObject") to: ResearchObject! @relationDocument(property: "toID") + toVersion: CommitID! + + # Revoke this relation + revoked: Boolean } diff --git a/composites/09-contributorRelation.graphql b/composites/09-contributorRelation.graphql index 9e2990a..c42a8ad 100644 --- a/composites/09-contributorRelation.graphql +++ b/composites/09-contributorRelation.graphql @@ -9,30 +9,28 @@ type Profile @loadModel(id: "$PROFILE_ID") { type ContributorRelation @createModel(accountRelation: LIST, description: "List a contributor on a research object") @createIndex(fields: [{ path: "role" }]) - @createIndex(fields: [{ path: "contributorID" }]) - #@createIndex(fields: [{ path: ["info", "orcid"] }]) - #@createIndex(fields: [{ path: ["info", "googleScholar"] }]) + @createIndex(fields: [{ path: "revoked" }]) + @createIndex(fields: [{ path: "researchObjectVersion" }]) { owner: DID! @documentAccount + version: CommitID! @documentVersion # E.g. credit taxonomy role: String! @string(maxLength: 256) researchObjectID: StreamID! @documentReference(model: "ResearchObject") researchObject: ResearchObject! @relationDocument(property: "researchObjectID") + researchObjectVersion: CommitID! + # Skipping stream versioning on profile reference contributorID: StreamID @documentReference(model: "Profile") contributor: Profile @relationDocument(property: "contributorID") # In case the author doesn't have a profile in the protocol, manual information - # can be listed. Otherwise, depend on what the author lists on this profile instead - info: FallbackInfo + # can be listed. Otherwise, depend on what the author lists on this profile instead. + # Arbitrary JSON mapping the service/social network to the contributors handle. + fallbackInfo: String @string(maxLength: 1024) - # Support revokation? + # Revoke this relation + revoked: Boolean } - -type FallbackInfo { - orcid: String @string(maxLength: 256) - googleScholar: String @string(maxLength: 256) - publicKey: String @string(maxLength: 512) -} \ No newline at end of file diff --git a/composites/10-researchFieldRelation.graphql b/composites/10-researchFieldRelation.graphql index 86b29a1..a4afabd 100644 --- a/composites/10-researchFieldRelation.graphql +++ b/composites/10-researchFieldRelation.graphql @@ -10,10 +10,16 @@ type ResearchFieldRelation @createModel(accountRelation: LIST, description: "Association between a research object and a field") { owner: DID! @documentAccount + version: CommitID! @documentVersion researchObjectID: StreamID! @documentReference(model: "ResearchObject") researchObject: ResearchObject! @relationDocument(property: "researchObjectID") + researchObjectVersion: CommitID! + # Skipping stream versioning on field refs fieldID: StreamID! @documentReference(model: "ResearchField") field: ResearchField! @relationDocument(property: "fieldID") + + # Revoke this relation + revoked: Boolean } \ No newline at end of file diff --git a/composites/11-annotation.graphql b/composites/11-annotation.graphql index d8d3b12..f5e5ab4 100644 --- a/composites/11-annotation.graphql +++ b/composites/11-annotation.graphql @@ -1,24 +1,27 @@ -type ResearchComponent @loadModel(id: "$RESEARCH_COMPONENT_ID") { - id: ID! -} - type Claim @loadModel(id: "$CLAIM_ID") { id: ID! } type Annotation - @createModel(accountRelation: LIST, description: "Research component commentary") + @createModel(accountRelation: LIST, description: "Textual commentary") + @createIndex(fields: [{ path: "targetVersion" }]) + @createIndex(fields: [{ path: "claimVersion" }]) { owner: DID! @documentAccount + version: CommitID! @documentVersion comment: String! @string(maxLength: 1024) - componentID: StreamID! @documentReference(model: "ResearchComponent") - component: ResearchComponent! @relationDocument(property: "componentID") + targetID: StreamID! # Research object, researchComponent, or annotation as reply + targetVersion: CommitID! - # A way to identify the location of the annotation in the target component - # payload, for example a JSON path, line number, or coordinates in a pdf + # If target is a component, identify the location of the annotation + # payload. For example a JSON path, line number, or coordinates in a pdf. path: String @string(maxLength: 512) + # Optionally tag a claim to contextualise the annotation claimID: StreamID @documentReference(model: "Claim") claim: Claim @relationDocument(property: "claimID") + claimVersion: CommitID! + + metadataPayload: String @string(maxLength: 1024) } diff --git a/composites/additional-relations.graphql b/composites/additional-relations.graphql index c662164..a51988b 100644 --- a/composites/additional-relations.graphql +++ b/composites/additional-relations.graphql @@ -1,10 +1,4 @@ # This composite holds additional relations that cause circular dependencies when deploying -#type ResearchObjectAttestation @loadModel(id: "$RESEARCH_OBJECT_ATTESTATION_ID") { -# id: ID! -#} -#type ProfileAttestation @loadModel(id: "$PROFILE_ATTESTATION_ID") { -# id: ID! -#} type Attestation @loadModel(id: "$ATTESTATION_ID") { id: ID! } @@ -22,12 +16,13 @@ type ResearchFieldRelation @loadModel(id: "$RESEARCH_FIELD_RELATION_ID") { } type Annotation @loadModel(id: "$ANNOTATION_ID") { - id: ID! + replies: [Annotation] @relationFrom(model: "Annotation", property: "targetID") + replyCount: Int! @relationCountFrom(model: "Annotation", property: "targetID") } type ResearchComponent @loadModel(id: "$RESEARCH_COMPONENT_ID") { - annotations: [Annotation] @relationFrom(model: "Annotation", property: "componentID") - annotationCount: Int! @relationCountFrom(model: "Annotation", property: "componentID") + annotations: [Annotation] @relationFrom(model: "Annotation", property: "targetID") + annotationCount: Int! @relationCountFrom(model: "Annotation", property: "targetID") } type Claim @loadModel(id: "$CLAIM_ID") { @@ -54,6 +49,9 @@ type ResearchObject @loadModel(id: "$RESEARCH_OBJECT_ID") { contributorCount: Int! @relationCountFrom(model: "ContributorRelation", property: "researchObjectID") researchFields: [ResearchFieldRelation] @relationFrom(model: "ResearchFieldRelation", property: "researchObjectID") + + annotations: [Annotation] @relationFrom(model: "Annotation", property: "targetID") + annotationCount: Int! @relationCountFrom(model: "Annotation", property: "targetID") } # Support for incoming relations on CeramicAccounts is coming, but we'd need a separate diff --git a/scripts/composites.mjs b/scripts/composites.mjs index 755f119..8d6b76c 100644 --- a/scripts/composites.mjs +++ b/scripts/composites.mjs @@ -1,4 +1,4 @@ -import { copyFileSync, readFileSync } from "fs"; +import { readFileSync } from "fs"; import { CeramicClient } from "@ceramicnetwork/http-client"; import { createComposite, @@ -102,8 +102,7 @@ export const writeComposite = async (spinner) => { const annotationSchema = readFileSync( "./composites/11-annotation.graphql", { encoding: "utf-8"} - ).replace("$RESEARCH_COMPONENT_ID", componentComposite.modelIDs[1]) - .replace("$CLAIM_ID", researchFieldComposite.modelIDs[0]); + ).replace("$CLAIM_ID", claimComposite.modelIDs[0]); const annotationComposite = await Composite.create({ ceramic, @@ -114,8 +113,6 @@ export const writeComposite = async (spinner) => { "./composites/additional-relations.graphql", { encoding: "utf-8" } ) - // .replace("$RESEARCH_OBJECT_ATTESTATION_ID", researchAttestationComposite.modelIDs[2]) - // .replace("$PROFILE_ATTESTATION_ID", profAttestationComposite.modelIDs[1]) .replace("$ATTESTATION_ID", attestationComposite.modelIDs[1]) .replace("$CLAIM_ID", claimComposite.modelIDs[0]) .replace("$RESEARCH_OBJECT_ID", researchObjComposite.modelIDs[0]) @@ -125,7 +122,7 @@ export const writeComposite = async (spinner) => { .replace("$REFERENCE_RELATION_ID", referenceRelationComposite.modelIDs[1]) .replace("$RESEARCH_FIELD_ID", researchFieldComposite.modelIDs[0]) .replace("$RESEARCH_FIELD_RELATION_ID", researchFieldRelationComposite.modelIDs[2]) - .replace("$ANNOTATION_ID", annotationComposite.modelIDs[2]); + .replace("$ANNOTATION_ID", annotationComposite.modelIDs[1]); const additionalRelationsComposite = await Composite.create({ ceramic, @@ -160,21 +157,7 @@ export const writeComposite = async (spinner) => { "./src/__generated__/definition.json" ); - // This is rediculous but there is a combination of things forcing - // requirements on the filenames - copyFileSync( - './src/__generated__/definition.js', - './src/__generated__/definition.mjs' - ); - const { definition } = await import('../src/__generated__/definition.mjs'); - const aliases = Object.entries(definition.models) - .map(([name, model]) => [name, model.id]); - // console.log('ALIASES:', aliases) - - const aliasedDeployComposite = deployComposite.setAliases( - Object.fromEntries(aliases) - ); - await aliasedDeployComposite.startIndexingOn(ceramic); + await deployComposite.startIndexingOn(ceramic); spinner.succeed("composite deployed & ready for use"); }; diff --git a/template_data.json b/template_data.json index e853e32..bbac70a 100644 --- a/template_data.json +++ b/template_data.json @@ -55,13 +55,13 @@ "dc0b1125d15f276c5e6fdaf2465ae9f25d5e9984a7cb062640345c941d3ed1e0", "researchObjects", 1, - "id" + "IDs" ], "toPath": [ "dc0b1125d15f276c5e6fdaf2465ae9f25d5e9984a7cb062640345c941d3ed1e0", "researchObjects", 0, - "id" + "IDs" ] } ], @@ -71,7 +71,7 @@ "dc0b1125d15f276c5e6fdaf2465ae9f25d5e9984a7cb062640345c941d3ed1e0", "researchObjects", 0, - "id" + "IDs" ], "fieldPath": [ "1a40e10dc4830864781716eab4f88c60d74d8718cc5ed9af268d338a2096833f", @@ -84,7 +84,7 @@ "dc0b1125d15f276c5e6fdaf2465ae9f25d5e9984a7cb062640345c941d3ed1e0", "researchObjects", 1, - "id" + "IDs" ], "fieldPath": [ "1a40e10dc4830864781716eab4f88c60d74d8718cc5ed9af268d338a2096833f", @@ -123,7 +123,7 @@ "17cd4168d7f322ef17a01dde0ae0900cf9beebdf2c2edd39029041819cdb1015", "researchObjects", 0, - "id" + "IDs" ], "contributorPath": [ "dc0b1125d15f276c5e6fdaf2465ae9f25d5e9984a7cb062640345c941d3ed1e0", @@ -138,7 +138,7 @@ "17cd4168d7f322ef17a01dde0ae0900cf9beebdf2c2edd39029041819cdb1015", "researchObjects", 0, - "id" + "IDs" ], "fieldPath": [ "1a40e10dc4830864781716eab4f88c60d74d8718cc5ed9af268d338a2096833f", @@ -154,7 +154,7 @@ "17cd4168d7f322ef17a01dde0ae0900cf9beebdf2c2edd39029041819cdb1015", "researchObjects", 0, - "id" + "IDs" ], "claimPath": [ "1a40e10dc4830864781716eab4f88c60d74d8718cc5ed9af268d338a2096833f", @@ -188,7 +188,7 @@ "17cd4168d7f322ef17a01dde0ae0900cf9beebdf2c2edd39029041819cdb1015", "researchObjects", 0, - "id" + "IDs" ], "claimPath": [ "1a40e10dc4830864781716eab4f88c60d74d8718cc5ed9af268d338a2096833f", @@ -201,7 +201,7 @@ { "comment": "I viewed this dataset and it seems to check out!", "path": ".", - "componentPath": [ + "targetPath": [ "17cd4168d7f322ef17a01dde0ae0900cf9beebdf2c2edd39029041819cdb1015", "researchObjects", 0, @@ -213,6 +213,20 @@ "claims", 1 ] + }, + { + "comment": "I recommend this RO gets stamped as quality content", + "targetPath": [ + "17cd4168d7f322ef17a01dde0ae0900cf9beebdf2c2edd39029041819cdb1015", + "researchObjects", + 0, + "IDs" + ], + "claimPath": [ + "1a40e10dc4830864781716eab4f88c60d74d8718cc5ed9af268d338a2096833f", + "claims", + 0 + ] } ] } diff --git a/test/root.spec.ts b/test/root.spec.ts index f70f76d..74a51d8 100644 --- a/test/root.spec.ts +++ b/test/root.spec.ts @@ -1,10 +1,10 @@ import { ComposeClient } from '@composedb/client' import { definition } from '@/src/__generated__/definition' import { RuntimeCompositeDefinition } from '@composedb/types' -import { test, describe, beforeAll } from 'vitest' +import { test, describe, beforeAll, expect } from 'vitest' import { mutationCreateAttestation, mutationCreateClaim, mutationCreateProfile, - mutationCreateResearchObject, mutationUpdateAttestation, mutationUpdateResearchObject + mutationCreateResearchObject, mutationUpdateAttestation, mutationUpdateResearchObject, queryResearchObjects } from '../utils/queries' import { randomDID } from './util' import { CeramicClient } from '@ceramicnetwork/http-client' @@ -90,16 +90,14 @@ describe('ComposeDB nodes', () => { await mutationCreateAttestation( composeClient, { - targetID: myResearchObject, - claimID: myClaim, + targetID: myResearchObject.streamID, + targetVersion: myResearchObject.commitID, + claimID: myClaim.streamID, + claimVersion: myClaim.commitID, revoked: false } ); }); - - test.skip('organization', async () => { - // pending membership modelling - }) }); describe('Attestations', async () => { @@ -128,8 +126,10 @@ describe('ComposeDB nodes', () => { await mutationCreateAttestation( composeClient, { - targetID: ownProfile, - claimID: testClaim, + targetID: ownProfile.streamID, + targetVersion: ownProfile.commitID, + claimID: testClaim.streamID, + claimVersion: testClaim.commitID, revoked: false } ); @@ -151,8 +151,10 @@ describe('ComposeDB nodes', () => { await mutationCreateAttestation( composeClient, { - targetID: user1ResearchObject, - claimID: testClaim, + targetID: user1ResearchObject.streamID, + targetVersion: user1ResearchObject.commitID, + claimID: testClaim.streamID, + claimVersion: testClaim.commitID, revoked: false } ); @@ -172,8 +174,10 @@ describe('ComposeDB nodes', () => { const attestation = await mutationCreateAttestation( composeClient, { - targetID: researchObject, - claimID: testClaim, + targetID: researchObject.streamID, + targetVersion: researchObject.commitID, + claimID: testClaim.streamID, + claimVersion: testClaim.commitID, revoked: false } ); @@ -181,13 +185,23 @@ describe('ComposeDB nodes', () => { await mutationUpdateAttestation( composeClient, { - id: attestation, + id: attestation.streamID, revoked: true } ); }) }) + describe.skip('Annotations', async () => { + test('can comment on research component', async () => { }); + + test('can comment on research object', async () => { }); + + test('can suggest metadata change on research component', async () => { }); + + test('can suggest metadata changes on research object', async () => { }); + }); + describe('User', async () => { const composeClient = freshClient(); const user = await randomDID(); @@ -204,7 +218,7 @@ describe('ComposeDB nodes', () => { await mutationUpdateResearchObject( composeClient, { - id: researchObject, + id: researchObject.streamID, title: 'A fancy new title', manifest: "bafkreibtsll3aq2bynvlxnqh6nxafzdm4cpiovr3bcncbkzjcy32xaaaaa" } @@ -233,12 +247,44 @@ describe('ComposeDB nodes', () => { ); }, TIMEOUT); - }) + }); + + describe('System', async () => { + const composeClient = freshClient(); + const user = await randomDID(); + composeClient.setDID(user); + + test('can get commits anchored before a certain time', async () => { + // This assumes anchors have been made, which is very fast running locally + // but are made in longer time periods with on-chain anchoring + + const { streamID } = await mutationCreateResearchObject( + composeClient, + { + title: 'Old', + manifest: A_CID + } + ); + const timeBetween = Date.now(); + await setTimeout(1000) + await mutationUpdateResearchObject( + composeClient, + { + id: streamID, + title: 'New' + } + ); + + const stream = await ceramic.loadStream(streamID); + expect(stream.state.content.title).toEqual('New'); + + const streamBetween = await ceramic.loadStream(streamID, { atTime: timeBetween }); + expect(streamBetween.state.content.title).toEqual('Old'); + }); + }); + - describe.skip('Querying relations', async () => { - test.todo('') - }) }) const freshClient = () => - new ComposeClient({ceramic, definition: definition as RuntimeCompositeDefinition}) + new ComposeClient({ ceramic, definition: definition as RuntimeCompositeDefinition }) diff --git a/types/index.ts b/types/index.ts index fb7f799..01be5ea 100644 --- a/types/index.ts +++ b/types/index.ts @@ -1,5 +1,6 @@ export type Profile = { id?: string + version?: string displayName?: string orcid?: string }; @@ -10,22 +11,27 @@ export type DID = { export type ROProps = { id?: string + version?: string title: string manifest: string components?: ResearchComponent[] + metadata?: string owner?: DID }; export type ResearchComponent = { owner?: DID + version?: string name: string mimeType: string dagNode: string researchObjectID: string + researchObjectVersion: string }; export type Claim = { id?: string + version?: string title: string description: string badge?: string @@ -33,21 +39,27 @@ export type Claim = { export type Attestation = { id?: string + version?: string source?: DID targetID: string + targetVersion: string claimID: string + claimVersion: string claim?: Claim revoked: boolean }; export type Annotation = { id?: string + version?: string comment: string - path: string - componentID: string - component?: ResearchComponent - claimID: string + path?: string + targetID: string + targetVersion: string + claimID?: string claim?: Claim + claimVersion?: string + metadataPayload?: string }; export type ContributorRelation = { @@ -56,18 +68,43 @@ export type ContributorRelation = { // info contributorID: string researchObjectID: string + researchObjectVersion: string }; export type ReferenceRelation = { id?: string toID: string + toVersion: string fromID: string + fromVersion: string +}; + +export type ResearchField = { + title: string }; export type ResearchFieldRelation = { id?: string fieldID: string researchObjectID: string + researchObjectVersion: string +}; + +export type MutationTarget = + Profile | + ROProps | + ResearchComponent | + Claim | + Attestation | + Annotation | + ContributorRelation | + ReferenceRelation | + ResearchField | + ResearchFieldRelation; + +export type NodeIDs = { + streamID: string, + commitID: string }; export type SidebarProps = { diff --git a/utils/populate.tsx b/utils/populate.tsx index dded257..35455b4 100644 --- a/utils/populate.tsx +++ b/utils/populate.tsx @@ -4,7 +4,8 @@ import { DID } from "dids" import { fromString } from "uint8arrays/from-string" import untypedTemplateData from "../template_data.json" import { ComposeClient } from "@composedb/client" -import { +import { + mutationCreateAnnotation, mutationCreateAttestation, mutationCreateClaim, mutationCreateContributorRelation, @@ -18,8 +19,9 @@ import { import { CeramicClient } from "@ceramicnetwork/http-client" import { definition } from '@/src/__generated__/definition' import { RuntimeCompositeDefinition } from "@composedb/types" -import { Attestation, ROProps, ResearchComponent } from "@/types" -import { +import { Annotation, Attestation, NodeIDs, ROProps } from "@/types" +import { + AnnotationTemplate, AttestationTemplate, ContributorRelationTemplate, DataTemplate, @@ -41,17 +43,17 @@ const didFromSeed = async (seed: string) => { }, }); await did.authenticate(); - return did -} + return did; +}; -type ProfileIndexResults = { data: { profileIndex: { edges: []}}} +type ProfileIndexResults = { data: { profileIndex: { edges: [] } } } export const loadIfUninitialised = async (ceramic: CeramicClient) => { const composeClient = new ComposeClient( { ceramic, definition: definition as RuntimeCompositeDefinition } - ) + ); const firstProfile = await composeClient.executeQuery(` query { profileIndex(first: 1) { @@ -62,32 +64,33 @@ export const loadIfUninitialised = async (ceramic: CeramicClient) => { } } } - `) as ProfileIndexResults + `) as ProfileIndexResults; if (firstProfile.data.profileIndex.edges.length === 0) { console.log("Profile index empty, loading template data...") await loadTemplateData(composeClient) } else { console.log("Found profiles in index, skipping template data initialisation.") - } -} + }; +}; -// Same shape as the template data, but with streamIDs for each leaf node -type ActorDataStreamIDs = { - profile?: string +// Same shape as the template data, but with NodeIDs for each leaf +type ActorDataNodeIDs = { + profile?: NodeIDs researchObjects: { - id: string, - components: string[] + IDs: NodeIDs + components: NodeIDs[] }[], - claims: string[], - researchFields: string[], - attestations: string[], - contributorRelations: string[], - referenceRelations: string[], - researchFieldRelations: string[] -} + claims: NodeIDs[], + researchFields: NodeIDs[], + attestations: NodeIDs[], + contributorRelations: NodeIDs[], + referenceRelations: NodeIDs[], + researchFieldRelations: NodeIDs[], + annotations: NodeIDs[] +}; -const freshActorRecord = (profile: string): ActorDataStreamIDs => ( +const freshActorRecord = (profile: NodeIDs): ActorDataNodeIDs => ( { profile, researchObjects: [], @@ -96,11 +99,12 @@ const freshActorRecord = (profile: string): ActorDataStreamIDs => ( researchFields: [], contributorRelations: [], referenceRelations: [], - researchFieldRelations: [] + researchFieldRelations: [], + annotations: [] } -) +); -type StreamIndex = Record +type StreamIndex = Record; /** * Iterates over template data file, and with a DID generated from each root entry @@ -113,8 +117,8 @@ const loadTemplateData = async (composeClient: ComposeClient) => { for (const [seed, template] of Object.entries(templateData)) { composeClient.setDID(await didFromSeed(seed)) - const profileID = await mutationCreateProfile(composeClient, template.profile); - streamIndex[seed] = freshActorRecord(profileID); + const profileIDs = await mutationCreateProfile(composeClient, template.profile); + streamIndex[seed] = freshActorRecord(profileIDs); streamIndex[seed].researchFields = await Promise.all( template.researchFields.map( @@ -129,7 +133,7 @@ const loadTemplateData = async (composeClient: ComposeClient) => { ); streamIndex[seed].contributorRelations = await Promise.all( - template.contributorRelations.map((contTemplate: any) => + template.contributorRelations.map((contTemplate: any) => loadContributorRelation(contTemplate, streamIndex, composeClient) ) ); @@ -152,105 +156,143 @@ const loadTemplateData = async (composeClient: ComposeClient) => { streamIndex[seed].attestations = await Promise.all( template.attestations.map(attTemplate => loadAttestation( - attTemplate, - streamIndex, - composeClient + attTemplate, streamIndex, composeClient )) ); - } - console.log("Loading template data done!") + + streamIndex[seed].annotations = await Promise.all( + template.annotations.map(annTemplate => loadAnnotation( + annTemplate, streamIndex, composeClient + )) + ); + }; + console.log("Loading template data done!"); } const loadResearchObject = async ( roTemplate: ResearchObjectTemplate, composeClient: ComposeClient -): Promise => { - const roProps: ROProps = { +): Promise => { + const roProps: ROProps = { title: roTemplate.title, manifest: roTemplate.manifest } - const researchObject = await mutationCreateResearchObject(composeClient, roProps) + const researchObject = await mutationCreateResearchObject(composeClient, roProps); // Possibly create manifest components if such exist const components = await Promise.all( - roTemplate.components.map((c: any) => + roTemplate.components.map((c: any) => mutationCreateResearchComponent( - composeClient, - { + composeClient, + { ...c, - researchObjectID: researchObject - } as ResearchComponent + researchObjectID: researchObject.streamID, + researchObjectVersion: researchObject.commitID + } ) ) ); - return { id: researchObject, components } -} + return { IDs: researchObject, components }; +}; const loadContributorRelation = async ( contTemplate: ContributorRelationTemplate, streamIndex: StreamIndex, composeClient: ComposeClient -): Promise => { +): Promise => { const { role, researchObjectPath, contributorPath } = contTemplate; - const researchObjectID = recursePathToID(streamIndex, researchObjectPath); - const contributorID = recursePathToID(streamIndex, contributorPath); + const researchObject = recursePathToID(streamIndex, researchObjectPath); + const contributor = recursePathToID(streamIndex, contributorPath); return await mutationCreateContributorRelation( - composeClient, + composeClient, { - role, - contributorID, - researchObjectID + role, + contributorID: contributor.streamID, + researchObjectID: researchObject.streamID, + researchObjectVersion: researchObject.commitID } ); -} +}; const loadReferenceRelation = async ( refTemplate: ReferenceRelationTemplate, streamIndex: StreamIndex, composeClient: ComposeClient -): Promise => { +): Promise => { const { toPath, fromPath } = refTemplate; - const toID = recursePathToID(streamIndex, toPath); - const fromID = recursePathToID(streamIndex, fromPath); + const to = recursePathToID(streamIndex, toPath); + const from = recursePathToID(streamIndex, fromPath); return await mutationCreateReferenceRelation( - composeClient, + composeClient, { - toID, - fromID + toID: to.streamID, + toVersion: to.commitID, + fromID: from.streamID, + fromVersion: from.commitID } ); -} +}; const loadResearchFieldRelation = async ( fieldRelTemplate: ResearchFieldRelationTemplate, streamIndex: StreamIndex, composeClient: ComposeClient -): Promise => { +): Promise => { const { researchObjectPath, fieldPath } = fieldRelTemplate; - const researchObjectID = recursePathToID(streamIndex, researchObjectPath); - const fieldID = recursePathToID(streamIndex, fieldPath); + const researchObject = recursePathToID(streamIndex, researchObjectPath); + const field = recursePathToID(streamIndex, fieldPath); return await mutationCreateResearchFieldRelation( - composeClient, + composeClient, { - researchObjectID, - fieldID + researchObjectID: researchObject.streamID, + researchObjectVersion: researchObject.commitID, + fieldID: field.streamID } ); -} +}; const loadAttestation = async ( attestationTemplate: AttestationTemplate, streamIndex: StreamIndex, composeClient: ComposeClient -): Promise => { +): Promise => { const { targetPath, claimPath } = attestationTemplate; - const targetID = recursePathToID(streamIndex, targetPath); - const claimID = recursePathToID(streamIndex, claimPath); - const attestation: Attestation = { targetID, claimID, revoked: false }; + const target = recursePathToID(streamIndex, targetPath); + const claim = recursePathToID(streamIndex, claimPath); + const attestation: Attestation = { + targetID: target.streamID, + targetVersion: target.commitID, + claimID: claim.streamID, + claimVersion: claim.commitID, + revoked: false + }; return mutationCreateAttestation(composeClient, attestation); -} +}; + +const loadAnnotation = async ( + annotationTemplate: AnnotationTemplate, + streamIndex: StreamIndex, + composeClient: ComposeClient +): Promise => { + const { comment, path, targetPath, claimPath } = annotationTemplate; + const target = recursePathToID(streamIndex, targetPath); + const annotation: Annotation = { + targetID: target.streamID, + targetVersion: target.commitID, + comment + }; + if (claimPath) { + const claim = recursePathToID(streamIndex, claimPath); + annotation.claimID = claim.streamID; + annotation.claimVersion = claim.commitID; + }; + if (path) { + annotation.path = path; + }; + return mutationCreateAnnotation(composeClient, annotation); +}; // Oblivious to human faults, enjoy the footgun -const recursePathToID = (object: any, path: ObjectPath): string => - path.length ? recursePathToID(object[path[0]], path.slice(1)) : object +const recursePathToID = (object: any, path: ObjectPath): NodeIDs => + path.length ? recursePathToID(object[path[0]], path.slice(1)) : object; diff --git a/utils/queries.ts b/utils/queries.ts index 3b24d7f..3e71001 100644 --- a/utils/queries.ts +++ b/utils/queries.ts @@ -1,11 +1,11 @@ import { ComposeClient } from "@composedb/client"; -import { Attestation, Claim, ResearchComponent, Profile, ROProps, ContributorRelation, ReferenceRelation, ResearchFieldRelation } from "../types"; +import { Attestation, Claim, ResearchComponent, Profile, ROProps, ContributorRelation, ReferenceRelation, ResearchFieldRelation, MutationTarget, Annotation, NodeIDs, ResearchField } from "../types"; import { ExecutionResult } from "graphql"; export const queryViewerId = async ( composeClient: ComposeClient ): Promise => { - const response = await composeClient.executeQuery<{viewer: { id: string}}>(` + const response = await composeClient.executeQuery<{ viewer: { id: string } }>(` query { viewer { id @@ -22,7 +22,7 @@ export const queryViewerId = async ( export const queryViewerProfile = async ( composeClient: ComposeClient ): Promise => { - const response = await composeClient.executeQuery<{ viewer: { profile: Profile | null} }>(` + const response = await composeClient.executeQuery<{ viewer: { profile: Profile | null } }>(` query { viewer { profile { @@ -65,7 +65,7 @@ export const queryViewerClaims = async ( composeClient: ComposeClient ): Promise => { const response = await composeClient.executeQuery< - { viewer:{ claimList: { edges: { node: Claim}[] } } } + { viewer: { claimList: { edges: { node: Claim }[] } } } >(` query { viewer { @@ -149,302 +149,238 @@ export const queryResearchObjectAttestations = async ( export const mutationCreateResearchObject = async ( composeClient: ComposeClient, inputs: ROProps -): Promise => { - const response = await composeClient.executeQuery< - { createResearchObject: { document: { id: string } } } - >(` - mutation ($title: String!, $manifest: InterPlanetaryCID!){ - createResearchObject(input: { - content: { - title: $title - manifest: $manifest - } - }) - { - document { - id - } - } - }`, - inputs - ) - assertMutationErrors(response, 'create research object') - return response.data!.createResearchObject.document.id -} +): Promise => genericCreate( + composeClient, + inputs, + { + title: "String!", + manifest: "InterPlanetaryCID!", + metadata: "String" + }, + 'createResearchObject' +); export const mutationCreateResearchComponent = async ( composeClient: ComposeClient, inputs: ResearchComponent -): Promise => { - const gqlTypes: Partial> = { +): Promise => genericCreate( + composeClient, + inputs, + { name: "String!", mimeType: "String!", dagNode: "InterPlanetaryCID!", - researchObjectID: "CeramicStreamID!" - }; - const [params, content] = getQueryFields(gqlTypes, inputs); - const response = await composeClient.executeQuery< - { createResearchComponent: { document: { id: string } } } - >(` - mutation ( ${params} ){ - createResearchComponent(input: { - content: { ${content} } - }) - { - document { - id - } - } - }`, - inputs - ) - assertMutationErrors(response, 'create research object'); - return response.data!.createResearchComponent.document.id; -} + researchObjectID: "CeramicStreamID!", + researchObjectVersion: "CeramicCommitID!" + }, + 'createResearchComponent' +); export const mutationUpdateResearchObject = async ( composeClient: ComposeClient, inputs: Partial & { id: string } -): Promise => { - const gqlParamTypes: Partial> = { +): Promise => genericUpdate( + composeClient, + inputs, + { manifest: "InterPlanetaryCID", - title: "String" - }; - - const [params, content] = getQueryFields(gqlParamTypes, inputs); - const response = await composeClient.executeQuery(` - mutation ($id: ID!, ${params}){ - updateResearchObject(input: { - id: $id - content: { ${content} } - }) - { - document { - id - } - } - }`, - inputs - ); - assertMutationErrors(response, 'update research object'); -} + title: "String", + metadata: "String" + }, + 'updateResearchObject' +); export const mutationCreateProfile = async ( composeClient: ComposeClient, inputs: Profile -): Promise => { - const response = await composeClient.executeQuery< - { createProfile: { document: { id: string } } } - >(` - mutation ($displayName: String!, $orcid: String){ - createProfile(input: { - content: { - displayName: $displayName - orcid: $orcid - } - }) - { - document { - id - } - } - }`, - inputs - ) - assertMutationErrors(response, 'create profile'); - return response.data!.createProfile.document.id; -} +): Promise => genericCreate( + composeClient, + inputs, + { + displayName: "String!", + orcid: "String" + }, + 'createProfile' +); export const mutationCreateClaim = async ( composeClient: ComposeClient, inputs: Claim -): Promise => { - const response = await composeClient.executeQuery< - { createClaim: { document: { id: string } } } - >(` - mutation ($title: String!, $description: String!, $badge: InterPlanetaryCID){ - createClaim(input: { - content: { - title: $title - description: $description - badge: $badge - } - }) - { - document { - id - } - } - } - `, - inputs - ) - assertMutationErrors(response, 'create claim') - return response.data!.createClaim.document.id -} +): Promise => genericCreate( + composeClient, + inputs, + { + title: "String!", + description: "String!", + badge: "InterPlanetaryCID!", + }, + 'createClaim' +); export const mutationCreateAttestation = async ( - composeClient: ComposeClient, + composeClient: ComposeClient, inputs: Attestation -): Promise => { - const response = await composeClient.executeQuery< - { createAttestation: { document: { id: string } } } - >(` - mutation ($targetID: CeramicStreamID!, $claimID: CeramicStreamID!, $revoked: Boolean!){ - createAttestation(input: { - content: { - targetID: $targetID - claimID: $claimID - revoked: $revoked - } - }) - { - document { - id - } - } - } - `, - inputs - ); - assertMutationErrors(response, 'create attestation') - return response.data!.createAttestation.document.id -} +): Promise => genericCreate( + composeClient, + inputs, + { + targetID: "CeramicStreamID!", + targetVersion: "CeramicCommitID!", + claimID: "CeramicStreamID!", + claimVersion: "CeramicCommitID!", + revoked: "Boolean" + }, + 'createAttestation' +); export const mutationUpdateAttestation = async ( - composeClient: ComposeClient, + composeClient: ComposeClient, inputs: Partial & { id: string } -): Promise => { - const gqlParamTypes: Record = { +): Promise => genericUpdate( + composeClient, + inputs, + { targetID: "CeramicStreamID", claimID: "CeramicStreamID", revoked: "Boolean" - }; + }, + 'updateAttestation' +); - const [params, content] = getQueryFields(gqlParamTypes, inputs); - const response = await composeClient.executeQuery< - { updateAttestation: { document: { id: string } } } - >(` - mutation ($id: ID!, ${params}){ - updateAttestation(input: { - id: $id - content: { ${content} } - }) - { - document { - id - } - } - } - `, - inputs - ); - assertMutationErrors(response, 'update attestation'); - return response.data!.updateAttestation.document.id; -} +export const mutationCreateAnnotation = async ( + composeClient: ComposeClient, + inputs: Annotation +): Promise => genericCreate( + composeClient, + inputs, + { + comment: "String!", + path: "String", + metadataPayload: "String", + targetID: "CeramicStreamID!", + targetVersion: "CeramicCommitID!", + claimID: "CeramicStreamID!", + claimVersion: "CeramicCommitID!" + }, + 'createAnnotation' +); export const mutationCreateContributorRelation = async ( composeClient: ComposeClient, inputs: ContributorRelation -): Promise => { - const response = await composeClient.executeQuery< - { createContributorRelation: { document: { id: string }}} - >(` - mutation ($role: String!, $contributorID: CeramicStreamID!, $researchObjectID: CeramicStreamID!) { - createContributorRelation(input: { - content: { - role: $role - contributorID: $contributorID - researchObjectID: $researchObjectID - } - }) - { - document { - id - } - } - } - `, inputs - ); - assertMutationErrors(response, 'create contributor relation'); - return response.data!.createContributorRelation.document.id; -} +): Promise => genericCreate( + composeClient, + inputs, + { + role: "String!", + contributorID: "CeramicStreamID!", + researchObjectID: "CeramicStreamID!", + researchObjectVersion: "CeramicCommitID!" + }, + 'createContributorRelation' +); export const mutationCreateReferenceRelation = async ( composeClient: ComposeClient, inputs: ReferenceRelation -): Promise => { - const response = await composeClient.executeQuery< - { createReferenceRelation: { document: { id: string }}} - >(` - mutation ($fromID: CeramicStreamID!, $toID: CeramicStreamID!) { - createReferenceRelation(input: { - content: { - fromID: $fromID - toID: $toID - } - }) - { - document { - id - } - } - } - `, inputs - ); - assertMutationErrors(response, 'create reference relation'); - return response.data!.createReferenceRelation.document.id; -} +): Promise => genericCreate( + composeClient, + inputs, + { + toID: "CeramicStreamID!", + toVersion: "CeramicCommitID!", + fromID: "CeramicStreamID!", + fromVersion: "CeramicCommitID!" + }, + 'createReferenceRelation' +); export const mutationCreateResearchFieldRelation = async ( composeClient: ComposeClient, inputs: ResearchFieldRelation -): Promise => { - const response = await composeClient.executeQuery< - { createResearchFieldRelation: { document: { id: string }}} - >(` - mutation ($fieldID: CeramicStreamID!, $researchObjectID: CeramicStreamID!) { - createResearchFieldRelation(input: { - content: { - researchObjectID: $researchObjectID - fieldID: $fieldID - } +): Promise => genericCreate( + composeClient, + inputs, + { + fieldID: "CeramicStreamID!", + researchObjectID: "CeramicStreamID!", + researchObjectVersion: "CeramicCommitID!" + }, + 'createResearchFieldRelation' +); + +export const mutationCreateResearchField = async ( + composeClient: ComposeClient, + inputs: ResearchField +): Promise => genericCreate( + composeClient, + inputs, + { + title: "String!", + }, + 'createResearchField' +); + +async function genericCreate( + composeClient: ComposeClient, + inputs: T, + // At least verify all keys exist in T, can still forget one though. + // Can't require it fully because some props are not allowed in the mutation. + gqlTypes: Partial>, + mutationName: string +): Promise { + const [params, content] = getQueryFields(gqlTypes as Record, inputs); + const response = await composeClient.executeQuery(` + mutation( ${params} ) { + ${mutationName}(input: { + content: { ${content} } }) { document { id + version } } - } - `, inputs - ); - assertMutationErrors(response, 'create research field relation'); - return response.data!.createResearchFieldRelation.document.id; -} + }`, inputs + ) as any; + assertMutationErrors(response, mutationName); + const nodeIDs: NodeIDs = { + streamID: response.data[mutationName].document.id, + commitID: response.data[mutationName].document.version + }; + return nodeIDs; +}; -export const mutationCreateResearchField = async ( +async function genericUpdate( composeClient: ComposeClient, - inputs: { title: string } -): Promise => { - const response = await composeClient.executeQuery< - { createResearchField: { document: { id: string }}} - >(` - mutation ($title: String!) { - createResearchField(input: { - content: { - title: $title + inputs: Partial & { id: string }, + // See note in genericCreate + gqlTypes: Partial>, + mutationName: string +): Promise { + const [params, content] = getQueryFields(gqlTypes as Record, inputs); + const response = await composeClient.executeQuery(` + mutation($id: ID!, ${params} ) { + ${mutationName}( + input: { + id: $id + content: { ${content} } } - }) + ) { document { id + version } } - } - `, inputs - ); - assertMutationErrors(response, 'create research field'); - return response.data!.createResearchField.document.id; + }`, inputs + ) as any; + assertMutationErrors(response, mutationName); + const nodeIDs: NodeIDs = { + streamID: response.data[mutationName].document.id, + commitID: response.data[mutationName].document.version + }; + return nodeIDs; } type SimpleMutationResult = Pick @@ -465,8 +401,8 @@ const assertQueryErrors = ( queryDescription: string ) => { if (result.errors || !result.data) { - console.error("Error:", result.errors?.toString()); - throw new Error(`Query failed: ${queryDescription}!`); + console.error("Error:", result.errors?.toString()); + throw new Error(`Query failed: ${queryDescription}!`); }; } @@ -487,11 +423,11 @@ const getQueryFields = ( inputs: Record ) => Object.keys(inputs) - .filter(p => p !== 'id') - .reduce<[string[], string[]]>( - (acc, next) => [ - [...acc[0], `$${next}: ${graphQLParamTypes[next]}`], - [...acc[1], `${next}: $${next}`] - ], - [[],[]] - ).map(stringArr => stringArr.join(', ')); + .filter(p => p !== 'id') + .reduce<[string[], string[]]>( + (acc, next) => [ + [...acc[0], `$${next}: ${graphQLParamTypes[next]}`], + [...acc[1], `${next}: $${next}`] + ], + [[], []] + ).map(stringArr => stringArr.join(', ')); diff --git a/utils/templateData.d.ts b/utils/templateData.d.ts index a55439f..a39d6f2 100644 --- a/utils/templateData.d.ts +++ b/utils/templateData.d.ts @@ -52,9 +52,9 @@ export type AttestationTemplate = { export type AnnotationTemplate = { comment: string, - path: string, - componentPath: ObjectPath, - claimPath: ObjectPath + path?: string, + targetPath: ObjectPath, + claimPath?: ObjectPath }; export type ActorTemplate = { @@ -65,7 +65,8 @@ export type ActorTemplate = { contributorRelations: ContributorRelationTemplate[], referenceRelations: ReferenceRelationTemplate[], researchFieldRelations: ResearchFieldRelationTemplate[], - attestations: AttestationTemplate[] + attestations: AttestationTemplate[], + annotations: AnnotationTemplate[] }; export type DataTemplate = Record