Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Removes log from DocumentState #40

Merged
merged 2 commits into from
Dec 12, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
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
50 changes: 24 additions & 26 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 @@ -145,29 +143,29 @@ export function assertValidUniqueValue(
* 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')
}
// 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]
// 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}`,
)
}
// // 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}`,
)
}
}
// 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
81 changes: 40 additions & 41 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,46 +15,46 @@ 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('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}`,
// )
// })
// })
Copy link
Collaborator

Choose a reason for hiding this comment

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

Same here, lets just delete the code.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done!


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