From 1653dbce2e90eba0738307861a513c108277aed8 Mon Sep 17 00:00:00 2001 From: danstarns Date: Sun, 29 Sep 2024 12:33:01 -0700 Subject: [PATCH] feat: data source mutable --- .../core/src/data_sources/model/DataRecord.ts | 7 + .../core/src/data_sources/model/DataSource.ts | 5 + packages/core/src/data_sources/types.ts | 5 + .../core/test/specs/data_sources/mutable.ts | 128 ++++++++++++++++++ 4 files changed, 145 insertions(+) create mode 100644 packages/core/test/specs/data_sources/mutable.ts diff --git a/packages/core/src/data_sources/model/DataRecord.ts b/packages/core/src/data_sources/model/DataRecord.ts index bf64dbfcf..0ddbc7b64 100644 --- a/packages/core/src/data_sources/model/DataRecord.ts +++ b/packages/core/src/data_sources/model/DataRecord.ts @@ -32,8 +32,11 @@ import EditorModel from '../../editor/model/Editor'; import { _StringKey } from 'backbone'; export default class DataRecord extends Model { + public mutable: boolean; + constructor(props: T, opts = {}) { super(props, opts); + this.mutable = props.mutable ?? true; this.on('change', this.handleChange); } @@ -137,6 +140,10 @@ export default class DataRecord ext options?: SetOptions | undefined, ): this; set(attributeName: unknown, value?: unknown, options?: SetOptions): DataRecord { + if (!this.isNew() && this.attributes.mutable === false) { + throw new Error('Cannot modify immutable record'); + } + const onRecordSetValue = this.dataSource?.transformers?.onRecordSetValue; const applySet = (key: string, val: unknown) => { diff --git a/packages/core/src/data_sources/model/DataSource.ts b/packages/core/src/data_sources/model/DataSource.ts index 37543901b..6b894c431 100644 --- a/packages/core/src/data_sources/model/DataSource.ts +++ b/packages/core/src/data_sources/model/DataSource.ts @@ -152,6 +152,11 @@ export default class DataSource extends Model { * @name removeRecord */ removeRecord(id: string | number, opts?: RemoveOptions): DataRecord | undefined { + const record = this.getRecord(id); + if (record?.mutable === false) { + throw new Error('Cannot remove immutable record'); + } + return this.records.remove(id, opts); } diff --git a/packages/core/src/data_sources/types.ts b/packages/core/src/data_sources/types.ts index ecbbedeab..3b23326e6 100644 --- a/packages/core/src/data_sources/types.ts +++ b/packages/core/src/data_sources/types.ts @@ -7,6 +7,11 @@ export interface DataRecordProps extends ObjectAny { * Record id. */ id: string; + + /** + * Specifies if the record is mutable. Defaults to `true`. + */ + mutable?: boolean; } export interface DataVariableListener { diff --git a/packages/core/test/specs/data_sources/mutable.ts b/packages/core/test/specs/data_sources/mutable.ts new file mode 100644 index 000000000..bf3bf0948 --- /dev/null +++ b/packages/core/test/specs/data_sources/mutable.ts @@ -0,0 +1,128 @@ +import DataSourceManager from '../../../src/data_sources'; +import { setupTestEditor } from '../../common'; +import EditorModel from '../../../src/editor/model/Editor'; + +describe('DataSource Immutability', () => { + let em: EditorModel; + let dsm: DataSourceManager; + + beforeEach(() => { + ({ em, dsm } = setupTestEditor()); + }); + + afterEach(() => { + em.destroy(); + }); + + test('set throws error for immutable record', () => { + const ds = dsm.add({ + id: 'testDs1', + records: [{ id: 'id1', name: 'Name1', value: 100, mutable: false }], + }); + const record = ds.getRecord('id1'); + + expect(() => record?.set('name', 'UpdatedName')).toThrow('Cannot modify immutable record'); + expect(record?.get('name')).toBe('Name1'); + }); + + test('set throws error for multiple attributes on immutable record', () => { + const ds = dsm.add({ + id: 'testDs2', + records: [{ id: 'id1', name: 'Name1', value: 100, mutable: false }], + }); + const record = ds.getRecord('id1'); + + expect(() => record?.set({ name: 'UpdatedName', value: 150 })).toThrow('Cannot modify immutable record'); + expect(record?.get('name')).toBe('Name1'); + expect(record?.get('value')).toBe(100); + }); + + test('removeRecord throws error for immutable record', () => { + const ds = dsm.add({ + id: 'testDs3', + records: [{ id: 'id1', name: 'Name1', value: 100, mutable: false }], + }); + + expect(() => ds.removeRecord('id1')).toThrow('Cannot remove immutable record'); + expect(ds.getRecord('id1')).toBeTruthy(); + }); + + test('addRecord creates an immutable record', () => { + const ds = dsm.add({ + id: 'testDs4', + records: [], + }); + + ds.addRecord({ id: 'id1', name: 'Name1', value: 100, mutable: false }); + const newRecord = ds.getRecord('id1'); + + expect(() => newRecord?.set('name', 'UpdatedName')).toThrow('Cannot modify immutable record'); + expect(newRecord?.get('name')).toBe('Name1'); + }); + + test('setRecords replaces all records with immutable ones', () => { + const ds = dsm.add({ + id: 'testDs5', + records: [], + }); + + ds.setRecords([ + { id: 'id1', name: 'Name1', value: 100, mutable: false }, + { id: 'id2', name: 'Name2', value: 200, mutable: false }, + ]); + + const record1 = ds.getRecord('id1'); + const record2 = ds.getRecord('id2'); + + expect(() => record1?.set('name', 'UpdatedName1')).toThrow('Cannot modify immutable record'); + expect(() => record2?.set('name', 'UpdatedName2')).toThrow('Cannot modify immutable record'); + expect(record1?.get('name')).toBe('Name1'); + expect(record2?.get('name')).toBe('Name2'); + }); + + test('batch update throws error for immutable records', () => { + const ds = dsm.add({ + id: 'testDs6', + records: [ + { id: 'id1', name: 'Name1', value: 100, mutable: false }, + { id: 'id2', name: 'Name2', value: 200, mutable: false }, + ], + }); + + expect(() => { + ds.records.set([ + { id: 'id1', name: 'BatchUpdate1' }, + { id: 'id2', name: 'BatchUpdate2' }, + ]); + }).toThrow('Cannot modify immutable record'); + + expect(ds.getRecord('id1')?.get('name')).toBe('Name1'); + expect(ds.getRecord('id2')?.get('name')).toBe('Name2'); + }); + + test('nested property update throws error for immutable record', () => { + const ds = dsm.add({ + id: 'testDs7', + records: [{ id: 'nested-id', nested: { prop: 'NestedValue' }, mutable: false }], + }); + const record = ds.getRecord('nested-id'); + + expect(() => record?.set('nested.prop', 'UpdatedNestedValue')).toThrow('Cannot modify immutable record'); + }); + + test('record remains immutable after serialization and deserialization', () => { + const ds = dsm.add({ + id: 'testDs8', + records: [{ id: 'id1', name: 'Name1', value: 100, mutable: false }], + }); + const serialized = JSON.parse(JSON.stringify(ds.toJSON())); + + dsm.remove(ds.id as string); + const newDs = dsm.add(serialized); + + const record = newDs.getRecord('id1'); + + expect(() => record?.set('name', 'SerializedUpdate')).toThrow('Cannot modify immutable record'); + expect(record?.get('name')).toBe('Name1'); + }); +});