Browse Source

Merge branch 'dev' into refactor-sorter

refactor-sorter^2
Artur Arseniev 1 year ago
committed by GitHub
parent
commit
20e32dc445
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 4
      docs/api/data_source_manager.md
  2. 24
      docs/modules/DataSources.md
  3. 2
      packages/core/src/common/index.ts
  4. 7
      packages/core/src/data_sources/model/DataRecord.ts
  5. 5
      packages/core/src/data_sources/model/DataSource.ts
  6. 5
      packages/core/src/data_sources/types.ts
  7. 128
      packages/core/test/specs/data_sources/mutable.ts

4
docs/api/data_source_manager.md

@ -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
@ -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
docs/modules/DataSources.md

@ -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:

2
packages/core/src/common/index.ts

@ -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;

7
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<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);
}
@ -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) => {

5
packages/core/src/data_sources/model/DataSource.ts

@ -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);
}

5
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 {

128
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');
});
});
Loading…
Cancel
Save