-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
15 changed files
with
442 additions
and
209 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
import { | ||
DeterministicInitEventPayload, | ||
DocumentDataEventPayload, | ||
DocumentInitEventPayload, | ||
} from '@ceramic-sdk/document-protocol' | ||
import { SignedEvent, TimeEvent } from '@ceramic-sdk/events' | ||
import { type TypeOf, union } from 'codeco' | ||
import 'ts-essentials' // Import needed for TS reference | ||
|
||
export const DocumentEvent = union( | ||
[ | ||
DeterministicInitEventPayload, | ||
SignedEvent, // non-deterministic init or data payload | ||
TimeEvent, | ||
], | ||
'DocumentEvent', | ||
) | ||
export type DocumentEvent = TypeOf<typeof DocumentEvent> | ||
|
||
export const DocumentEventPayload = union( | ||
[ | ||
DeterministicInitEventPayload, | ||
DocumentInitEventPayload, | ||
DocumentDataEventPayload, | ||
TimeEvent, | ||
], | ||
'DocumentEventPayload', | ||
) | ||
export type DocumentEventPayload = TypeOf<typeof DocumentEventPayload> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,67 +1,163 @@ | ||
import { | ||
type DeterministicInitEventPayload, | ||
DocumentDataEventPayload, | ||
DocumentInitEventPayload, | ||
DocumentMetadata, | ||
type EncodedDocumentMetadata, | ||
assertValidContentLength, | ||
getDeterministicStreamID, | ||
getStreamID, | ||
} from '@ceramic-sdk/document-protocol' | ||
import { eventToContainer } from '@ceramic-sdk/events' | ||
import type { StreamID } from '@ceramic-sdk/identifiers' | ||
import { TimeEvent, eventToContainer } from '@ceramic-sdk/events' | ||
import jsonpatch from 'fast-json-patch' | ||
import type { CID } from 'multiformats/cid' | ||
|
||
import { assertValidContent, assertValidInitHeader } from './assertions.js' | ||
import { | ||
assertEventLinksToState, | ||
assertNoImmutableFieldChange, | ||
assertValidContent, | ||
assertValidInitHeader, | ||
assertValidUniqueValue, | ||
} from './assertions.js' | ||
import { type DocumentEvent, DocumentEventPayload } from './codecs.js' | ||
import type { Context, DocumentState } from './types.js' | ||
import { getImmutableFieldsToCheck } from './utils.js' | ||
import { validateRelationsContent } from './validation.js' | ||
|
||
export async function handleInitEvent( | ||
event: unknown, | ||
export async function handleDeterministicInitPayload( | ||
cid: CID, | ||
payload: DeterministicInitEventPayload, | ||
context: Context, | ||
): Promise<DocumentState> { | ||
const container = await eventToContainer( | ||
context.verifier, | ||
DocumentInitEventPayload, | ||
event, | ||
) | ||
const { data, header } = container.payload | ||
const { data, header } = payload | ||
if (data !== null) { | ||
throw new Error( | ||
'Deterministic init commits for ModelInstanceDocuments must not have content', | ||
) | ||
} | ||
|
||
const modelID = header.model.toString() | ||
const definition = await context.loadModelDefinition(modelID) | ||
const definition = await context.getModelDefinition(modelID) | ||
assertValidInitHeader(definition, header) | ||
|
||
let streamID: StreamID | ||
let metadata: EncodedDocumentMetadata | ||
if (container.signed) { | ||
// Handle non-deterministic event - should have content | ||
assertValidContentLength(data) | ||
assertValidContent(modelID, definition.schema, data) | ||
await validateRelationsContent(context, definition, data) | ||
streamID = getStreamID(container.cid) | ||
metadata = DocumentMetadata.encode({ | ||
return { | ||
cid, | ||
content: null, | ||
metadata: { | ||
controller: header.controllers[0], | ||
model: header.model, | ||
unique: header.unique, | ||
}, | ||
log: [payload], | ||
} | ||
} | ||
|
||
export async function handleInitPayload( | ||
cid: CID, | ||
payload: DocumentInitEventPayload, | ||
context: Context, | ||
): Promise<DocumentState> { | ||
const { data, header } = payload | ||
assertValidContentLength(data) | ||
|
||
const modelID = header.model.toString() | ||
const definition = await context.getModelDefinition(modelID) | ||
assertValidInitHeader(definition, header) | ||
|
||
assertValidContent(modelID, definition.schema, data) | ||
await validateRelationsContent(context, definition, data) | ||
|
||
return { | ||
cid, | ||
content: data, | ||
metadata: { | ||
controller: header.controllers[0], | ||
model: header.model, | ||
unique: header.unique, | ||
context: header.context, | ||
shouldIndex: header.shouldIndex, | ||
}) | ||
} else { | ||
// Handle deterministic event - no content | ||
if (data !== null) { | ||
}, | ||
log: [payload], | ||
} | ||
} | ||
|
||
export async function handleDataPayload( | ||
payload: DocumentDataEventPayload, | ||
context: Context, | ||
): Promise<DocumentState> { | ||
const state = await context.getDocumentState(payload.id) | ||
assertEventLinksToState(payload, state) | ||
|
||
// Check the header is valid when provided | ||
if (payload.header != null) { | ||
const { shouldIndex, ...others } = payload.header | ||
const otherKeys = Object.keys(others) | ||
if (otherKeys.length) { | ||
throw new Error( | ||
'Deterministic init commits for ModelInstanceDocuments must not have content', | ||
`Updating metadata for ModelInstanceDocument Streams is not allowed. Tried to change metadata for Stream ${getStreamID(payload.id)} from ${JSON.stringify( | ||
state.metadata, | ||
)} to ${JSON.stringify(payload.header)}`, | ||
) | ||
} | ||
streamID = getDeterministicStreamID(header) | ||
metadata = DocumentMetadata.encode({ | ||
controller: header.controllers[0], | ||
model: header.model, | ||
unique: header.unique, | ||
}) | ||
// Update metadata if needed | ||
if (shouldIndex != null) { | ||
state.metadata.shouldIndex = shouldIndex | ||
} | ||
} | ||
|
||
return { | ||
id: streamID.toString(), | ||
content: data, | ||
metadata, | ||
log: [event], | ||
// Get updated content by applying the patch to the current content | ||
const content = jsonpatch.applyPatch( | ||
state.content ?? {}, | ||
payload.data, | ||
).newDocument | ||
assertValidContentLength(content) | ||
|
||
// Load the model definition for the document and validate the content | ||
const modelID = state.metadata.model.toString() | ||
const definition = await context.getModelDefinition(modelID) | ||
assertValidContent(modelID, definition.schema, content) | ||
|
||
// Check the content satifies the SET account relations and immutable fields constraints | ||
assertValidUniqueValue(definition, state.metadata, content) | ||
const immutableFields = getImmutableFieldsToCheck(definition, state) | ||
if (immutableFields != null) { | ||
assertNoImmutableFieldChange(payload.data, immutableFields) | ||
} | ||
|
||
// Validate relations | ||
await validateRelationsContent(context, definition, content) | ||
|
||
return { ...state, log: [...state.log, payload] } | ||
} | ||
|
||
export async function handleTimeEvent( | ||
event: TimeEvent, | ||
context: Context, | ||
): Promise<DocumentState> { | ||
const state = await context.getDocumentState(event.id) | ||
assertEventLinksToState(event, state) | ||
return { ...state, log: [...state.log, event] } | ||
} | ||
|
||
export async function handleEvent( | ||
cid: CID, | ||
event: DocumentEvent, | ||
context: Context, | ||
): Promise<DocumentState> { | ||
const container = await eventToContainer( | ||
context.verifier, | ||
DocumentEventPayload, | ||
event, | ||
) | ||
if (container.signed) { | ||
// Signed event is either non-deterministic init or data | ||
if (DocumentDataEventPayload.is(container.payload)) { | ||
return await handleDataPayload(container.payload, context) | ||
} | ||
if (DocumentInitEventPayload.is(container.payload)) { | ||
return await handleInitPayload(cid, container.payload, context) | ||
} | ||
} | ||
// Unsigned event is either deterministic init or time | ||
if (TimeEvent.is(container.payload)) { | ||
return await handleTimeEvent(container.payload as TimeEvent, context) | ||
} | ||
return await handleDeterministicInitPayload(cid, container.payload, context) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,21 +1,34 @@ | ||
import type { EncodedDocumentMetadata } from '@ceramic-sdk/document-protocol' | ||
import type { | ||
DeterministicInitEventPayload, | ||
DocumentDataEventPayload, | ||
DocumentInitEventPayload, | ||
DocumentMetadata, | ||
} from '@ceramic-sdk/document-protocol' | ||
import type { TimeEvent } from '@ceramic-sdk/events' | ||
import type { ModelDefinition } from '@ceramic-sdk/model-protocol' | ||
import type { DID } from 'dids' | ||
import type { CID } from 'multiformats/cid' | ||
|
||
export type InitEventPayload = | ||
| DeterministicInitEventPayload | ||
| DocumentInitEventPayload | ||
export type ChangeEventPayload = DocumentDataEventPayload | TimeEvent | ||
|
||
export type UnknowContent = Record<string, unknown> | ||
|
||
export type DocumentSnapshot = { | ||
content: UnknowContent | null | ||
metadata: EncodedDocumentMetadata | ||
metadata: DocumentMetadata | ||
} | ||
|
||
export type DocumentState = DocumentSnapshot & { | ||
id: string | ||
log: Array<unknown> // TODO: events | ||
cid: CID | ||
log: [InitEventPayload, ...Array<ChangeEventPayload>] | ||
} | ||
|
||
export type Context = { | ||
loadDocument: (id: string) => Promise<DocumentSnapshot> | ||
loadModelDefinition: (id: string) => Promise<ModelDefinition> | ||
getDocumentSnapshot: (id: string) => Promise<DocumentSnapshot> | ||
getDocumentState: (cid: CID) => Promise<DocumentState> | ||
getModelDefinition: (id: string) => Promise<ModelDefinition> | ||
verifier: DID | ||
} |
Oops, something went wrong.