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: data source mutable #6176

Merged
merged 3 commits into from
Sep 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
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
4 changes: 3 additions & 1 deletion docs/api/data_source_manager.md
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ data record, and optional property path.

Store data sources to a JSON object.

Returns **[Object][6]** Stored data sources.
Returns **[Array][8]** Stored data sources.

## load

Expand All @@ -155,3 +155,5 @@ Returns **[Object][6]** Loaded data sources.
[6]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object

[7]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String

[8]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array
24 changes: 24 additions & 0 deletions docs/modules/DataSources.md
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,30 @@ console.log(loadedDataSource.getRecord('id1').get('content')); // Outputs: "This

Remember that DataSources with `skipFromStorage: true` will not be available after a project is loaded unless you add them programmatically.


## Record Mutability

DataSource records are mutable by default, but can be set as immutable to prevent modifications. Use the mutable flag when creating records to control this behavior.

```ts
const dataSource = {
id: 'my-datasource',
records: [
{ id: 'id1', content: 'Mutable content' },
{ id: 'id2', content: 'Immutable content', mutable: false },
],
};


editor.DataSources.add(dataSource);

const ds = editor.DataSources.get('my-datasource');
ds.getRecord('id1').set('content', 'Updated content'); // Succeeds
ds.getRecord('id2').set('content', 'New content'); // Throws error
```

Immutable records cannot be modified or removed, ensuring data integrity for critical information.

## Benefits of Using DataSources

DataSources are integrated with GrapesJS's runtime and BackboneJS models, enabling dynamic updates and synchronization between your data and UI components. This allows you to:
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/common/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export type UndoOptions = { fromUndo?: boolean };

export type WithHTMLParserOptions = { parserOptions?: HTMLParserOptions };

export type RemoveOptions = Backbone.Silenceable & UndoOptions;
export type RemoveOptions = Backbone.Silenceable & UndoOptions & { dangerously?: boolean };

export type EventHandler = Backbone.EventHandler;

Expand Down
7 changes: 7 additions & 0 deletions packages/core/src/data_sources/model/DataRecord.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,11 @@ import EditorModel from '../../editor/model/Editor';
import { _StringKey } from 'backbone';

export default class DataRecord<T extends DataRecordProps = DataRecordProps> extends Model<T> {
public mutable: boolean;

constructor(props: T, opts = {}) {
super(props, opts);
this.mutable = props.mutable ?? true;
this.on('change', this.handleChange);
}

Expand Down Expand Up @@ -137,6 +140,10 @@ export default class DataRecord<T extends DataRecordProps = DataRecordProps> 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) => {
Expand Down
5 changes: 5 additions & 0 deletions packages/core/src/data_sources/model/DataSource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,11 @@ export default class DataSource extends Model<DataSourceProps> {
* @name removeRecord
*/
removeRecord(id: string | number, opts?: RemoveOptions): DataRecord | undefined {
const record = this.getRecord(id);
if (record?.mutable === false && !opts?.dangerously) {
throw new Error('Cannot remove immutable record');
}

return this.records.remove(id, opts);
}

Expand Down
5 changes: 5 additions & 0 deletions packages/core/src/data_sources/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
128 changes: 128 additions & 0 deletions packages/core/test/specs/data_sources/mutable.ts
Original file line number Diff line number Diff line change
@@ -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');
});
});