diff --git a/packages/stream-model-instance-handler/src/__tests__/__snapshots__/model-instance-document-handler.test.ts.snap b/packages/stream-model-instance-handler/src/__tests__/__snapshots__/model-instance-document-handler.test.ts.snap index f4e75d2b03..e930b2486a 100644 --- a/packages/stream-model-instance-handler/src/__tests__/__snapshots__/model-instance-document-handler.test.ts.snap +++ b/packages/stream-model-instance-handler/src/__tests__/__snapshots__/model-instance-document-handler.test.ts.snap @@ -6,6 +6,7 @@ exports[`ModelInstanceDocumentHandler MIDs with SET account relation validate si "content": { "myData": 2, "one": "foo", + "three": "three", "two": "bar", }, "log": [ diff --git a/packages/stream-model-instance-handler/src/__tests__/model-instance-document-handler.test.ts b/packages/stream-model-instance-handler/src/__tests__/model-instance-document-handler.test.ts index dcfad3b9be..aed413d5bf 100644 --- a/packages/stream-model-instance-handler/src/__tests__/model-instance-document-handler.test.ts +++ b/packages/stream-model-instance-handler/src/__tests__/model-instance-document-handler.test.ts @@ -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, @@ -197,6 +198,7 @@ const MODEL_DEFINITION_SET: ModelDefinition = { }, required: ['myData'], }, + immutableFields: ['three'], } const MODEL_DEFINITION_BLOB: ModelDefinition = { @@ -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) @@ -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) diff --git a/packages/stream-model-instance-handler/src/model-instance-document-handler.ts b/packages/stream-model-instance-handler/src/model-instance-document-handler.ts index 34863ed203..f2a11fd9f6 100644 --- a/packages/stream-model-instance-handler/src/model-instance-document-handler.ts +++ b/packages/stream-model-instance-handler/src/model-instance-document-handler.ts @@ -161,6 +161,7 @@ export class ModelInstanceDocumentHandler implements StreamHandler, context: StreamReaderWriter ): Promise { + const deterministicTypes = ['set', 'single'] // Retrieve the payload const payload = commitData.commit StreamUtils.assertCommitLinksToState(state, payload) @@ -196,7 +197,17 @@ export class ModelInstanceDocumentHandler implements StreamHandler(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, @@ -238,8 +249,9 @@ export class ModelInstanceDocumentHandler implements StreamHandler { if ( genesis && @@ -268,7 +281,7 @@ export class ModelInstanceDocumentHandler implements StreamHandler { let midRelationMetadata: ModelInstanceDocumentMetadataArgs let modelSingle: Model let midSingleMetadata: ModelInstanceDocumentMetadataArgs + let modelSet: Model beforeAll(async () => { ipfs = await createIPFS() @@ -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) @@ -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 + 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(