Skip to content

Commit

Permalink
fix: ensure SchemaRecord immutability (#9540)
Browse files Browse the repository at this point in the history
* fix: ensure SchemaRecord immutability

* Fix existing tests to make checkout work and test immutability

* Add immutability tests, fix immutability in cases that don't work

---------

Co-authored-by: Rich Glazerman <[email protected]>
  • Loading branch information
runspired and richgt authored Oct 19, 2024
1 parent a078cd6 commit 857175a
Show file tree
Hide file tree
Showing 7 changed files with 2,289 additions and 54 deletions.
39 changes: 35 additions & 4 deletions packages/schema-record/src/-private/managed-array.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,9 +46,12 @@ const SYNC_PROPS = new Set<KeyType>(['[]', 'length']);
function isArrayGetter<T>(prop: KeyType): prop is keyof Array<T> {
return ARRAY_GETTER_METHODS.has(prop);
}
// function isArraySetter<T>(prop: KeyType): prop is keyof Array<T> {
// return ARRAY_SETTER_METHODS.has(prop);
// }
const ARRAY_SETTER_METHODS = new Set<KeyType>(['push', 'pop', 'unshift', 'shift', 'splice', 'sort']);

function isArraySetter<T>(prop: KeyType): prop is keyof Array<T> {
return ARRAY_SETTER_METHODS.has(prop);
}

// function isSelfProp<T extends object>(self: T, prop: KeyType): prop is keyof T {
// return prop in self;
// }
Expand Down Expand Up @@ -129,7 +132,7 @@ export class ManagedArray {
const self = this;
this[SOURCE] = data?.slice();
this[ARRAY_SIGNAL] = createSignal(this, 'length');
this[Editable] = editable;
const IS_EDITABLE = (this[Editable] = editable ?? false);
this[Legacy] = legacy;
const _SIGNAL = this[ARRAY_SIGNAL];
const boundFns = new Map<KeyType, ProxiedMethod>();
Expand Down Expand Up @@ -286,9 +289,37 @@ export class ManagedArray {
return fn;
}

if (isArraySetter(prop)) {
let fn = boundFns.get(prop);

if (fn === undefined) {
fn = function () {
if (!IS_EDITABLE) {
throw new Error(
`Mutating this array via ${String(prop)} is not allowed because the record is not editable`
);
}
subscribe(_SIGNAL);
transaction = true;
const result = Reflect.apply(target[prop] as ProxiedMethod, receiver, arguments) as unknown;
transaction = false;
return result;
};
boundFns.set(prop, fn);
}
return fn;
}

return Reflect.get(target, prop, receiver);
},
set(target, prop: KeyType, value, receiver) {
if (!IS_EDITABLE) {
let errorPath = identifier.type;
if (path) {
errorPath = path[path.length - 1];
}
throw new Error(`Cannot set ${String(prop)} on ${errorPath} because the record is not editable`);
}
if (prop === 'identifier') {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
self.identifier = value;
Expand Down
2 changes: 1 addition & 1 deletion packages/schema-record/src/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export function instantiateRecord(
): SchemaRecord {
const schema = store.schema as unknown as SchemaService;
const isLegacy = schema.resource(identifier)?.legacy ?? false;
const isEditable = isLegacy || Boolean(createArgs);
const isEditable = isLegacy || store.cache.isNew(identifier);
const record = new SchemaRecord(store, identifier, {
[Editable]: isEditable,
[Legacy]: isLegacy,
Expand Down
34 changes: 31 additions & 3 deletions packages/schema-record/src/record.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { dependencySatisfies, importSync, macroCondition } from '@embroider/macr
import type { MinimalLegacyRecord } from '@ember-data/model/-private/model-methods';
import type Store from '@ember-data/store';
import type { NotificationType } from '@ember-data/store';
import { recordIdentifierFor, setRecordIdentifier } from '@ember-data/store/-private';
import { addToTransaction, entangleSignal, getSignal, type Signal, Signals } from '@ember-data/tracking/-private';
import { assert } from '@warp-drive/build-config/macros';
import type { StableRecordIdentifier } from '@warp-drive/core-types';
Expand Down Expand Up @@ -44,7 +45,7 @@ const getLegacySupport = macroCondition(dependencySatisfies('@ember-data/model',
? (importSync('@ember-data/model/-private') as typeof import('@ember-data/model/-private')).lookupLegacySupport
: null;

export { Editable, Legacy } from './symbols';
export { Editable, Legacy, Checkout } from './symbols';
const IgnoredGlobalFields = new Set<string>(['length', 'nodeType', 'then', 'setInterval', 'document', STRUCTURED]);
const symbolList = [
Destroy,
Expand All @@ -66,6 +67,7 @@ function isPathMatch(a: string[], b: string[]) {
return a.length === b.length && a.every((v, i) => v === b[i]);
}

const Editables = new WeakMap<SchemaRecord, SchemaRecord>();
export class SchemaRecord {
declare [RecordStore]: Store;
declare [Identifier]: StableRecordIdentifier;
Expand Down Expand Up @@ -333,7 +335,8 @@ export class SchemaRecord {
},
set(target: SchemaRecord, prop: string | number | symbol, value: unknown, receiver: typeof Proxy<SchemaRecord>) {
if (!IS_EDITABLE) {
throw new Error(`Cannot set ${String(prop)} on ${identifier.type} because the record is not editable`);
const type = isEmbedded ? embeddedType : identifier.type;
throw new Error(`Cannot set ${String(prop)} on ${type} because the record is not editable`);
}

const maybeField = prop === identityField?.name ? identityField : fields.get(prop as string);
Expand Down Expand Up @@ -655,6 +658,31 @@ export class SchemaRecord {
this[RecordStore].notifications.unsubscribe(this.___notifications);
}
[Checkout](): Promise<SchemaRecord> {
return Promise.resolve(this);
const editable = Editables.get(this);
if (editable) {
return Promise.resolve(editable);
}

const embeddedType = this[EmbeddedType];
const embeddedPath = this[EmbeddedPath];
const isEmbedded = embeddedType !== null && embeddedPath !== null;

if (isEmbedded) {
throw new Error(`Cannot checkout an embedded record (yet)`);
}

const editableRecord = new SchemaRecord(
this[RecordStore],
this[Identifier],
{
[Editable]: true,
[Legacy]: this[Legacy],
},
isEmbedded,
embeddedType,
embeddedPath
);
setRecordIdentifier(editableRecord, recordIdentifierFor(this));
return Promise.resolve(editableRecord);
}
}
Loading

0 comments on commit 857175a

Please sign in to comment.