Skip to content

Commit

Permalink
Interfaces support for MIDs (#2999)
Browse files Browse the repository at this point in the history
  • Loading branch information
PaulLeCam committed Oct 17, 2023
1 parent a7dc964 commit e6f71df
Show file tree
Hide file tree
Showing 2 changed files with 349 additions and 22 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import * as sha256 from '@stablelib/sha256'
import cloneDeep from 'lodash.clonedeep'
import jsonpatch from 'fast-json-patch'
import { ModelInstanceDocument } from '@ceramicnetwork/stream-model-instance'
import { Model, ModelDefinition } from '@ceramicnetwork/stream-model'
import type { ModelDefinition } from '@ceramicnetwork/stream-model'
import {
CeramicApi,
CommitType,
Expand Down Expand Up @@ -67,6 +67,30 @@ const FAKE_MODEL_ID2 = StreamID.fromString(
const FAKE_MODEL_IDBLOB = StreamID.fromString(
'kjzl6hvfrbw6c9aememmuuc3xj3xy0zvzbxstv8dnhl6f3jg7mqeengdgdist5b'
)
const FAKE_MODEL_INTERFACE_ID = StreamID.fromString(
'kjzl6hvfrbw6c9aememmuuc3xj3xy0zvzbxstv8dnhl6f3jg7mqeengdgdist5c'
)
const FAKE_MODEL_REQUIRED_RELATION_ID = StreamID.fromString(
'kjzl6hvfrbw6c9aememmuuc3xj3xy0zvzbxstv8dnhl6f3jg7mqeengdgdist5d'
)
const FAKE_MODEL_OPTIONAL_RELATION_ID = StreamID.fromString(
'kjzl6hvfrbw6c9aememmuuc3xj3xy0zvzbxstv8dnhl6f3jg7mqeengdgdist5e'
)
const FAKE_MODEL_INTERFACE_RELATION_ID = StreamID.fromString(
'kjzl6hvfrbw6c9aememmuuc3xj3xy0zvzbxstv8dnhl6f3jg7mqeengdgdist5f'
)
const FAKE_MODEL_IMPLEMENTS_RELATION_ID = StreamID.fromString(
'kjzl6hvfrbw6c9aememmuuc3xj3xy0zvzbxstv8dnhl6f3jg7mqeengdgdist5g'
)
const FAKE_MID_ID = StreamID.fromString(
'k2t6wzhkh1dbrv7qx7oii5uwjngvzgatek9lzvqnv2wq87jvfhafvi1lxbx200'
)
const FAKE_MID_ID2 = StreamID.fromString(
'k2t6wzhkh1dbrv7qx7oii5uwjngvzgatek9lzvqnv2wq87jvfhafvi1lxbx202'
)
const FAKE_MID_ID3 = StreamID.fromString(
'k2t6wzhkh1dbrv7qx7oii5uwjngvzgatek9lzvqnv2wq87jvfhafvi1lxbx203'
)

const CONTENT0 = { myData: 0 }
const CONTENT1 = { myData: 1 }
Expand Down Expand Up @@ -251,6 +275,155 @@ const MODEL_DEFINITION_BLOB: ModelDefinition = {
},
}

const MODEL_INTERFACE_DEFINITION: ModelDefinition = {
name: 'MyInterfaceModel',
version: '2.0',
interface: true,
implements: [],
accountRelation: { type: 'none' },
schema: {
$schema: 'https://json-schema.org/draft/2020-12/schema',
type: 'object',
additionalProperties: false,
properties: {
myData: {
type: 'string',
},
},
required: ['myData'],
},
}

const MODEL_DEFINITION_REQUIRED_RELATION: ModelDefinition = {
name: 'MyModel',
version: '1.0',
accountRelation: { type: 'list' },
schema: {
$schema: 'https://json-schema.org/draft/2020-12/schema',
type: 'object',
additionalProperties: false,
properties: {
myData: { type: 'integer', maximum: 100, minimum: 0 },
relationID: { type: 'string' },
},
required: ['myData', 'relationID'],
},
relations: {
relationID: { type: 'document', model: FAKE_MODEL_ID.toString() },
},
}

const MODEL_DEFINITION_OPTIONAL_RELATION: ModelDefinition = {
name: 'MyModel',
version: '2.0',
interface: false,
implements: [],
accountRelation: { type: 'list' },
schema: {
$schema: 'https://json-schema.org/draft/2020-12/schema',
type: 'object',
additionalProperties: false,
properties: {
myData: { type: 'integer', maximum: 100, minimum: 0 },
relationID: { type: 'string' },
},
required: ['myData'],
},
relations: {
relationID: { type: 'document', model: null },
},
}

const MODEL_DEFINITION_INTERFACE_RELATION: ModelDefinition = {
name: 'MyModel',
version: '2.0',
interface: false,
implements: [],
accountRelation: { type: 'list' },
schema: {
$schema: 'https://json-schema.org/draft/2020-12/schema',
type: 'object',
additionalProperties: false,
properties: {
myData: { type: 'integer', maximum: 100, minimum: 0 },
relationID: { type: 'string' },
},
required: ['myData'],
},
relations: {
relationID: { type: 'document', model: FAKE_MODEL_INTERFACE_ID.toString() },
},
}

const MODEL_DEFINITION_IMPLEMENTS_RELATION: ModelDefinition = {
name: 'MyModel',
version: '2.0',
interface: false,
implements: [FAKE_MODEL_INTERFACE_ID.toString()],
accountRelation: { type: 'list' },
schema: {
$schema: 'https://json-schema.org/draft/2020-12/schema',
type: 'object',
additionalProperties: false,
properties: {
myData: { type: 'integer', maximum: 100, minimum: 0 },
},
required: ['myData'],
},
}

const STREAMS = {
[FAKE_MODEL_ID.toString()]: {
content: MODEL_DEFINITION,
commitId: FAKE_MODEL_ID,
},
[FAKE_MODEL_ID2.toString()]: {
content: MODEL_DEFINITION_SINGLE,
commitId: FAKE_MODEL_ID2,
},
[FAKE_MODEL_IDBLOB.toString()]: {
content: MODEL_DEFINITION_BLOB,
commitId: FAKE_MODEL_IDBLOB,
},
[FAKE_MODEL_INTERFACE_ID.toString()]: {
id: FAKE_MODEL_INTERFACE_ID,
content: MODEL_INTERFACE_DEFINITION,
commitId: FAKE_MODEL_ID,
},
[FAKE_MODEL_REQUIRED_RELATION_ID.toString()]: {
id: FAKE_MODEL_REQUIRED_RELATION_ID,
content: MODEL_DEFINITION_REQUIRED_RELATION,
commitId: FAKE_MODEL_ID,
},
[FAKE_MODEL_OPTIONAL_RELATION_ID.toString()]: {
id: FAKE_MODEL_OPTIONAL_RELATION_ID,
content: MODEL_DEFINITION_OPTIONAL_RELATION,
commitId: FAKE_MODEL_ID,
},
[FAKE_MODEL_INTERFACE_RELATION_ID.toString()]: {
id: FAKE_MODEL_INTERFACE_RELATION_ID,
content: MODEL_DEFINITION_INTERFACE_RELATION,
commitId: FAKE_MODEL_ID,
},
[FAKE_MODEL_IMPLEMENTS_RELATION_ID.toString()]: {
id: FAKE_MODEL_IMPLEMENTS_RELATION_ID,
content: MODEL_DEFINITION_IMPLEMENTS_RELATION,
commitId: FAKE_MODEL_ID,
},
[FAKE_MID_ID.toString()]: {
content: {},
metadata: { model: FAKE_MODEL_ID },
},
[FAKE_MID_ID2.toString()]: {
content: {},
metadata: { model: FAKE_MODEL_ID2 },
},
[FAKE_MID_ID3.toString()]: {
content: {},
metadata: { model: FAKE_MODEL_IMPLEMENTS_RELATION_ID },
},
}

describe('ModelInstanceDocumentHandler', () => {
let did: DID
let handler: ModelInstanceDocumentHandler
Expand Down Expand Up @@ -292,26 +465,13 @@ describe('ModelInstanceDocumentHandler', () => {
return ['fakechain:123']
}),
loadStream: jest.fn(async (streamId: StreamID) => {
if (streamId.toString() === FAKE_MODEL_ID.toString()) {
return {
content: MODEL_DEFINITION,
commitId: FAKE_MODEL_ID,
}
} else if (streamId.toString() === FAKE_MODEL_ID2.toString()) {
return {
content: MODEL_DEFINITION_SINGLE,
commitId: FAKE_MODEL_ID2,
}
} else if (streamId.toString() == FAKE_MODEL_IDBLOB.toString()) {
return {
content: MODEL_DEFINITION_BLOB,
commitId: FAKE_MODEL_IDBLOB,
}
} else {
const stream = STREAMS[streamId.toString()]
if (stream == null) {
throw new Error(
'Trying to load unexpected stream in model-instance-document-handler.test.ts'
)
}
return stream
}),
did,
}
Expand Down Expand Up @@ -1081,4 +1241,132 @@ describe('ModelInstanceDocumentHandler', () => {

expect(state).toMatchSnapshot()
})

test('throws when trying to create a MID with an interface model', async () => {
const commit = (await ModelInstanceDocument._makeGenesis(signerUsingNewKey, CONTENT0, {
controller: METADATA.controller,
model: FAKE_MODEL_INTERFACE_ID,
})) as SignedCommitContainer
await context.ipfs.dag.put(commit, FAKE_CID_1)

const payload = dagCBOR.decode(commit.linkedBlock)
await context.ipfs.dag.put(payload, commit.jws.link)

const commitData = {
cid: FAKE_CID_1,
type: CommitType.GENESIS,
commit: payload,
envelope: commit.jws,
}
await expect(handler.applyCommit(commitData, context)).rejects.toThrow(
`ModelInstanceDocument Streams cannot be created on interface Models. Use a different model than ${FAKE_MODEL_INTERFACE_ID.toString()} to create the ModelInstanceDocument.`
)
})

test('validates relations with required model - throws if invalid', async () => {
const genesisCommit = (await ModelInstanceDocument._makeGenesis(
signerUsingNewKey,
{ myData: 3, relationID: FAKE_MID_ID2.toString() },
{ controller: METADATA.controller, model: FAKE_MODEL_REQUIRED_RELATION_ID }
)) as SignedCommitContainer
await context.ipfs.dag.put(genesisCommit, FAKE_CID_1)

const payload = dagCBOR.decode(genesisCommit.linkedBlock)
await context.ipfs.dag.put(payload, genesisCommit.jws.link)

// apply genesis
const genesisCommitData = {
cid: FAKE_CID_1,
type: CommitType.GENESIS,
commit: payload,
envelope: genesisCommit.jws,
}
await expect(handler.applyCommit(genesisCommitData, context)).rejects.toThrow(
`Relation on field relationID points to Stream ${FAKE_MID_ID2.toString()}, which belongs to Model ${FAKE_MODEL_ID2.toString()}, but this Stream's Model (${FAKE_MODEL_REQUIRED_RELATION_ID.toString()}) specifies that this relation must be to a Stream in the Model ${FAKE_MODEL_ID.toString()}`
)
})

test('validates relations with required model - model match', async () => {
const genesisCommit = (await ModelInstanceDocument._makeGenesis(
signerUsingNewKey,
{ myData: 3, relationID: FAKE_MID_ID.toString() },
{ controller: METADATA.controller, model: FAKE_MODEL_REQUIRED_RELATION_ID }
)) as SignedCommitContainer
await context.ipfs.dag.put(genesisCommit, FAKE_CID_1)

const payload = dagCBOR.decode(genesisCommit.linkedBlock)
await context.ipfs.dag.put(payload, genesisCommit.jws.link)

// apply genesis
const genesisCommitData = {
cid: FAKE_CID_1,
type: CommitType.GENESIS,
commit: payload,
envelope: genesisCommit.jws,
}
await expect(handler.applyCommit(genesisCommitData, context)).resolves
})

test('validates relations with optional model - linked MID not provided', async () => {
const genesisCommit = (await ModelInstanceDocument._makeGenesis(
signerUsingNewKey,
{ myData: 3 },
{ controller: METADATA.controller, model: FAKE_MODEL_OPTIONAL_RELATION_ID }
)) as SignedCommitContainer
await context.ipfs.dag.put(genesisCommit, FAKE_CID_1)

const payload = dagCBOR.decode(genesisCommit.linkedBlock)
await context.ipfs.dag.put(payload, genesisCommit.jws.link)

// apply genesis
const genesisCommitData = {
cid: FAKE_CID_1,
type: CommitType.GENESIS,
commit: payload,
envelope: genesisCommit.jws,
}
await expect(handler.applyCommit(genesisCommitData, context)).resolves
})

test('validates relations with optional model - linked MID provided', async () => {
const genesisCommit = (await ModelInstanceDocument._makeGenesis(
signerUsingNewKey,
{ myData: 3, relationID: FAKE_MID_ID2.toString() },
{ controller: METADATA.controller, model: FAKE_MODEL_OPTIONAL_RELATION_ID }
)) as SignedCommitContainer
await context.ipfs.dag.put(genesisCommit, FAKE_CID_1)

const payload = dagCBOR.decode(genesisCommit.linkedBlock)
await context.ipfs.dag.put(payload, genesisCommit.jws.link)

// apply genesis
const genesisCommitData = {
cid: FAKE_CID_1,
type: CommitType.GENESIS,
commit: payload,
envelope: genesisCommit.jws,
}
await expect(handler.applyCommit(genesisCommitData, context)).resolves
})

test('validates relations with interface model', async () => {
const genesisCommit = (await ModelInstanceDocument._makeGenesis(
signerUsingNewKey,
{ myData: 3, relationID: FAKE_MID_ID3.toString() },
{ controller: METADATA.controller, model: FAKE_MODEL_INTERFACE_RELATION_ID }
)) as SignedCommitContainer
await context.ipfs.dag.put(genesisCommit, FAKE_CID_1)

const payload = dagCBOR.decode(genesisCommit.linkedBlock)
await context.ipfs.dag.put(payload, genesisCommit.jws.link)

// apply genesis
const genesisCommitData = {
cid: FAKE_CID_1,
type: CommitType.GENESIS,
commit: payload,
envelope: genesisCommit.jws,
}
await expect(handler.applyCommit(genesisCommitData, context)).resolves
})
})
Loading

0 comments on commit e6f71df

Please sign in to comment.