Skip to content

Commit

Permalink
Removes log from DocumentState (#40)
Browse files Browse the repository at this point in the history
  • Loading branch information
mzkrasner authored Dec 12, 2024
1 parent 32d429a commit 720f22c
Show file tree
Hide file tree
Showing 20 changed files with 130 additions and 257 deletions.
13 changes: 12 additions & 1 deletion packages/model-client/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
SignedEvent,
createSignedInitEvent,
decodeMultibaseToJSON,
decodeMultibaseToStreamID,
eventToContainer,
} from '@ceramic-sdk/events'
import { StreamID } from '@ceramic-sdk/identifiers'
Expand Down Expand Up @@ -69,11 +70,21 @@ export class ModelClient extends StreamClient {
return getModelStreamID(cid)
}

/** Retrieve the stringified model stream ID from a stream */
async getDocumentModel(streamID: StreamID | string): Promise<string> {
const id =
typeof streamID === 'string' ? StreamID.fromString(streamID) : streamID
const streamState = await this.getStreamState(id)
const stream = decodeMultibaseToStreamID(streamState.dimensions.model)
return stream.toString()
}

/** Retrieve a model's JSON definition */
async getModelDefinition(
streamID: StreamID | string,
): Promise<ModelDefinition> {
const id = typeof streamID === 'string' ? streamID : streamID.toString() // Convert StreamID to string
const id =
typeof streamID === 'string' ? StreamID.fromString(streamID) : streamID
const streamState = await this.getStreamState(id)
const decodedData = decodeMultibaseToJSON(streamState.data)
.content as ModelDefinition
Expand Down
12 changes: 8 additions & 4 deletions packages/model-instance-client/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import {
decodeMultibaseToJSON,
decodeMultibaseToStreamID,
} from '@ceramic-sdk/events'
import { CommitID, type StreamID } from '@ceramic-sdk/identifiers'
import { CommitID, StreamID } from '@ceramic-sdk/identifiers'
import {
DocumentEvent,
getStreamID,
Expand Down Expand Up @@ -120,8 +120,10 @@ export class ModelInstanceClient extends StreamClient {
}

/** Retrieve and return document state */
async getDocumentState(streamID: string): Promise<DocumentState> {
const streamState = await this.getStreamState(streamID)
async getDocumentState(streamID: StreamID | string): Promise<DocumentState> {
const id =
typeof streamID === 'string' ? StreamID.fromString(streamID) : streamID
const streamState = await this.getStreamState(id)
return this.streamStateToDocumentState(streamState)
}

Expand All @@ -133,7 +135,9 @@ export class ModelInstanceClient extends StreamClient {
let currentId: CommitID
// If currentState is not provided, fetch the current state
if (!params.currentState) {
const streamState = await this.getStreamState(params.streamID)
const streamState = await this.getStreamState(
StreamID.fromString(params.streamID),
)
currentState = this.streamStateToDocumentState(streamState)
currentId = this.getCurrentID(streamState.event_cid)
} else {
Expand Down
11 changes: 9 additions & 2 deletions packages/model-instance-client/test/lib.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import { assertSignedEvent, getSignedEventPayload } from '@ceramic-sdk/events'
import type { CeramicClient } from '@ceramic-sdk/http-client'
import { CommitID, randomCID, randomStreamID } from '@ceramic-sdk/identifiers'
import {
CommitID,
StreamID,
randomCID,
randomStreamID,
} from '@ceramic-sdk/identifiers'
import {
DataInitEventPayload,
DocumentDataEventPayload,
Expand Down Expand Up @@ -250,7 +255,9 @@ describe('ModelInstanceClient', () => {
} as unknown as CeramicClient
const client = new ModelInstanceClient({ ceramic, did: authenticatedDID })

const documentState = await client.getDocumentState(streamId)
const documentState = await client.getDocumentState(
StreamID.fromString(streamId),
)
expect(documentState.content).toEqual(docState.content)
expect(documentState.metadata.model.toString()).toEqual(
docState.metadata.model,
Expand Down
40 changes: 1 addition & 39 deletions packages/model-instance-handler/src/assertions.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
import type { TimeEvent } from '@ceramic-sdk/events'
import type {
DocumentDataEventPayload,
DocumentInitEventHeader,
DocumentMetadata,
JSONPatchOperation,
Expand All @@ -10,7 +8,7 @@ import addFormats from 'ajv-formats'
import Ajv from 'ajv/dist/2020.js'
import { equals } from 'uint8arrays'

import type { DocumentState, UnknownContent } from './types.js'
import type { UnknownContent } from './types.js'
import { getUniqueFieldsValue } from './utils.js'

const validator = new Ajv({
Expand Down Expand Up @@ -135,39 +133,3 @@ export function assertValidUniqueValue(
)
}
}

/**
* Asserts that the 'id' and 'prev' properties of the given event properly link to the tip of
* the given document state.
*
* By the time the code gets into a StreamtypeHandler's applyCommit function the link to the state
* should already have been established by the stream loading and conflict resolution code, so
* if this check were to fail as part of a StreamtypeHandler's applyCommit function, that would
* indicate a programming error.
*/
export function assertEventLinksToState(
payload: DocumentDataEventPayload | TimeEvent,
state: DocumentState,
) {
if (state.log.length === 0) {
throw new Error('Invalid document state: log is empty')
}

const initCID = state.log[0]

// Older versions of the CAS created time events without an 'id' field, so only check
// the event payload 'id' field if it is present.
if (payload.id != null && payload.id.toString() !== initCID) {
throw new Error(
`Invalid init CID in event payload for document, expected ${initCID} but got ${payload.id}`,
)
}

const prev = payload.prev.toString()
const expectedPrev = state.log[state.log.length - 1]
if (prev !== expectedPrev) {
throw new Error(
`Commit doesn't properly point to previous event payload in log for document ${initCID}. Expected ${expectedPrev}, found 'prev' ${prev}`,
)
}
}
26 changes: 8 additions & 18 deletions packages/model-instance-handler/src/handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import {
import jsonpatch from 'fast-json-patch'

import {
assertEventLinksToState,
assertNoImmutableFieldChange,
assertValidContent,
assertValidInitHeader,
Expand All @@ -22,7 +21,6 @@ import { getImmutableFieldsToCheck } from './utils.js'
import { validateRelationsContent } from './validation.js'

function createInitState(
cid: string,
header: DocumentInitEventHeader,
content: Record<string, unknown> | null,
): DocumentState {
Expand All @@ -35,12 +33,10 @@ function createInitState(
context: header.context,
shouldIndex: header.shouldIndex,
},
log: [cid],
}
}

export async function handleDeterministicInitPayload(
cid: string,
payload: DeterministicInitEventPayload,
context: Context,
): Promise<DocumentState> {
Expand All @@ -55,11 +51,10 @@ export async function handleDeterministicInitPayload(
const definition = await context.getModelDefinition(modelID)
assertValidInitHeader(definition, header)

return createInitState(cid, header, null)
return createInitState(header, null)
}

export async function handleInitPayload(
cid: string,
payload: DocumentInitEventPayload,
context: Context,
): Promise<DocumentState> {
Expand All @@ -78,17 +73,15 @@ export async function handleInitPayload(
assertValidContent(modelID, definition.schema, data)
await validateRelationsContent(context, definition, data)

return createInitState(cid, header, data)
return createInitState(header, data)
}

export async function handleDataPayload(
cid: string,
payload: DocumentDataEventPayload,
context: Context,
): Promise<DocumentState> {
const streamID = getStreamID(payload.id).toString()
const state = await context.getDocumentState(streamID)
assertEventLinksToState(payload, state)

const metadata = { ...state.metadata }

Expand Down Expand Up @@ -131,22 +124,19 @@ export async function handleDataPayload(
// Validate relations
await validateRelationsContent(context, definition, content)

return { content, metadata, log: [...state.log, cid] }
return { content, metadata }
}

export async function handleTimeEvent(
cid: string,
event: TimeEvent,
context: Context,
): Promise<DocumentState> {
const streamID = getStreamID(event.id).toString()
const state = await context.getDocumentState(streamID)
assertEventLinksToState(event, state)
return { ...state, log: [...state.log, cid] }
return { ...state }
}

export async function handleEvent(
cid: string,
event: DocumentEvent,
context: Context,
): Promise<DocumentState> {
Expand All @@ -158,15 +148,15 @@ export async function handleEvent(
if (container.signed) {
// Signed event is either non-deterministic init or data
if (DocumentDataEventPayload.is(container.payload)) {
return await handleDataPayload(cid, container.payload, context)
return await handleDataPayload(container.payload, context)
}
if (DocumentInitEventPayload.is(container.payload)) {
return await handleInitPayload(cid, container.payload, context)
return await handleInitPayload(container.payload, context)
}
}
// Unsigned event is either deterministic init or time
if (TimeEvent.is(container.payload)) {
return await handleTimeEvent(cid, container.payload as TimeEvent, context)
return await handleTimeEvent(container.payload as TimeEvent, context)
}
return await handleDeterministicInitPayload(cid, container.payload, context)
return await handleDeterministicInitPayload(container.payload, context)
}
1 change: 0 additions & 1 deletion packages/model-instance-handler/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ export type UnknownContent = Record<string, unknown>
export type DocumentState = {
content: UnknownContent | null
metadata: DocumentMetadata
log: [string, ...Array<string>]
}

export type Context = {
Expand Down
5 changes: 1 addition & 4 deletions packages/model-instance-handler/src/utils.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { DocumentDataEventPayload } from '@ceramic-sdk/model-instance-protocol'
import type { ModelDefinition } from '@ceramic-sdk/model-protocol'
import { fromString as bytesFromString } from 'uint8arrays'

Expand All @@ -22,9 +21,7 @@ export function getImmutableFieldsToCheck(
// Check if the stream is deterministic
if (['set', 'single'].includes(definition.accountRelation.type)) {
// Should check immutable fields if there is already a data event present, otherwise it is the first data event that sets the content of the deterministic stream
return state.log.some(DocumentDataEventPayload.is)
? definition.immutableFields
: []
return state.content != null ? definition.immutableFields : []
}

// Should check immutable fields for all data events on non-deterministic streams
Expand Down
42 changes: 0 additions & 42 deletions packages/model-instance-handler/test/assertions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import type {
import type { ModelDefinitionV2 } from '@ceramic-sdk/model-protocol'

import {
assertEventLinksToState,
assertNoImmutableFieldChange,
assertValidContent,
assertValidInitHeader,
Expand All @@ -16,47 +15,6 @@ import {
import type { DocumentState } from '../src/types.js'
import { encodeUniqueFieldsValue } from '../src/utils.js'

describe('assertEventLinksToState()', () => {
test('throws if the state log is empty', () => {
const cid = randomCID()
expect(() => {
assertEventLinksToState(
{ id: cid } as unknown as DocumentDataEventPayload,
{ log: [] } as unknown as DocumentState,
)
}).toThrow('Invalid document state: log is empty')
})

test('throws if the event id does not match the init event cid', () => {
const expectedID = randomCID().toString()
const invalidID = randomCID()
expect(() => {
assertEventLinksToState(
{ id: invalidID } as unknown as DocumentDataEventPayload,
{ log: [expectedID] } as unknown as DocumentState,
)
}).toThrow(
`Invalid init CID in event payload for document, expected ${expectedID} but got ${invalidID}`,
)
})

test('throws if the event prev does not match the previous event cid', () => {
const initID = randomCID()
const expectedID = randomCID()
const invalidID = randomCID()
expect(() => {
assertEventLinksToState(
{ id: initID, prev: invalidID } as unknown as DocumentDataEventPayload,
{
log: [initID.toString(), expectedID.toString()],
} as unknown as DocumentState,
)
}).toThrow(
`Commit doesn't properly point to previous event payload in log for document ${initID}. Expected ${expectedID}, found 'prev' ${invalidID}`,
)
})
})

describe('assertNoImmutableFieldChange()', () => {
test('throws if an immutable field is changed', () => {
expect(() => {
Expand Down
Loading

0 comments on commit 720f22c

Please sign in to comment.