Skip to content

Commit

Permalink
feat (internal): support legacy model behaviors in SchemaRecord legac…
Browse files Browse the repository at this point in the history
…y mode (#9095)

* more progress

* more features

* all the things

* references

* more impls

* implement more tests

* add more tests

* make sure lifecycle flags work

* more tests

* more tests

* implement destroy as locals

* fix

* fix tests

* fix prod test

* fix prettier
  • Loading branch information
runspired committed Nov 11, 2023
1 parent 7df83ce commit 73f7ae6
Show file tree
Hide file tree
Showing 34 changed files with 1,166 additions and 237 deletions.
4 changes: 3 additions & 1 deletion config/eslint/qunit.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@ function defaults(config = {}) {
],
}),
config?.rules,
{}
{
'qunit/no-ok-equality': 'off',
}
),
};
}
Expand Down
25 changes: 23 additions & 2 deletions config/rollup/external.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,30 @@ function external(manual = []) {
const peers = Object.keys(pkg.peerDependencies || {});
const all = new Set([...deps, ...peers, ...manual]);

const result = [...all.keys()];
// console.log({ externals: result });
return result;
return function (id) {
if (all.has(id)) {
return true;
}

for (const dep of deps) {
if (id.startsWith(dep + '/')) {
return true;
}
}

for (const dep of peers) {
if (id.startsWith(dep + '/')) {
return true;
}
}

if (id.startsWith('@ember/') || id.startsWith('@ember-data/') || id.startsWith('@warp-drive/')) {
throw new Error(`Unexpected import: ${id}`);
}

return false;
};
}

module.exports = {
Expand Down
2 changes: 1 addition & 1 deletion packages/core-types/rollup.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export default {
plugins: [
// These are the modules that users should be able to import from your
// addon. Anything not listed here may get optimized away.
addon.publicEntrypoints(['index.js', 'identifier.js', 'request.js']),
addon.publicEntrypoints(['index.js', 'identifier.js', 'request.js', 'symbols.js']),

nodeResolve({ extensions: ['.ts'] }),
babel({
Expand Down
1 change: 1 addition & 0 deletions packages/core-types/src/symbols.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const RecordStore = Symbol('Store');
2 changes: 1 addition & 1 deletion packages/legacy-compat/rollup.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export default {
// You can augment this if you need to.
output: addon.output(),

external: external(['@ember/debug', '@embroider/macros', '@ember-data/store/-private']),
external: external(['@ember/debug', '@ember/application', '@embroider/macros', '@ember-data/store/-private']),

plugins: [
// These are the modules that users should be able to import from your
Expand Down
2 changes: 1 addition & 1 deletion packages/model/rollup.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ export default {
plugins: [
// These are the modules that users should be able to import from your
// addon. Anything not listed here may get optimized away.
addon.publicEntrypoints(['index.js', '-private.js', 'hooks.js']),
addon.publicEntrypoints(['index.js', '-private.js', 'hooks.js', 'migration-support.js']),

nodeResolve({ extensions: ['.ts', '.js'] }),
babel({
Expand Down
4 changes: 2 additions & 2 deletions packages/model/src/-private.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,5 @@ export { default as ManyArray } from './-private/many-array';
export { default as PromiseBelongsTo } from './-private/promise-belongs-to';
export { default as PromiseManyArray } from './-private/promise-many-array';

// // Used by tests
export { LEGACY_SUPPORT } from './-private/model';
// // Used by tests, migration support
export { lookupLegacySupport, LEGACY_SUPPORT } from './-private/legacy-relationships-support';
2 changes: 1 addition & 1 deletion packages/model/src/-private/belongs-to.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { computed } from '@ember/object';

import { DEBUG } from '@ember-data/env';

import { lookupLegacySupport } from './model';
import { lookupLegacySupport } from './legacy-relationships-support';
import { computedMacroWithOptionalParams, normalizeModelName } from './util';

/**
Expand Down
2 changes: 1 addition & 1 deletion packages/model/src/-private/has-many.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { singularize } from 'ember-inflector';
import { DEPRECATE_NON_STRICT_TYPES } from '@ember-data/deprecations';
import { DEBUG } from '@ember-data/env';

import { lookupLegacySupport } from './model';
import { lookupLegacySupport } from './legacy-relationships-support';
import { computedMacroWithOptionalParams } from './util';

function normalizeType(type) {
Expand Down
23 changes: 20 additions & 3 deletions packages/model/src/-private/legacy-relationships-support.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ import type { LocalRelationshipOperation } from '@warp-drive/core-types/graph';
import type { CollectionResourceRelationship, SingleResourceRelationship } from '@warp-drive/core-types/spec/raw';

import RelatedCollection from './many-array';
import type Model from './model';
import type { MinimalLegacyRecord } from './model-methods';
import type { BelongsToProxyCreateArgs, BelongsToProxyMeta } from './promise-belongs-to';
import PromiseBelongsTo from './promise-belongs-to';
import type { HasManyProxyCreateArgs } from './promise-many-array';
Expand All @@ -38,8 +38,25 @@ import HasManyReference from './references/has-many';

type PromiseBelongsToFactory = { create(args: BelongsToProxyCreateArgs): PromiseBelongsTo };

export const LEGACY_SUPPORT: Map<StableRecordIdentifier | MinimalLegacyRecord, LegacySupport> = new Map();

export function lookupLegacySupport(record: MinimalLegacyRecord): LegacySupport {
const identifier = recordIdentifierFor(record);
assert(`Expected a record`, identifier);
let support = LEGACY_SUPPORT.get(identifier);

if (!support) {
assert(`Memory Leak Detected`, !record.isDestroyed && !record.isDestroying);
support = new LegacySupport(record);
LEGACY_SUPPORT.set(identifier, support);
LEGACY_SUPPORT.set(record, support);
}

return support;
}

export class LegacySupport {
declare record: Model;
declare record: MinimalLegacyRecord;
declare store: Store;
declare graph: Graph;
declare cache: Cache;
Expand All @@ -53,7 +70,7 @@ export class LegacySupport {
declare isDestroying: boolean;
declare isDestroyed: boolean;

constructor(record: Model) {
constructor(record: MinimalLegacyRecord) {
this.record = record;
this.store = storeFor(record)!;
this.identifier = recordIdentifierFor(record);
Expand Down
135 changes: 135 additions & 0 deletions packages/model/src/-private/model-methods.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
import { assert } from '@ember/debug';

import { importSync } from '@embroider/macros';

import { upgradeStore } from '@ember-data/legacy-compat/-private';
import Store, { recordIdentifierFor } from '@ember-data/store';
import { peekCache } from '@ember-data/store/-private';
import { RecordStore } from '@warp-drive/core-types/symbols';

import Errors from './errors';
import { lookupLegacySupport } from './legacy-relationships-support';
import RecordState from './record-state';

export interface MinimalLegacyRecord {
errors: Errors;
___recordState: RecordState;
currentState: RecordState;
isDestroyed: boolean;
isDestroying: boolean;
isReloading: boolean;
[RecordStore]: Store;

deleteRecord(): void;
unloadRecord(): void;
save<T extends MinimalLegacyRecord>(this: T, options?: Record<string, unknown>): Promise<T>;
destroyRecord<T extends MinimalLegacyRecord>(this: T, options?: Record<string, unknown>): Promise<T>;
}

export function rollbackAttributes(this: MinimalLegacyRecord) {
const { currentState } = this;
const { isNew } = currentState;

this[RecordStore]._join(() => {
peekCache(this).rollbackAttrs(recordIdentifierFor(this));
this.errors.clear();
currentState.cleanErrorRequests();
if (isNew) {
this.unloadRecord();
}
});
}

export function unloadRecord(this: MinimalLegacyRecord) {
if (this.currentState.isNew && (this.isDestroyed || this.isDestroying)) {
return;
}
this[RecordStore].unloadRecord(this);
}

export function belongsTo(this: MinimalLegacyRecord, prop: string) {
return lookupLegacySupport(this).referenceFor('belongsTo', prop);
}

export function hasMany(this: MinimalLegacyRecord, prop: string) {
return lookupLegacySupport(this).referenceFor('hasMany', prop);
}

export function reload(this: MinimalLegacyRecord, options: Record<string, unknown> = {}) {
options.isReloading = true;
options.reload = true;

const identifier = recordIdentifierFor(this);
assert(`You cannot reload a record without an ID`, identifier.id);

this.isReloading = true;
const promise = this[RecordStore].request({
op: 'findRecord',
data: {
options,
record: identifier,
},
cacheOptions: { [Symbol.for('wd:skip-cache')]: true },
})
.then(() => this)
.finally(() => {
this.isReloading = false;
});

return promise;
}

export function changedAttributes(this: MinimalLegacyRecord) {
return peekCache(this).changedAttrs(recordIdentifierFor(this));
}

export function serialize(this: MinimalLegacyRecord, options?: Record<string, unknown>) {
upgradeStore(this[RecordStore]);
return this[RecordStore].serializeRecord(this, options);
}

export function deleteRecord(this: MinimalLegacyRecord) {
// ensure we've populated currentState prior to deleting a new record
if (this.currentState) {
this[RecordStore].deleteRecord(this);
}
}

export function save<T extends MinimalLegacyRecord>(this: T, options?: Record<string, unknown>): Promise<T> {
let promise: Promise<T>;

if (this.currentState.isNew && this.currentState.isDeleted) {
promise = Promise.resolve(this);
} else {
this.errors.clear();
promise = this[RecordStore].saveRecord(this, options) as Promise<T>;
}

return promise;
}

export function destroyRecord(this: MinimalLegacyRecord, options?: Record<string, unknown>) {
const { isNew } = this.currentState;
this.deleteRecord();
if (isNew) {
return Promise.resolve(this);
}
return this.save(options).then((_) => {
this.unloadRecord();
return this;
});
}

export function createSnapshot(this: MinimalLegacyRecord) {
const store = this[RecordStore];

upgradeStore(store);
if (!store._fetchManager) {
const FetchManager = (
importSync('@ember-data/legacy-compat/-private') as typeof import('@ember-data/legacy-compat/-private')
).FetchManager;
store._fetchManager = new FetchManager(store);
}

return store._fetchManager.createSnapshot(recordIdentifierFor(this));
}
Loading

0 comments on commit 73f7ae6

Please sign in to comment.