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

feat: update how we check immutability #3188

Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ exports[`ModelInstanceDocumentHandler MIDs with SET account relation validate si
"content": {
"myData": 2,
"one": "foo",
"three": "three",
"two": "bar",
},
"log": [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,7 @@ const MODEL_DEFINITION_SET: ModelDefinition = {
properties: {
one: { type: 'string', minLength: 2 },
two: { type: 'string', minLength: 2 },
three: { type: 'string', minLength: 2 },
myData: {
type: 'integer',
maximum: 100,
Expand All @@ -197,6 +198,7 @@ const MODEL_DEFINITION_SET: ModelDefinition = {
},
required: ['myData'],
},
immutableFields: ['three'],
}

const MODEL_DEFINITION_BLOB: ModelDefinition = {
Expand Down Expand Up @@ -893,7 +895,7 @@ describe('ModelInstanceDocumentHandler', () => {
context.signer,
doc.commitId,
doc.content,
{ one: 'foo', two: 'bar', myData: 2 }
{ one: 'foo', two: 'bar', three: 'three', myData: 2 }
)) as SignedCommitContainer
await ipfs.dag.put(signedCommitOK, FAKE_CID_3)

Expand Down Expand Up @@ -926,7 +928,7 @@ describe('ModelInstanceDocumentHandler', () => {
commit: genesisCommit,
}

// The deterministic genesis creation works independently of content validation as determinitic commits have no content
// The deterministic genesis creation works independently of content validation as deterministic commits have no content
const state = await handler.applyCommit(genesisCommitData, context)
const state$ = TestUtils.runningState(state)
const doc = new ModelInstanceDocument(state$, context)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,7 @@ export class ModelInstanceDocumentHandler implements StreamHandler<ModelInstance
state: StreamState<ModelInstanceDocumentStateMetadata>,
context: StreamReaderWriter
): Promise<StreamState> {
const deterministicTypes = ['set', 'single']
// Retrieve the payload
const payload = commitData.commit
StreamUtils.assertCommitLinksToState(state, payload)
Expand Down Expand Up @@ -196,7 +197,17 @@ export class ModelInstanceDocumentHandler implements StreamHandler<ModelInstance
const oldContent = state.content ?? {}
const newContent = jsonpatch.applyPatch(oldContent, payload.data).newDocument
const modelStream = await context.loadStream<Model>(metadata.model)
await this._validateContent(context, modelStream, newContent, false, payload)
const isDetType = deterministicTypes.includes(modelStream.content.accountRelation.type)
const isFirstDataCommit = !state.log.some((c) => c.type === EventType.DATA)

await this._validateContent(
context,
modelStream,
newContent,
false,
payload,
isDetType && isFirstDataCommit
)
await this._validateUnique(
modelStream,
metadata as unknown as ModelInstanceDocumentMetadata,
Expand Down Expand Up @@ -238,16 +249,18 @@ export class ModelInstanceDocumentHandler implements StreamHandler<ModelInstance
* Validates content against the schema of the model stream with given stream id
* @param ceramic - Interface for reading streams from ceramic network
* @param model - The model that this ModelInstanceDocument belongs to
* @param content - content to validate
* @param genesis - whether the commit being applied is a genesis commit
* @param content - Content to validate
* @param genesis - Whether the commit being applied is a genesis commit
* @param skipImmutableFieldsCheck - Whether the incoming commit is the first data commit for a model with deterministic creation (Optional)
* @private
*/
async _validateContent(
ceramic: StreamReader,
model: Model,
content: any,
genesis: boolean,
payload?: Payload
payload?: Payload,
skipImmutableFieldsCheck?: boolean
): Promise<void> {
if (
genesis &&
Expand All @@ -268,7 +281,7 @@ export class ModelInstanceDocumentHandler implements StreamHandler<ModelInstance

// Now validate the relations
await this._validateRelationsContent(ceramic, model, content)
if (!genesis && payload) {
if (!genesis && payload && !skipImmutableFieldsCheck) {
await this._validateLockedFieldsUpdate(model, payload)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,22 +39,51 @@ const MODEL_DEFINITION: ModelDefinition = {
}

const MODEL_DEFINITION_SINGLE: ModelDefinition = {
name: 'MySingleModel',
version: '1.0',
name: 'MyModel',
version: '2.0',
interface: false,
implements: [],
accountRelation: { type: 'single' },
schema: {
$schema: 'https://json-schema.org/draft/2020-12/schema',
type: 'object',
additionalProperties: false,
properties: {
one: { type: 'string', minLength: 2 },
myData: {
type: 'integer',
maximum: 10000,
maximum: 100,
minimum: 0,
},
},
required: ['myData'],
},
immutableFields: ['one'],
}

const MODEL_DEFINITION_SET: ModelDefinition = {
name: 'MyModel',
version: '2.0',
interface: false,
implements: [],
accountRelation: { type: 'set', fields: ['one', 'two'] },
schema: {
$schema: 'https://json-schema.org/draft/2020-12/schema',
type: 'object',
additionalProperties: false,
properties: {
one: { type: 'string', minLength: 2 },
two: { type: 'string', minLength: 2 },
three: { type: 'string', minLength: 2 },
myData: {
type: 'integer',
maximum: 100,
minimum: 0,
},
},
required: ['one', 'two'],
},
immutableFields: ['three'],
}

// The model above will always result in this StreamID when created with the fixed did:key
Expand Down Expand Up @@ -97,6 +126,7 @@ describe('ModelInstanceDocument API http-client tests', () => {
let midRelationMetadata: ModelInstanceDocumentMetadataArgs
let modelSingle: Model
let midSingleMetadata: ModelInstanceDocumentMetadataArgs
let modelSet: Model

beforeAll(async () => {
ipfs = await createIPFS()
Expand Down Expand Up @@ -129,6 +159,7 @@ describe('ModelInstanceDocument API http-client tests', () => {
midRelationMetadata = { model: modelWithRelation.id }
modelSingle = await Model.create(ceramic, MODEL_DEFINITION_SINGLE)
midSingleMetadata = { model: modelSingle.id }
modelSet = await Model.create(ceramic, MODEL_DEFINITION_SET)

await core.index.indexModels([{ streamID: model.id }])
}, 12000)
Expand Down Expand Up @@ -224,6 +255,47 @@ describe('ModelInstanceDocument API http-client tests', () => {
expect(doc.state.log[1].type).toEqual(EventType.DATA)
})

test('Can upsert immutable fields in a set/single relation model', async () => {
// set
const doc = await ModelInstanceDocument.set(
ceramic,
{ controller: ceramic.did!.id, model: modelSet.id },
['foo', 'bar']
)

expect(doc.content).toBeNull()
const newContent = { one: 'foo', two: 'bar', three: 'foobar', myData: 1 }
await doc.replace(newContent)

expect(doc.content).toEqual(newContent)
expect(doc.state.log.length).toEqual(2)
expect(doc.state.log[0].type).toEqual(EventType.INIT)
expect(doc.state.log[1].type).toEqual(EventType.DATA)

//second update
newContent.three = 'barfoo'
await expect(doc.replace(newContent)).rejects.toThrow(
new RegExp(`.*Immutable field \\\\\\"three\\\\\\" cannot be updated.*`)
)

// single
JulissaDantes marked this conversation as resolved.
Show resolved Hide resolved
const singleDoc = await ModelInstanceDocument.single(ceramic, midSingleMetadata)
expect(singleDoc.content).toBeNull()
const singleNewContent = { one: 'foo', myData: 1 }
await singleDoc.replace(singleNewContent)

expect(singleDoc.content).toEqual(singleNewContent)
expect(singleDoc.state.log.length).toEqual(2)
expect(singleDoc.state.log[0].type).toEqual(EventType.INIT)
expect(singleDoc.state.log[1].type).toEqual(EventType.DATA)

//second update
singleNewContent.one = 'barfoo'
await expect(singleDoc.replace(singleNewContent)).rejects.toThrow(
new RegExp(`.*Immutable field \\\\\\"one\\\\\\" cannot be updated.*`)
)
})

test(`Cannot create document with relation that isn't a valid streamid`, async () => {
const relationContent = { linkedDoc: 'this is a streamid' }
await expect(
Expand Down